import { useCallback, useMemo, useState } from 'react'; // Normalized external-agent status (#10). Consumed from the // `agent_status_updated` WS frame the coder backend publishes: // { type: 'agent_status_updated'; chat_id; agent; status; reason?; at } // BooCoder collapses ~30 vendor lifecycle events into these four buckets: // working — turn in flight // blocked — waiting on a permission / approval // idle — clean completion // error — crash / failure export type AgentStatus = 'working' | 'blocked' | 'idle' | 'error'; export interface AgentStatusEntry { status: AgentStatus; reason?: string; at: string; } const key = (chatId: string, agent: string): string => `${chatId}:${agent}`; // Per-(chat,agent) live status map. The dot reflects the latest frame for the // active agent in the current chat; entries are reset when the chat switches so // a stale "working"/"blocked" from a previous chat never leaks into the next. export function useAgentStatus() { const [map, setMap] = useState>({}); const record = useCallback( (chatId: string, agent: string, entry: AgentStatusEntry) => { setMap((prev) => ({ ...prev, [key(chatId, agent)]: entry })); }, [], ); // Drop every entry for a chat (called on chat switch). No-op when nothing // matches so it's safe to call unconditionally from an effect. const reset = useCallback((chatId: string | undefined) => { setMap((prev) => { if (!chatId) return prev; const prefix = `${chatId}:`; let changed = false; const next: Record = {}; for (const [k, v] of Object.entries(prev)) { if (k.startsWith(prefix)) { changed = true; continue; } next[k] = v; } return changed ? next : prev; }); }, []); const get = useCallback( (chatId: string | undefined, agent: string | undefined): AgentStatusEntry | null => { if (!chatId || !agent) return null; return map[key(chatId, agent)] ?? null; }, [map], ); return useMemo(() => ({ record, reset, get }), [record, reset, get]); }