diff --git a/apps/docs/src/components/kanban/AgentRunBar.tsx b/apps/docs/src/components/kanban/AgentRunBar.tsx index a7f50a0..6128c4d 100644 --- a/apps/docs/src/components/kanban/AgentRunBar.tsx +++ b/apps/docs/src/components/kanban/AgentRunBar.tsx @@ -1,16 +1,21 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { Card } from '../../lib/kanbanApi'; -import type { RunEvent } from '../../lib/orchestratorApi'; import type { UseOrchestrator } from './useOrchestrator'; +import { DiffModal } from './DiffModal'; +import { RunEventList } from './RunEventList'; /** - * Inline agentic control for one kanban card. + * Inline agentic console for one kanban card. * * Lets the operator launch a `pi` run against the card, watch its live * tool/thought stream, steer it mid-flight, and stop it — all while the rest of * the board stays interactive. Settled runs show their outcome, commit, and * summary, with the worktree-backed changes recorded back onto the card and the * documentation by the agent itself. + * + * The run is presented as a small "mission console": a live stats header, a + * prominent status banner, an action bar, and a rich grouped activity log + * (assistant chat bubbles + a tool/bevy timeline). */ interface AgentRunBarProps { @@ -18,12 +23,15 @@ interface AgentRunBarProps { orch: UseOrchestrator; } -const STATUS_META: Record = { - queued: { label: 'Queued', color: 'var(--muted)' }, - running: { label: 'Running', color: 'var(--accent)' }, - completed: { label: 'Completed', color: 'var(--green)' }, - failed: { label: 'Failed', color: 'var(--red)' }, - stopped: { label: 'Stopped', color: 'var(--fg-dim)' }, +const STATUS_META: Record< + string, + { label: string; color: string; glyph: string; sub: string } +> = { + queued: { label: 'Queued', color: 'var(--muted)', glyph: '○', sub: 'Waiting to start…' }, + running: { label: 'Running', color: 'var(--accent)', glyph: '●', sub: 'Agent is working' }, + completed: { label: 'Completed', color: 'var(--green)', glyph: '✓', sub: 'Run finished successfully' }, + failed: { label: 'Failed', color: 'var(--red)', glyph: '⚠', sub: 'Run ended with an error' }, + stopped: { label: 'Stopped', color: 'var(--fg-dim)', glyph: '■', sub: 'Run was stopped' }, }; export function AgentRunBar({ card, orch }: AgentRunBarProps) { @@ -33,22 +41,45 @@ export function AgentRunBar({ card, orch }: AgentRunBarProps) { const [promptDraft, setPromptDraft] = useState(''); const [steerDraft, setSteerDraft] = useState(''); const [showPrompt, setShowPrompt] = useState(false); + const [showDiff, setShowDiff] = useState(false); + const [mergeResult, setMergeResult] = useState<{ ok: boolean; text: string } | null>(null); + const [mergeBusy, setMergeBusy] = useState(false); const [busy, setBusy] = useState(false); const logRef = useRef(null); + const now = useNow(isActive); - // Stream the active run's events while it is running. + const bevyRunning = run ? orch.bevyIsRunning(run.id) : false; + + // Stream the run's events while it exists (agent run OR a later Bevy test); + // the card stays expanded so playtest output keeps flowing after settle. useEffect(() => { - if (!run || !isActive) return; + if (!run) return; orch.watch(run.id); return () => orch.unwatch(run.id); - }, [run, isActive, orch]); + }, [run, orch]); + + // Sync Bevy status from the server when the run changes (truth after a + // reconnect/refresh; live lifecycle is mirrored via `bevy` events). + useEffect(() => { + if (run) void orch.refreshBevyStatus(run.id); + }, [run, orch]); - // Auto-scroll the log to the latest event. const events = run ? orch.eventsForRun(run.id) : []; useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [events.length]); + // --- derived stats --- + const toolCount = useMemo(() => events.filter((e) => e.type === 'tool_start').length, [events]); + const durationLabel = useMemo(() => { + if (!run?.startedAt) return null; + const start = Date.parse(run.startedAt); + if (!Number.isFinite(start)) return null; + const end = run.finishedAt ? Date.parse(run.finishedAt) : now; + return fmtDuration((Number.isFinite(end) ? end : now) - start); + }, [run, now]); + + // --- handlers (unchanged behavior, clearer presentation) --- const start = async () => { setBusy(true); try { @@ -94,54 +125,86 @@ export function AgentRunBar({ card, orch }: AgentRunBarProps) { } }; + const onMerge = async () => { + if (!run) return; + setMergeBusy(true); + setMergeResult(null); + try { + const r = await orch.mergeRun(run.id); + if (r.alreadyMerged) setMergeResult({ ok: true, text: `Already in ${r.target}.` }); + else if (r.ok) setMergeResult({ ok: true, text: `Merged into ${r.target} — review & mark the card done.` }); + else setMergeResult({ ok: false, text: r.output }); + } catch (e) { + setMergeResult({ ok: false, text: e instanceof Error ? e.message : 'merge failed' }); + } finally { + setMergeBusy(false); + } + }; + + const settled = run && !isActive; + const hasWorktree = Boolean(run?.branch && run.worktreePath); + return ( -
-
- 🤖 Agent - {run && ( - +
+ {/* Animated gradient edge while the agent is working. */} + {isActive && } + +
+ + {run && } + + {/* Action bar */} +
+ {!run && !showPrompt && ( + + )} + {isActive && ( + + )} + {settled && ( + <> + + + )} {run?.branch && ( - + ⎇ {run.branch} )} - - {!run && !showPrompt && ( - - )} - {isActive && ( - - )} - {run && !isActive && ( - <> - - - - )} - + {run?.commitSha && ( + + ◉ {run.commitSha.slice(0, 7)} + + )}
{/* Start prompt (create or re-run) */} {showPrompt && ( -
+