From a3c72bb87872a2cdfee62e48c0442f34d9efb661 Mon Sep 17 00:00:00 2001 From: francy51 Date: Tue, 16 Jun 2026 15:44:29 -0400 Subject: [PATCH] feat(kanban): persist board, reference pages, and run agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the localStorage kanban with the backend-backed board, add typed clients and a React hook with optimistic updates. Cards can reference static doc pages and user-created custom pages (new "custom" reference type with purple chips). Add the agentic orchestrator UI: a per-card panel to launch `pi` runs, watch a live tool/thought stream over SSE, steer mid-run, and stop — while the board stays fully interactive. The board page wires the orchestrator and custom-pages stores into every card. --- .../src/components/kanban/AgentRunBar.tsx | 390 ++++++ .../docs/src/components/kanban/KanbanCard.tsx | 423 ++++++ .../src/components/kanban/ReferenceLinks.tsx | 140 ++ .../src/components/kanban/useKanbanBoard.ts | 236 ++++ .../src/components/kanban/useOrchestrator.ts | 226 ++++ apps/docs/src/lib/kanbanApi.ts | 125 ++ apps/docs/src/lib/orchestratorApi.ts | 102 ++ apps/docs/src/pages/docs/KanbanBoardPage.tsx | 1172 ++++------------- 8 files changed, 1906 insertions(+), 908 deletions(-) create mode 100644 apps/docs/src/components/kanban/AgentRunBar.tsx create mode 100644 apps/docs/src/components/kanban/KanbanCard.tsx create mode 100644 apps/docs/src/components/kanban/ReferenceLinks.tsx create mode 100644 apps/docs/src/components/kanban/useKanbanBoard.ts create mode 100644 apps/docs/src/components/kanban/useOrchestrator.ts create mode 100644 apps/docs/src/lib/kanbanApi.ts create mode 100644 apps/docs/src/lib/orchestratorApi.ts diff --git a/apps/docs/src/components/kanban/AgentRunBar.tsx b/apps/docs/src/components/kanban/AgentRunBar.tsx new file mode 100644 index 0000000..a7f50a0 --- /dev/null +++ b/apps/docs/src/components/kanban/AgentRunBar.tsx @@ -0,0 +1,390 @@ +import { useEffect, useRef, useState } from 'react'; +import type { Card } from '../../lib/kanbanApi'; +import type { RunEvent } from '../../lib/orchestratorApi'; +import type { UseOrchestrator } from './useOrchestrator'; + +/** + * Inline agentic control 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. + */ + +interface AgentRunBarProps { + card: Card; + 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)' }, +}; + +export function AgentRunBar({ card, orch }: AgentRunBarProps) { + const run = orch.runForCard(card.id); + const isActive = run?.status === 'running'; + + const [promptDraft, setPromptDraft] = useState(''); + const [steerDraft, setSteerDraft] = useState(''); + const [showPrompt, setShowPrompt] = useState(false); + const [busy, setBusy] = useState(false); + const logRef = useRef(null); + + // Stream the active run's events while it is running. + useEffect(() => { + if (!run || !isActive) return; + orch.watch(run.id); + return () => orch.unwatch(run.id); + }, [run, isActive, 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]); + + const start = async () => { + setBusy(true); + try { + await orch.start({ cardId: card.id, prompt: promptDraft.trim() || undefined }); + setPromptDraft(''); + setShowPrompt(false); + } catch (e) { + alert(e instanceof Error ? e.message : 'Failed to start run'); + } finally { + setBusy(false); + } + }; + + const steer = async () => { + if (!run || !steerDraft.trim()) return; + setBusy(true); + try { + await orch.message(run.id, steerDraft.trim(), 'steer'); + setSteerDraft(''); + } catch (e) { + alert(e instanceof Error ? e.message : 'Failed to steer'); + } finally { + setBusy(false); + } + }; + + const stop = async () => { + if (!run) return; + setBusy(true); + try { + await orch.stop(run.id); + } finally { + setBusy(false); + } + }; + + const dismiss = async () => { + if (!run) return; + try { + await orch.remove(run.id); + } catch (e) { + alert(e instanceof Error ? e.message : 'Failed to remove run'); + } + }; + + return ( +
+
+ 🤖 Agent + {run && ( + + )} + {run?.branch && ( + + ⎇ {run.branch} + + )} + + {!run && !showPrompt && ( + + )} + {isActive && ( + + )} + {run && !isActive && ( + <> + + + + )} + +
+ + {/* Start prompt (create or re-run) */} + {showPrompt && ( +
+