diff --git a/packages/web/src/app/sessions/[id]/page.test.tsx b/packages/web/src/app/sessions/[id]/page.test.tsx index ec96b721c..45eb15790 100644 --- a/packages/web/src/app/sessions/[id]/page.test.tsx +++ b/packages/web/src/app/sessions/[id]/page.test.tsx @@ -44,6 +44,16 @@ describe("SessionPage project polling", () => { beforeEach(() => { vi.useFakeTimers(); sessionDetailSpy.mockClear(); + + const eventSourceMock = { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + close: vi.fn(), + readyState: 1, + }; + global.EventSource = vi.fn( + () => eventSourceMock as unknown as EventSource, + ) as unknown as typeof EventSource; }); afterEach(() => { diff --git a/packages/web/src/app/sessions/[id]/page.tsx b/packages/web/src/app/sessions/[id]/page.tsx index 453b6782c..fc8c33b00 100644 --- a/packages/web/src/app/sessions/[id]/page.tsx +++ b/packages/web/src/app/sessions/[id]/page.tsx @@ -4,17 +4,22 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { useParams } from "next/navigation"; import { isOrchestratorSession } from "@composio/ao-core/types"; import { SessionDetail } from "@/components/SessionDetail"; -import { type DashboardSession, getAttentionLevel, type AttentionLevel } from "@/lib/types"; +import { type DashboardSession, type ActivityState, getAttentionLevel, type AttentionLevel } from "@/lib/types"; import { activityIcon } from "@/lib/activity-icons"; +import { useSSESessionActivity } from "@/hooks/useSSESessionActivity"; function truncate(s: string, max: number): string { return s.length > max ? s.slice(0, max) + "..." : s; } /** Build a descriptive tab title from session data. */ -function buildSessionTitle(session: DashboardSession): string { +function buildSessionTitle( + session: DashboardSession, + activityOverride?: ActivityState | null, +): string { const id = session.id; - const emoji = session.activity ? (activityIcon[session.activity] ?? "") : ""; + const activity = activityOverride !== undefined ? activityOverride : session.activity; + const emoji = activity ? (activityIcon[activity] ?? "") : ""; const isOrchestrator = isOrchestratorSession(session); let detail: string; @@ -62,14 +67,17 @@ export default function SessionPage() { const sessionIsOrchestratorRef = useRef(false); const resolvedProjectSessionsKeyRef = useRef(null); - // Update document title based on session data + // Subscribe to SSE for real-time activity updates (title emoji) + const sseActivity = useSSESessionActivity(id); + + // Update document title based on session data + SSE activity override useEffect(() => { if (session) { - document.title = buildSessionTitle(session); + document.title = buildSessionTitle(session, sseActivity?.activity); } else { document.title = `${id} | Session Detail`; } - }, [session, id]); + }, [session, id, sseActivity]); useEffect(() => { sessionProjectIdRef.current = sessionProjectId; diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index 0ec40bc1f..3dbb05adf 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -14,7 +14,7 @@ import { isPRMergeReady, } from "@/lib/types"; import { AttentionZone } from "./AttentionZone"; -import { DynamicFavicon } from "./DynamicFavicon"; +import { DynamicFavicon, countNeedingAttention } from "./DynamicFavicon"; import { useSessionEvents } from "@/hooks/useSessionEvents"; import { ProjectSidebar } from "./ProjectSidebar"; import { ThemeToggle } from "./ThemeToggle"; @@ -72,10 +72,18 @@ function DashboardInner({ orchestrators, }: DashboardProps) { const orchestratorLinks = orchestrators ?? EMPTY_ORCHESTRATORS; - const { sessions, globalPause, connectionStatus } = useSessionEvents( + const initialAttentionLevels = useMemo(() => { + const levels: Record = {}; + for (const s of initialSessions) { + levels[s.id] = getAttentionLevel(s); + } + return levels; + }, [initialSessions]); + const { sessions, globalPause, connectionStatus, sseAttentionLevels } = useSessionEvents( initialSessions, initialGlobalPause, projectId, + initialAttentionLevels, ); const searchParams = useSearchParams(); const activeSessionId = searchParams.get("session") ?? undefined; @@ -139,6 +147,13 @@ function DashboardInner({ setActiveOrchestrators((current) => mergeOrchestrators(current, orchestratorLinks)); }, [orchestratorLinks]); + // Update document title with live attention counts from SSE + useEffect(() => { + const needsAttention = countNeedingAttention(sseAttentionLevels); + const label = projectName ?? "ao"; + document.title = needsAttention > 0 ? `${label} (${needsAttention} need attention)` : label; + }, [sseAttentionLevels, projectName]); + useEffect(() => { setMobileMenuOpen(false); }, [searchParams]); @@ -494,7 +509,7 @@ function DashboardInner({ )}