import { useEffect, useRef } from "react"; import type { SessionStatus } from "@opengeni/sdk"; import { useEmbedded } from "./app-host"; /** Tab-title marker for an open session that settled while the tab was in the background. */ export const ATTENTION_TITLE_PREFIX = "● "; const BUSY_STATUSES: ReadonlySet = new Set([ "queued", "running", "recovering", "waiting_capacity", ]); // Finished (idle, including an agent wait for input), failed, or waiting on the // user for an approval or answer. Cancellation is the user's own action. const ATTENTION_STATUSES: ReadonlySet = new Set([ "idle", "failed", "requires_action", ]); /** True when a working session settles into a state the user should look at. */ export function isAttentionTransition( previous: SessionStatus | null, next: SessionStatus | null, ): boolean { return ( previous !== null && next !== null && BUSY_STATUSES.has(previous) && ATTENTION_STATUSES.has(next) ); } export function withAttentionPrefix(title: string): string { return title.startsWith(ATTENTION_TITLE_PREFIX) ? title : `${ATTENTION_TITLE_PREFIX}${title}`; } export function withoutAttentionPrefix(title: string): string { return title.startsWith(ATTENTION_TITLE_PREFIX) ? title.slice(ATTENTION_TITLE_PREFIX.length) : title; } function inBackground(doc: Document): boolean { return doc.visibilityState === "hidden" || !doc.hasFocus(); } /** * Marks the document title when the open session finishes, fails, or needs * input while the tab is hidden or unfocused, and clears the mark as soon as * the tab is back in front. Presentation only: it never reads or writes * session state. */ export function useBackgroundAttentionTitle(sessionId: string, status: SessionStatus | null) { const previous = useRef<{ sessionId: string; status: SessionStatus | null } | null>(null); // An embedded app never renames the page around it. const embedded = useEmbedded(); useEffect(() => { if (embedded) return; const prior = previous.current; previous.current = { sessionId, status }; if (!prior || prior.sessionId !== sessionId) return; if (!isAttentionTransition(prior.status, status) || !inBackground(document)) return; document.title = withAttentionPrefix(document.title); }, [embedded, sessionId, status]); useEffect(() => { if (embedded) return; const clear = () => { if (!inBackground(document)) document.title = withoutAttentionPrefix(document.title); }; // Returning by clicking straight into an embedded frame (desktop stream, // Site preview) sends the parent no window "focus"; the frame taking focus // still reaches it as "blur"/"focusin", and any other click as "pointerdown". const windowEvents = ["focus", "blur"] as const; const documentEvents = ["visibilitychange", "focusin", "pointerdown"] as const; for (const type of windowEvents) window.addEventListener(type, clear); for (const type of documentEvents) document.addEventListener(type, clear, true); return () => { for (const type of windowEvents) window.removeEventListener(type, clear); for (const type of documentEvents) document.removeEventListener(type, clear, true); document.title = withoutAttentionPrefix(document.title); }; }, [embedded]); }