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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Check, ChevronDown, RefreshCw, Loader2, Shield, Brain, Bot } from 'luci
|
||||
import { api } from '@/api/client';
|
||||
import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types';
|
||||
import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot';
|
||||
import type { AgentStatusEntry } from '@/hooks/useAgentStatus';
|
||||
import { providerIcon } from '@/components/coder/providerIcons';
|
||||
import { useAgentSessions } from '@/hooks/useAgentSessions';
|
||||
import {
|
||||
@@ -183,6 +184,11 @@ interface Props {
|
||||
// True once the chat has at least one prior turn — gates the chip so it stays
|
||||
// hidden on a brand-new chat. Defaults to false (no chip).
|
||||
hasPriorTurn?: boolean;
|
||||
// #10: normalized status (working|blocked|idle|error) for the active external
|
||||
// agent in this chat, or null for native boocode / before any frame. Renders
|
||||
// a status dot DISTINCT from the WS-liveness `connected` dot. Undefined for
|
||||
// non-coder callers — no dot.
|
||||
agentStatus?: AgentStatusEntry | null;
|
||||
}
|
||||
|
||||
// Condensed token count: 950 → "950", 12_400 → "12.4K", 3_200_000 → "3.2M".
|
||||
@@ -210,7 +216,42 @@ function relativeTime(iso: string | null): string {
|
||||
return `${day}d ago`;
|
||||
}
|
||||
|
||||
export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected, sessionId, hasPriorTurn }: Props) {
|
||||
// #10: normalized external-agent status dot. Mirrors StatusDot's visual
|
||||
// language but on the four normalized buckets (working|blocked|idle|error),
|
||||
// and is DISTINCT from the WS-liveness `connected` dot beside it:
|
||||
// working — emerald spinning ring (subtle motion, like chat streaming)
|
||||
// blocked — amber dot (matches the permission/blocked state colour)
|
||||
// idle — gray dot
|
||||
// error — red dot
|
||||
function AgentStatusDot({ entry, agent }: { entry: AgentStatusEntry; agent: string }) {
|
||||
const title =
|
||||
`${agent}: ${entry.status}` + (entry.reason ? ` — ${entry.reason}` : '');
|
||||
|
||||
if (entry.status === 'working') {
|
||||
return (
|
||||
<span
|
||||
aria-label={`Agent status: working${entry.reason ? ` — ${entry.reason}` : ''}`}
|
||||
title={title}
|
||||
className="inline-block w-3 h-3 rounded-full border-2 border-emerald-500 border-t-transparent animate-spin shrink-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bg =
|
||||
entry.status === 'blocked' ? 'bg-amber-500'
|
||||
: entry.status === 'error' ? 'bg-destructive'
|
||||
: 'bg-muted-foreground/40';
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-label={`Agent status: ${entry.status}${entry.reason ? ` — ${entry.reason}` : ''}`}
|
||||
title={title}
|
||||
className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', bg)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected, sessionId, hasPriorTurn, agentStatus }: Props) {
|
||||
const allEntries = useProviderSnapshot(projectPath);
|
||||
// 5.5 — the composer picker only offers ENABLED providers that are ready (or
|
||||
// still loading). Disabled (enabled:false) and unavailable/error providers are
|
||||
@@ -434,6 +475,11 @@ export function AgentComposerBar({ projectPath, value, onChange, onProviderComma
|
||||
{/* Status dot + refresh as one right-aligned unit so the refresh button
|
||||
stays on the top line instead of wrapping past the edge-pinned dot. */}
|
||||
<div className="ml-auto flex items-center gap-1 shrink-0">
|
||||
{/* #10: normalized agent status — only for an external agent with a
|
||||
live status frame. Distinct from the WS-liveness dot that follows. */}
|
||||
{agentStatus && value.provider !== 'boocode' && (
|
||||
<AgentStatusDot entry={agentStatus} agent={value.provider} />
|
||||
)}
|
||||
{connected !== undefined && (
|
||||
<span
|
||||
className={cn('inline-block w-1.5 h-1.5 rounded-full shrink-0', connected ? 'bg-green-500' : 'bg-red-500')}
|
||||
|
||||
Reference in New Issue
Block a user