// v2.6 Phase 1-UX §9b — chat-scoped agent-session state. // // Reads GET /api/coder/sessions/:id/agent-sessions (the per-(chat,agent) // backend-session rows) and drives the AgentComposerBar resumed/new-session // chip. Module-singleton external store keyed by sessionId — same shape as // useProviderSnapshot — so the two consumers (CoderPane, which owns the // message_complete WS signal, and AgentComposerBar, which renders the chip) // share one cache and one fetch per chat. CoderPane calls // refreshAgentSessions(sessionId) on each message_complete (the same trigger // usePendingChanges already keys off); the chip then reflects the freshly // resumed/created session. import { useEffect, useSyncExternalStore } from 'react'; import { api, type AgentSessionInfo } from '@/api/client'; type Entry = { data: AgentSessionInfo[]; inflight: Promise | null; }; const store = new Map(); const listeners = new Set<() => void>(); const EMPTY: AgentSessionInfo[] = []; function notify(): void { for (const fn of listeners) fn(); } function subscribe(fn: () => void): () => void { listeners.add(fn); return () => listeners.delete(fn); } function getEntry(sessionId: string): Entry { let entry = store.get(sessionId); if (!entry) { entry = { data: EMPTY, inflight: null }; store.set(sessionId, entry); } return entry; } async function doFetch(sessionId: string): Promise { const data = await api.coder.agentSessions(sessionId); const entry = getEntry(sessionId); entry.data = data; entry.inflight = null; notify(); return data; } function ensureLoaded(sessionId: string): void { const entry = getEntry(sessionId); if (entry.data !== EMPTY || entry.inflight) return; entry.inflight = doFetch(sessionId).catch(() => { // boocoder may be down or the chat has no agent-session rows yet; treat as // empty (the chip falls back to "new session" / hides). const e = getEntry(sessionId); e.inflight = null; return EMPTY; }); } /** Force a refetch for one chat. Wired to message_complete by CoderPane. */ export function refreshAgentSessions(sessionId: string): Promise { const entry = getEntry(sessionId); entry.inflight = null; return doFetch(sessionId); } /** * Chat-scoped agent-session rows. Pass `undefined` to opt out (no fetch, empty * result) — AgentComposerBar does this for BooChat callers and fresh chats so * the chip stays hidden. Fetches on mount (and on sessionId change); refetch on * message_complete is driven externally via refreshAgentSessions. */ export function useAgentSessions(sessionId: string | undefined): { sessions: AgentSessionInfo[]; } { const sessions = useSyncExternalStore( subscribe, () => (sessionId ? getEntry(sessionId).data : EMPTY), ); useEffect(() => { if (sessionId) ensureLoaded(sessionId); }, [sessionId]); return { sessions: sessionId ? sessions : EMPTY }; }