import { useEffect, useRef, useState } from 'react' import { AlertTriangle, BrainCircuit, Check, Inbox, Loader2, Wrench } from 'lucide-react' import { api } from '@/lib/api' /** The live run record as returned by GET /api/assembler/runs/{id}. */ interface Run { id: string status: 'Queued' | 'Running' | 'Completed' | 'Failed' | string actionType: string | null actionRisk: string | null output: string | null error: string | null resultJson: string | null latencyMs: number | null createdAtUtc: string completedAtUtc: string | null } interface ToolCall { name?: string tool?: string server?: string ok?: boolean } const TERMINAL = new Set(['Completed', 'Failed']) /** Pulls the tool-call list out of the run's result JSON, if the agent used any MCP tools. */ function readToolCalls(run: Run | null): ToolCall[] { if (!run?.resultJson) return [] try { const parsed = JSON.parse(run.resultJson) as { toolCalls?: ToolCall[] } return Array.isArray(parsed.toolCalls) ? parsed.toolCalls : [] } catch { return [] } } /** * Watches one agent run live: polls the run until it reaches a terminal state and renders a * step-by-step timeline (Queued → Thinking → Done) with elapsed time, the action the agent took, * its risk tag, any tool calls, and where the result landed. */ export function RunProgress({ runId, onSettled, }: { runId: string /** Fired once when the run reaches Completed/Failed — lets the parent refresh the board + badge. */ onSettled?: (run: Run) => void }) { const [run, setRun] = useState(null) const [elapsed, setElapsed] = useState(0) const settledRef = useRef(false) const onSettledRef = useRef(onSettled) onSettledRef.current = onSettled useEffect(() => { let active = true let pollTimer = 0 const start = Date.now() const tick = window.setInterval(() => { if (active && !settledRef.current) setElapsed(Math.round((Date.now() - start) / 1000)) }, 1000) const poll = async () => { try { const r = await api.get(`/api/assembler/runs/${runId}`) if (!active) return setRun(r) if (TERMINAL.has(r.status)) { settledRef.current = true onSettledRef.current?.(r) return } } catch { /* transient — keep polling */ } if (active) pollTimer = window.setTimeout(poll, 1200) } void poll() return () => { active = false window.clearTimeout(pollTimer) window.clearInterval(tick) } }, [runId]) const status = run?.status ?? 'Queued' const failed = status === 'Failed' const done = status === 'Completed' const running = status === 'Running' const tools = readToolCalls(run) const seconds = run?.latencyMs != null ? (run.latencyMs / 1000).toFixed(1) : elapsed.toString() return (
Agent at work {seconds}s
    {tools.length > 0 && ( t.name ?? t.tool).filter(Boolean).join(', ')} /> )}
{done && run?.actionType && (
{run.actionType} {run.actionRisk && ( {run.actionRisk} risk )}
)} {done && run?.output && (
Show output
{run.output}
)}
) } type StepState = 'pending' | 'active' | 'done' | 'error' function Step({ state, icon: Icon, title, detail, }: { state: StepState icon: typeof Check title: string detail?: string }) { const ring = state === 'done' ? 'border-approved bg-approved/15 text-approved' : state === 'active' ? 'border-primary bg-primary/15 text-primary' : state === 'error' ? 'border-destructive bg-destructive/15 text-destructive' : 'border-border bg-muted/40 text-muted-foreground' return (
  • {state === 'active' ? : }

    {title}

    {detail &&

    {detail}

    }
  • ) }