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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
/**
|
|
* normalize-agent-status (#10) — clean-room vendor-event → bucket mapping.
|
|
*
|
|
* Different coding agents (claude, opencode, codex/gemini, goose, qwen) emit
|
|
* lifecycle hook events under inconsistent names: PascalCase (`SessionStart`),
|
|
* snake_case (`session_start`), camelCase (`sessionStart`), and a handful of
|
|
* provider-specific approval events (`exec_approval_request`). This module
|
|
* collapses every known event name into one of three coarse signals:
|
|
*
|
|
* working — the agent is actively progressing a turn
|
|
* blocked — the agent is waiting on a human (permission / approval / question)
|
|
* done — the turn / session ended cleanly
|
|
*
|
|
* `null` is returned for anything unrecognized so callers can ignore noise.
|
|
*
|
|
* Built now for the scoped status-publish, but specifically shaped for reuse by
|
|
* the documented config-injection follow-on: a future notify-hook injected into
|
|
* each agent's native config will POST the RAW vendor event name to a BooCoder
|
|
* endpoint, which runs this helper to derive the normalized status. The names
|
|
* below are facts about each agent's hook surface — not copied vendor code.
|
|
*/
|
|
|
|
export type AgentStatus = 'working' | 'blocked' | 'idle' | 'error';
|
|
|
|
/** The coarse signal a raw vendor event collapses to. */
|
|
export type AgentEventBucket = 'working' | 'blocked' | 'done';
|
|
|
|
// Each bucket lists the canonical vendor event names. Lookup is
|
|
// case-insensitive AND separator-insensitive (snake_case / camelCase /
|
|
// PascalCase all fold to the same key), so we normalize the raw input the same
|
|
// way before matching rather than enumerating every spelling here.
|
|
const WORKING_EVENTS = [
|
|
'SessionStart',
|
|
'UserPromptSubmit',
|
|
'UserPromptSubmitted',
|
|
'PostToolUse',
|
|
'PostToolUseFailure',
|
|
'BeforeAgent',
|
|
'AfterTool',
|
|
'task_started',
|
|
] as const;
|
|
|
|
const BLOCKED_EVENTS = [
|
|
'PreToolUse',
|
|
'Notification',
|
|
'PermissionRequest',
|
|
'exec_approval_request',
|
|
'apply_patch_approval_request',
|
|
'request_user_input',
|
|
] as const;
|
|
|
|
const DONE_EVENTS = [
|
|
'Stop',
|
|
'AfterAgent',
|
|
'SessionEnd',
|
|
'task_complete',
|
|
'agent-turn-complete',
|
|
] as const;
|
|
|
|
/**
|
|
* Fold a raw event name to a separator/case-insensitive key:
|
|
* strip every non-alphanumeric character and lowercase. So `post_tool_use`,
|
|
* `postToolUse`, `PostToolUse`, and `POST-TOOL-USE` all map to `posttooluse`.
|
|
*/
|
|
function foldKey(raw: string): string {
|
|
return raw.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
}
|
|
|
|
function buildLookup(
|
|
groups: ReadonlyArray<readonly [AgentEventBucket, readonly string[]]>,
|
|
): Map<string, AgentEventBucket> {
|
|
const map = new Map<string, AgentEventBucket>();
|
|
for (const [bucket, names] of groups) {
|
|
for (const name of names) map.set(foldKey(name), bucket);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
const EVENT_LOOKUP = buildLookup([
|
|
['working', WORKING_EVENTS],
|
|
['blocked', BLOCKED_EVENTS],
|
|
['done', DONE_EVENTS],
|
|
]);
|
|
|
|
/**
|
|
* Map a raw vendor hook-event name to its normalized bucket, or `null` when the
|
|
* name is unknown / undefined. Case- and separator-insensitive.
|
|
*/
|
|
export function normalizeAgentEvent(raw: string | undefined): AgentEventBucket | null {
|
|
if (!raw) return null;
|
|
return EVENT_LOOKUP.get(foldKey(raw)) ?? null;
|
|
}
|