DiffPanel renders a per-row agent badge (icon+label; null -> 'manual') + a 'Changes from X, Y' note when the pending set spans >1 agent. AgentComposerBar gains an optional sessionId prop -> resumed/history/new-session chip beside the Provider picker (gated, so BooChat callers are unchanged), driven by a new useAgentSessions hook (refetch on message-complete). providerIcon extracted to shared components/coder/providerIcons.tsx; api.coder gains agentSessions(sessionId); PendingChange type gains agent. web tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
// 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<AgentSessionInfo[]> | null;
|
|
};
|
|
|
|
const store = new Map<string, Entry>();
|
|
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<AgentSessionInfo[]> {
|
|
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<AgentSessionInfo[]> {
|
|
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 };
|
|
}
|