feat: normalized external-agent status (#10 scoped) (v2.7.6)
Scoped half of boocode_code_review_v2 §1 #10 — publish the agent status BooCoder already observes (the config-injection notify-hook is the documented follow-on, clean-room from superset ELv2). - agent_status_updated WS frame (working|blocked|idle|error), server+web parity. - Published from the dispatcher's turn boundaries (warm-acp/opencode/sdk/pty: working at start, idle/error at end) + the permission flow (blocked/working). Best-effort, never breaks a turn. - Clean-room normalizeAgentEvent helper (superset's vendor-event -> Start/blocked /Stop collapse, event names as facts) + 25 tests — reused by the follow-on. - AgentComposerBar status dot (distinct from the WS-liveness dot), tracked per (chat,agent) by a useAgentStatus map in CoderPane. Built by 2 parallel agents vs a pinned frame contract. Server 545 + coder 294 tests passing (25 new); web tsc + builds clean; ws-frames parity green. Clears the actionable review backlog (#1/#3/#4/#6-#12). Builds on v2.7.5.
This commit is contained in:
62
apps/web/src/hooks/useAgentStatus.ts
Normal file
62
apps/web/src/hooks/useAgentStatus.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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<Record<string, AgentStatusEntry>>({});
|
||||
|
||||
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<string, AgentStatusEntry> = {};
|
||||
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]);
|
||||
}
|
||||
@@ -189,6 +189,12 @@ function applyFrame(state: State, frame: WsFrame): State {
|
||||
// duplicating async work inside a synchronous reducer.
|
||||
return state;
|
||||
}
|
||||
case 'agent_status_updated': {
|
||||
// agent-status-normalize (#10): coder-only frame consumed by CoderPane's
|
||||
// own WS handler, not BooChat's native message reducer. No-op here to keep
|
||||
// TS exhaustiveness satisfied (native sessions never emit it).
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user