import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import hljs from "highlight.js/lib/core" import bash from "highlight.js/lib/languages/bash" import css from "highlight.js/lib/languages/css" import go from "highlight.js/lib/languages/go" import json from "highlight.js/lib/languages/json" import markdown from "highlight.js/lib/languages/markdown" import python from "highlight.js/lib/languages/python" import rust from "highlight.js/lib/languages/rust" import sql from "highlight.js/lib/languages/sql" import typescript from "highlight.js/lib/languages/typescript" hljs.registerLanguage("bash", bash) hljs.registerLanguage("css", css) hljs.registerLanguage("go", go) hljs.registerLanguage("json", json) hljs.registerLanguage("markdown", markdown) hljs.registerLanguage("python", python) hljs.registerLanguage("rust", rust) hljs.registerLanguage("sql", sql) hljs.registerLanguage("typescript", typescript) type Panel = { id: string name: string baseUrl: string } type Agent = { id: string name: string baseUrl: string status: string lastSeenAt?: string lastSeq: number } type WorkerStatus = { agentId?: string running?: boolean idle?: boolean tokenCount?: number contextSize?: number processStartedAt?: string uptimeMs?: number error?: string client?: { connected?: boolean clientId?: string capabilities?: string[] workspace?: { root?: string } } } type Compaction = { agentId: string seq: number eventId: string metricId?: number timestamp: string method?: string before?: number after?: number savedTokens?: number summary?: string } type WorkerEvent = { id: string agentId: string seq: number type: string createdAt: string payload: unknown } type Overview = { agent: Agent status: WorkerStatus compactions: Compaction[] } type DiscordChannel = { channel_id: string guild_id: string | null guild_name: string | null channel_name: string | null channel_type: number | null is_dm: number configured: number topic: string | null last_seen_at: string } type DiscordMessage = { message_id: string channel_id: string author_username: string | null content: string created_at: string is_from_bot: number } type ChatLine = { key: string seq: number at: string kind: "agent" | "user" | "tool" | "note" | "context" | "error" | "thinking" label: string text: string detail?: string } const PANEL_COOKIE = "niri_control_panels" const NOTES_WIDTH_STORAGE_KEY = "niri_notes_width" const NOTES_WIDTH_MIN = 220 const NOTES_WIDTH_MAX = 520 function clampNotesWidth(value: number): number { return Math.min(NOTES_WIDTH_MAX, Math.max(NOTES_WIDTH_MIN, value)) } function readNotesWidth(): number { const raw = window.localStorage.getItem(NOTES_WIDTH_STORAGE_KEY) const value = raw ? Number(raw) : NaN return Number.isFinite(value) ? clampNotesWidth(value) : 286 } function cookieValue(name: string): string | null { const prefix = `${name}=` const item = document.cookie.split("; ").find((part) => part.startsWith(prefix)) return item ? decodeURIComponent(item.slice(prefix.length)) : null } function writeCookie(name: string, value: string): void { document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=31536000; Path=/; SameSite=Lax` } function createId(prefix: string): string { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return `${prefix}-${crypto.randomUUID()}` } return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` } function cleanBaseUrl(raw: string): string { const trimmed = raw.trim().replace(/\/+$/, "") if (!trimmed) return "" if (/^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)) return trimmed if (trimmed.startsWith("//")) return `${window.location.protocol}${trimmed}` return `http://${trimmed}` } function urlFor(panel: Panel, path: string): string { const base = panel.baseUrl || window.location.origin return new URL(path, base.endsWith("/") ? base : `${base}/`).toString() } function readPanels(): Panel[] { const raw = cookieValue(PANEL_COOKIE) if (raw) { try { const parsed = JSON.parse(raw) as Panel[] if (Array.isArray(parsed) && parsed.length > 0) { return parsed .filter((panel) => typeof panel.id === "string" && typeof panel.baseUrl === "string") .map((panel) => ({ id: panel.id, name: panel.name || panel.baseUrl || "this control", baseUrl: cleanBaseUrl(panel.baseUrl), })) } } catch {} } return [ { id: "same-origin", name: "this control", baseUrl: "", }, ] } function savePanels(panels: Panel[]): void { writeCookie(PANEL_COOKIE, JSON.stringify(panels)) } async function requestJson(url: string, init?: RequestInit): Promise { const res = await fetch(url, { ...init, headers: { ...(init?.body ? { "content-type": "application/json" } : {}), ...(init?.headers ?? {}), }, }) const text = await res.text() const data = text ? JSON.parse(text) : null if (!res.ok) { const message = data && typeof data === "object" && "error" in data ? String(data.error) : `${res.status} ${res.statusText}` throw new Error(message) } return data as T } async function consumeWorkerStream(url: string, signal: AbortSignal, onEvent: (event: WorkerEvent) => void): Promise { const res = await fetch(url, { signal }) if (!res.ok || !res.body) throw new Error(res.ok ? "stream body missing" : `${res.status} ${res.statusText}`) const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = "" while (true) { const { done, value } = await reader.read() if (done) return buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n") const blocks = buffer.split("\n\n") buffer = blocks.pop() ?? "" for (const block of blocks) { const payload = block .split("\n") .filter((line) => line.startsWith("data:")) .map((line) => line.slice(5).trimStart()) .join("\n") if (!payload) continue try { onEvent(JSON.parse(payload) as WorkerEvent) } catch {} } } } function eventPayloadObject(event: WorkerEvent): Record { return event.payload && typeof event.payload === "object" ? (event.payload as Record) : {} } function formatTime(value: string | undefined): string { if (!value) return "" const date = new Date(value) if (Number.isNaN(date.getTime())) return "" return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) } function formatDuration(ms: number | undefined): string { if (!ms || ms < 0) return "just started" const totalSeconds = Math.floor(ms / 1000) const hours = Math.floor(totalSeconds / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) if (hours > 0) return `${hours}h ${minutes}m` if (minutes > 0) return `${minutes}m` return `${Math.max(1, totalSeconds)}s` } function formatNumber(value: number | undefined): string { if (typeof value !== "number" || !Number.isFinite(value)) return "0" return new Intl.NumberFormat().format(value) } function discordChannelLabel(channel: DiscordChannel): string { if (channel.is_dm) return channel.channel_name || channel.guild_name || "direct message" return channel.channel_name ? `#${channel.channel_name}` : channel.channel_id } function jsonToolLabel(value: unknown): string | null { if (Array.isArray(value)) { if (value.length === 0) return "[]" const first = value[0] if (first && typeof first === "object" && "item_id" in first) return `discord_inbox ${value.length} item${value.length === 1 ? "" : "s"}` if (first && typeof first === "object" && "message_id" in first) return `discord_messages ${value.length}` return `json array ${value.length}` } if (!value || typeof value !== "object") return null const record = value as Record if ("sent_message_id" in record) return `discord_send ${record.ok === true ? "ok" : "result"}` if ("item_id" in record && "status" in record) return `discord_mark ${String(record.status)}` if ("scanned_channels" in record) return `discord_scan ${formatNumber(Number(record.fetched_messages))} messages` if ("ok" in record) return record.ok === true ? "ok" : "tool result" return null } function toolLabel(text: string): string { const lines = text.split("\n").map((line) => line.trim()).filter(Boolean) const first = lines[0] if (!first) return "tool" if (first.startsWith("discord_send ok")) { const sent = lines.find((line) => line.startsWith("sent: "))?.slice(6).trim() if (sent) { const label = `discord_send ok: ${sent}` return label.length <= 120 ? label : `${label.slice(0, 117)}...` } } if (first.trim().startsWith("{") || first.trim().startsWith("[")) { try { return jsonToolLabel(JSON.parse(text)) ?? "tool result" } catch { return "tool result" } } if (first.length <= 82) return first return `${first.slice(0, 79)}...` } function discordContextLine(text: string): Pick { const stats = text.match(/new_messages=(\d+).*?channels=(\d+).*?pending_inbox=(\d+)/s) const recent = text.match(/recent messages:\s*([\s\S]*?)(?:\n\npending preview:|$)/) const newMessages = stats?.[1] ?? "some" const channels = stats?.[2] ?? "?" const pending = stats?.[3] ?? "?" return { kind: "context", label: "discord context", text: `${newMessages} recent messages across ${channels} channel${channels === "1" ? "" : "s"} - ${pending} pending`, detail: recent?.[1]?.trim() || text.trim(), } } function isDiscordContextEnvelope(envelopeName: string): boolean { return ( envelopeName.startsWith("incoming") || envelopeName.startsWith("discord batch") || envelopeName.startsWith("discord context") ) } function userLineFromContent(content: string, fallbackLabel = "you"): Pick { const trimmed = content.trim() const envelope = trimmed.match(/^\[([^\]]+)\]\s*([\s\S]*)$/) const envelopeName = envelope?.[1]?.toLowerCase() ?? "" const body = envelope?.[2]?.trim() ?? trimmed if (isDiscordContextEnvelope(envelopeName)) return discordContextLine(body) if (envelopeName.startsWith("system")) { return { kind: "context", label: "system", text: body.split("\n").find(Boolean)?.trim() || "system event", detail: body, } } if (envelopeName.startsWith("wake")) { return { kind: "note", label: "wake", text: envelope?.[1] ?? "wake", detail: body, } } if (envelopeName.startsWith("harness restarted")) { return { kind: "note", label: "harness", text: "restarted", detail: body, } } return { kind: "user", label: fallbackLabel, text: trimmed, } } function linesFromEvents(events: WorkerEvent[]): ChatLine[] { const lines: ChatLine[] = [] const sorted = [...events].sort((a, b) => a.seq - b.seq) const streamUserTexts = new Set() for (const event of sorted) { if (event.type !== "stream.event") continue const payload = eventPayloadObject(event) if (payload.type === "user" && typeof payload.text === "string" && payload.text.trim()) { streamUserTexts.add(payload.text.trim()) } } // Coalesce adjacent thinking stream events into single lines. let thinkingBuffer = "" let thinkingStartEvent: WorkerEvent | null = null const flushThinking = () => { if (!thinkingStartEvent || !thinkingBuffer.trim()) { thinkingBuffer = "" thinkingStartEvent = null return } lines.push({ key: thinkingStartEvent.id, seq: thinkingStartEvent.seq, at: thinkingStartEvent.createdAt, kind: "thinking", label: "thinking", text: thinkingBuffer, }) thinkingBuffer = "" thinkingStartEvent = null } for (const event of sorted) { const payload = eventPayloadObject(event) // Handle thinking stream events — coalesce adjacent chunks. if (event.type === "stream.event" && payload.type === "thinking" && typeof payload.text === "string") { if (!thinkingStartEvent) thinkingStartEvent = event thinkingBuffer += payload.text continue } // Non-thinking event flushes any pending thinking buffer. flushThinking() if (event.type === "stream.event" && payload.type === "user" && typeof payload.text === "string") { const text = payload.text.trim() if (!text) continue const clean = userLineFromContent(text, String(payload.source ?? "you")) lines.push({ key: event.id, seq: event.seq, at: event.createdAt, kind: clean.kind, label: clean.label === "chat" ? "you" : clean.label, text: clean.text, detail: clean.detail, }) continue } if (event.type === "conversation.started") { lines.push({ key: event.id, seq: event.seq, at: event.createdAt, kind: "note", label: "session", text: `started from ${String(payload.source ?? "unknown")}`, }) continue } if (event.type === "conversation.ended") { lines.push({ key: event.id, seq: event.seq, at: event.createdAt, kind: "note", label: "session", text: `ended with ${formatNumber(typeof payload.tokens === "number" ? payload.tokens : undefined)} tokens`, }) continue } if (event.type !== "conversation.message") continue const role = String(payload.role ?? "") const content = String(payload.content ?? "") if (!content.trim()) continue if (role === "assistant") { lines.push({ key: event.id, seq: event.seq, at: String(payload.createdAt ?? event.createdAt), kind: "agent", label: "agent", text: content, }) continue } if (role === "tool") { lines.push({ key: event.id, seq: event.seq, at: String(payload.createdAt ?? event.createdAt), kind: "tool", label: toolLabel(content), text: content, }) continue } if (role === "user") { if (streamUserTexts.has(content.trim())) continue if (content.trim().startsWith("[wake]")) continue const clean = userLineFromContent(content) lines.push({ key: event.id, seq: event.seq, at: String(payload.createdAt ?? event.createdAt), kind: clean.kind, label: clean.label, text: clean.text, detail: clean.detail, }) } } // Flush any trailing thinking buffer. flushThinking() return lines } function overviewStatusText(status: WorkerStatus | null): string { if (!status) return "not checked" if (status.error) return "unreachable" if (status.running) return status.idle ? "waiting" : "awake" return "resting" } function clientStatusText(status: WorkerStatus | null): string { const client = status?.client if (!client) return "client status unavailable" if (!client.connected) return "no tool client" const label = client.clientId || "tool client" const capabilityCount = client.capabilities?.length ?? 0 return `${label} attached${capabilityCount ? ` · ${capabilityCount} tools` : ""}` } function codeLanguage(text: string): string | undefined { const trimmed = text.trim() if (!trimmed) return undefined if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json" if (/\.(tsx?|jsx?|mjs|cjs)\b/.test(trimmed)) return "typescript" if (/\.(css|html|json|md|py|rs|go|sh|bash|zsh)\b/.test(trimmed)) { const ext = trimmed.match(/\.(css|html|json|md|py|rs|go|sh|bash|zsh)\b/)?.[1] if (ext === "py") return "python" if (ext === "rs") return "rust" if (ext === "md") return "markdown" if (ext === "sh" || ext === "zsh") return "bash" return ext } if (/\b(import|export|const|let|function|type|interface)\b/.test(trimmed)) return "typescript" if (/\b(select|insert|update|create table|from|where)\b/i.test(trimmed)) return "sql" return undefined } function HighlightedCode({ text, language: requestedLanguage }: { text: string; language?: string }) { const language = requestedLanguage ?? codeLanguage(text) let html: string try { html = language && hljs.getLanguage(language) ? hljs.highlight(text, { language }).value : hljs.highlightAuto(text).value } catch { html = text.replace(/[&<>"']/g, (char) => { if (char === "&") return "&" if (char === "<") return "<" if (char === ">") return ">" if (char === "\"") return """ return "'" }) } return (
      
    
) } function MarkdownBody({ text }: { text: string }) { return (
{children} }, code({ className, children }) { const match = /language-([a-z0-9_-]+)/i.exec(className ?? "") const code = String(children).replace(/\n$/, "") if (match) return return {children} }, }} > {text}
) } function prefixForLine(line: ChatLine, agentName: string): string { if (line.kind === "agent") return agentName if (line.kind === "user") return "you" if (line.kind === "tool") return "tool" if (line.kind === "thinking") return "thinking" if (line.kind === "context" && line.label.includes("discord")) return "discord" return line.label } function TerminalMessage({ line, agentName, expanded, onToggle, }: { line: ChatLine agentName: string expanded: boolean onToggle: (key: string) => void }) { const prefix = prefixForLine(line, agentName) if (line.kind === "tool") { return (
tool:
{expanded ? (
) : null}
) } if (line.kind === "thinking") { return (
thinking:
{expanded ? (
) : null}
) } if (line.kind === "context" || line.detail) { return (
{prefix}:

{line.text}

{line.detail ? ( <> {expanded ? (
) : null} ) : null}
) } return (
{prefix}:
) } export function App() { const [panels, setPanels] = useState(() => readPanels()) const [panelDraft, setPanelDraft] = useState("") const [panelNameDraft, setPanelNameDraft] = useState("") const [agentsByPanel, setAgentsByPanel] = useState>({}) const [selectedKey, setSelectedKey] = useState("") const [events, setEvents] = useState([]) const [overview, setOverview] = useState(null) const [input, setInput] = useState("") const [busy, setBusy] = useState(false) const [error, setError] = useState("") const [openCompaction, setOpenCompaction] = useState(null) const [expandedTools, setExpandedTools] = useState>(() => new Set()) const [discordChannels, setDiscordChannels] = useState([]) const [selectedDiscordChannel, setSelectedDiscordChannel] = useState(null) const [discordMessages, setDiscordMessages] = useState([]) const [discordLoading, setDiscordLoading] = useState(false) const [notesWidth, setNotesWidth] = useState(() => readNotesWidth()) const resizeStart = useRef<{ pointerId: number; startX: number; startWidth: number } | null>(null) const streamRef = useRef(null) const startNotesResize = useCallback((event: React.PointerEvent) => { event.preventDefault() event.currentTarget.setPointerCapture(event.pointerId) resizeStart.current = { pointerId: event.pointerId, startX: event.clientX, startWidth: notesWidth } document.body.classList.add("is-resizing-notes") }, [notesWidth]) const moveNotesResize = useCallback((event: React.PointerEvent) => { const start = resizeStart.current if (!start || start.pointerId !== event.pointerId) return setNotesWidth(clampNotesWidth(start.startWidth + start.startX - event.clientX)) }, []) const endNotesResize = useCallback((event: React.PointerEvent) => { const start = resizeStart.current if (!start || start.pointerId !== event.pointerId) return resizeStart.current = null document.body.classList.remove("is-resizing-notes") event.currentTarget.releasePointerCapture(event.pointerId) setNotesWidth((width) => { window.localStorage.setItem(NOTES_WIDTH_STORAGE_KEY, String(width)) return width }) }, []) const handleNotesResizeKeyDown = useCallback((event: React.KeyboardEvent) => { const step = event.shiftKey ? 40 : 16 if (event.key === "ArrowLeft") setNotesWidth((width) => clampNotesWidth(width + step)) else if (event.key === "ArrowRight") setNotesWidth((width) => clampNotesWidth(width - step)) else if (event.key === "Home") setNotesWidth(NOTES_WIDTH_MIN) else if (event.key === "End") setNotesWidth(NOTES_WIDTH_MAX) else return event.preventDefault() window.localStorage.setItem(NOTES_WIDTH_STORAGE_KEY, String(notesWidth)) }, [notesWidth]) useEffect(() => { window.localStorage.setItem(NOTES_WIDTH_STORAGE_KEY, String(notesWidth)) }, [notesWidth]) const selected = useMemo(() => { if (!selectedKey) return null const [panelId, agentId] = selectedKey.split("::") const panel = panels.find((item) => item.id === panelId) const agent = panel ? agentsByPanel[panel.id]?.find((item) => item.id === agentId) : undefined return panel && agent ? { panel, agent } : null }, [agentsByPanel, panels, selectedKey]) const chatLines = useMemo(() => linesFromEvents(events), [events]) const mergeEvents = useCallback((incoming: WorkerEvent[]) => { setEvents((prev) => { const byId = new Map(prev.map((event) => [event.id, event])) for (const event of incoming) byId.set(event.id, event) return [...byId.values()].sort((a, b) => a.seq - b.seq) }) }, []) const persistPanels = useCallback((next: Panel[]) => { setPanels(next) savePanels(next) }, []) const loadAgents = useCallback(async () => { const next: Record = {} const failures: string[] = [] await Promise.all( panels.map(async (panel) => { try { const data = await requestJson<{ agents: Agent[] }>(urlFor(panel, "/agents")) next[panel.id] = data.agents ?? [] } catch (err) { failures.push(`${panel.name}: ${err instanceof Error ? err.message : String(err)}`) next[panel.id] = [] } }), ) setAgentsByPanel(next) setError(failures[0] ?? "") if (!selectedKey) { const firstPanel = panels.find((panel) => next[panel.id]?.length) const firstAgent = firstPanel ? next[firstPanel.id]?.[0] : undefined if (firstPanel && firstAgent) setSelectedKey(`${firstPanel.id}::${firstAgent.id}`) } }, [panels, selectedKey]) const loadSelected = useCallback(async () => { if (!selected) return setError("") try { const [overviewData, eventData, discordData] = await Promise.all([ requestJson(urlFor(selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/overview`)), requestJson<{ events: WorkerEvent[] }>( urlFor(selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/events?tail=1&limit=1000&view=chat`), ), requestJson<{ channels: DiscordChannel[] }>( urlFor(selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/discord/channels`), ).catch(() => ({ channels: [] })), ]) setOverview(overviewData) setEvents(eventData.events ?? []) setDiscordChannels(discordData.channels ?? []) setSelectedDiscordChannel((current) => current && (discordData.channels ?? []).some((channel) => channel.channel_id === current) ? current : discordData.channels?.[0]?.channel_id ?? null, ) } catch (err) { setError(err instanceof Error ? err.message : String(err)) setOverview(null) setEvents([]) setDiscordChannels([]) setSelectedDiscordChannel(null) setDiscordMessages([]) } }, [selected]) useEffect(() => { if (!selected || !selectedDiscordChannel) { setDiscordMessages([]) return } setDiscordLoading(true) void requestJson<{ messages: DiscordMessage[] }>( urlFor( selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/discord/channels/${encodeURIComponent(selectedDiscordChannel)}/messages?limit=100`, ), ) .then((data) => setDiscordMessages((data.messages ?? []).slice().reverse())) .catch(() => setDiscordMessages([])) .finally(() => setDiscordLoading(false)) }, [selected, selectedDiscordChannel]) useEffect(() => { void loadAgents() }, [loadAgents]) useEffect(() => { void loadSelected() }, [loadSelected]) useEffect(() => { if (!selected) return streamRef.current?.abort() const controller = new AbortController() streamRef.current = controller const streamUrl = urlFor( selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/stream?after_seq=${selected.agent.lastSeq}`, ) void consumeWorkerStream(streamUrl, controller.signal, (event) => { mergeEvents([event]) if (event.type === "metric.recorded") void loadSelected() }).catch((error) => { if (!controller.signal.aborted) setError(`stream lost for ${selected.agent.name}: ${error instanceof Error ? error.message : String(error)}`) }) return () => { controller.abort() } }, [loadSelected, mergeEvents, selected]) useEffect(() => { if (!selected) return const timer = setInterval(() => { requestJson(urlFor(selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/overview`)) .then(setOverview) .catch((err) => setError(err instanceof Error ? err.message : String(err))) }, 10_000) return () => clearInterval(timer) }, [selected]) const addPanel = useCallback( (event: FormEvent) => { event.preventDefault() const baseUrl = cleanBaseUrl(panelDraft) if (!baseUrl) return const next = [ ...panels, { id: createId("panel"), name: panelNameDraft.trim() || baseUrl, baseUrl, }, ] persistPanels(next) setPanelDraft("") setPanelNameDraft("") }, [panelDraft, panelNameDraft, panels, persistPanels], ) const removePanel = useCallback( (panelId: string) => { const next = panels.filter((panel) => panel.id !== panelId) persistPanels(next.length ? next : readPanels()) if (selectedKey.startsWith(`${panelId}::`)) { setSelectedKey("") setEvents([]) setOverview(null) setDiscordChannels([]) setSelectedDiscordChannel(null) setDiscordMessages([]) } }, [panels, persistPanels, selectedKey], ) const sendMessage = useCallback( async (event: FormEvent) => { event.preventDefault() if (!selected) return const content = input.trim() if (!content || busy) return setBusy(true) setError("") setInput("") try { await requestJson(urlFor(selected.panel, `/agents/${encodeURIComponent(selected.agent.id)}/events`), { method: "POST", body: JSON.stringify({ content }), }) await loadSelected() } catch (err) { setError(err instanceof Error ? err.message : String(err)) setInput(content) } finally { setBusy(false) } }, [busy, input, loadSelected, selected], ) const toggleTool = useCallback((key: string) => { setExpandedTools((prev) => { const next = new Set(prev) if (next.has(key)) next.delete(key) else next.add(key) return next }) }, []) const compactions = overview?.compactions ?? [] const status = overview?.status ?? null return (

{selected ? selected.agent.name : "choose an agent"}

{overviewStatusText(status)} {status?.processStartedAt ? ` since ${formatTime(status.processStartedAt)}` : ""}

{clientStatusText(status)}

{formatNumber(status?.contextSize)} ctx {formatNumber(status?.tokenCount)} total {formatDuration(status?.uptimeMs)}
{error ?

{error}

: null}
{chatLines.length === 0 ? (

No mirrored history yet. Send a message or open the stream to start collecting it.

) : ( chatLines.map((line) => { const expanded = expandedTools.has(line.key) return ( ) }) )}
setInput(event.target.value)} disabled={!selected || busy} placeholder={selected ? `message ${selected.agent.name}` : "choose an agent first"} aria-label="message" />
) }