import "./BackgroundTasksIndicator.css"; import { useState, useRef, useEffect, useMemo } from "react"; import { Lightbulb, Layers, Target, Loader2, HelpCircle, X, Lock, AlertCircle } from "lucide-react"; import type { AiSessionSummary } from "../api"; import { useAiSessionSync } from "../hooks/useAiSessionSync"; import { useConfirm } from "../hooks/useConfirm"; import { getSessionTabId } from "../utils/getSessionTabId"; interface BackgroundTasksIndicatorProps { sessions: AiSessionSummary[]; generating: number; needsInput: number; onOpenSession: (session: AiSessionSummary) => void; onDismissSession: (id: string) => void; } const TYPE_ICONS = { planning: Lightbulb, subtask: Layers, mission_interview: Target, milestone_interview: Target, slice_interview: Target, } as const; const TYPE_LABELS = { planning: "Planning", subtask: "Subtask Breakdown", mission_interview: "Mission Interview", milestone_interview: "Milestone Interview", slice_interview: "Slice Interview", } as const; export function BackgroundTasksIndicator({ sessions, generating, needsInput, onOpenSession, onDismissSession, }: BackgroundTasksIndicatorProps) { const { confirm } = useConfirm(); const [popoverOpen, setPopoverOpen] = useState(false); const [recentlyUpdated, setRecentlyUpdated] = useState>(new Set()); const containerRef = useRef(null); const previousSessionSignatureRef = useRef>(new Map()); const clearUpdatedTimerRef = useRef | null>(null); const { activeTabMap } = useAiSessionSync(); const localSessionTabId = useMemo(() => getSessionTabId(), []); // Close popover on outside click useEffect(() => { if (!popoverOpen) return; const handler = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setPopoverOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [popoverOpen]); // Animate per-item changes when session status/lock/timestamp changes. useEffect(() => { const changed = new Set(); const nextSignature = new Map(); for (const session of sessions) { const ownership = activeTabMap.get(session.id); const signature = [ session.status, session.updatedAt, ownership?.tabId ?? session.lockedByTab ?? "", ownership?.stale ? "stale" : "fresh", ].join("|"); const previous = previousSessionSignatureRef.current.get(session.id); if (previous && previous !== signature) { changed.add(session.id); } nextSignature.set(session.id, signature); } previousSessionSignatureRef.current = nextSignature; if (changed.size === 0) { return; } setRecentlyUpdated(changed); if (clearUpdatedTimerRef.current) { clearTimeout(clearUpdatedTimerRef.current); } clearUpdatedTimerRef.current = setTimeout(() => { setRecentlyUpdated(new Set()); clearUpdatedTimerRef.current = null; }, 500); return () => { if (clearUpdatedTimerRef.current) { clearTimeout(clearUpdatedTimerRef.current); clearUpdatedTimerRef.current = null; } }; }, [activeTabMap, sessions]); if (sessions.length === 0) return null; const total = sessions.length; const hasAttention = needsInput > 0; return (
{popoverOpen && (
Background Tasks
{sessions.map((session) => { const Icon = TYPE_ICONS[session.type]; const isGenerating = session.status === "generating"; const isAwaiting = session.status === "awaiting_input"; const isError = session.status === "error"; const activeTab = activeTabMap.get(session.id); const owningTabId = activeTab?.tabId ?? session.lockedByTab ?? null; const activeElsewhere = Boolean( owningTabId && owningTabId !== localSessionTabId && !activeTab?.stale, ); const isUpdated = recentlyUpdated.has(session.id); return (
{ if (activeElsewhere) { const shouldOpen = await confirm({ title: "Open Active Session", message: "This session is active in another tab. Open anyway?", }); if (!shouldOpen) { return; } } onOpenSession(session); setPopoverOpen(false); }} > {isError ? ( ) : ( )}
{session.title}
{isError ? "Failed" : TYPE_LABELS[session.type]} {isGenerating && " — generating..."} {isAwaiting && !activeElsewhere && " — needs input"} {isAwaiting && activeElsewhere && " — active in another tab"}
{isGenerating && ( )} {isAwaiting && !activeElsewhere && ( )} {isAwaiting && activeElsewhere && ( )}
); })}
)}
); }