import { atom, read, update } from 'claude-code' import type { EngineInterface, Register, RenderChildren } from 'claude-code' import type { AgentCard, Architect, Bucket, Check, Gate, Layout, LogLine, Loop, Main, Roster, Turn, Usage, View } from '../types' import { DEFAULT_ARCHITECT, addCard, resumeCard, DEFAULT_GATE, DEFAULT_MAIN, DEFAULT_ROSTER, DEFAULT_TURN, DEFAULT_USAGE, DEFAULT_VIEW, EDIT_TOOLS, SCHEMA_VERSION, afterCall, applyStep, bucketOf, cellWidth, piecesThatFit, cardTitle, afterCompactRead, claimCompactRead, contextFill, faultKey, forgetCompactRead, titleLines, consultTimeline, describeInput, endConsult, fitLegend, fmtClock, fmtDuration, fmtTimer, fmtUsd, plural, gateSummary, gauge, isAdvising, isLoopActive, kTokens, lanes, limitLabel, limitOf, fmtResetIn, fmtSpeed, listOf, logRows, marksIn, momentOf, normalize, normalizeCard, normalizeGate, normalizeLog, noteTool, PALETTES, SVG_COLORS, parseConfig, prettyModel, promptLine, handbackOf, adviceLine, receiptOf, recordCheck, settleCheck, shorten, startConsult, stepSpeed, stepLoop, } from './core' import type { Config, Panel } from './core' const PANE = 'flightdeck' const TITLE = 'Flightdeck' // ---------------------------------------------------------------- state const meta = atom({ plugin: 'flightdeck', key: 'meta' } as const, { schemaVersion: SCHEMA_VERSION }) const main = atom({ plugin: 'flightdeck', key: 'main' } as const, DEFAULT_MAIN) const usage = atom({ plugin: 'flightdeck', key: 'usage' } as const, DEFAULT_USAGE) const architect = atom({ plugin: 'flightdeck', key: 'architect' } as const, DEFAULT_ARCHITECT) const gate = atom({ plugin: 'flightdeck', key: 'gate' } as const, DEFAULT_GATE) const agents = atom({ plugin: 'flightdeck', key: 'agents' } as const, []) const loops = atom({ plugin: 'flightdeck', key: 'loops' } as const, []) const log = atom({ plugin: 'flightdeck', key: 'log' } as const, []) const turn = atom({ plugin: 'flightdeck', key: 'turn' } as const, DEFAULT_TURN) const receipt = atom({ plugin: 'flightdeck', key: 'receipt' } as const, null) const view = atom({ plugin: 'flightdeck', key: 'view' } as const, DEFAULT_VIEW) const roster = atom({ plugin: 'flightdeck', key: 'roster' } as const, DEFAULT_ROSTER) type ServerBlock = { type: string; id?: string; name?: string; tool_use_id?: string } // Every read goes through these, so a value saved under an older shape still reads. async function getMain($: EngineInterface): Promise
{ return normalize(DEFAULT_MAIN, await read($, main)) } async function getUsage($: EngineInterface): Promise { return normalize(DEFAULT_USAGE, await read($, usage)) } async function getArchitect($: EngineInterface): Promise { const a = normalize(DEFAULT_ARCHITECT, await read($, architect)) return { ...a, consults: listOf(a.consults), ids: listOf(a.ids), seen: listOf(a.seen) } } async function getGate($: EngineInterface): Promise { return normalizeGate(await read($, gate)) } async function getCards($: EngineInterface): Promise { return listOf(await read($, agents)).map(normalizeCard) } async function getLoops($: EngineInterface): Promise { return listOf(await read($, loops)) } async function getLog($: EngineInterface): Promise { return normalizeLog(await read($, log)) } async function getTurn($: EngineInterface): Promise { return normalize(DEFAULT_TURN, await read($, turn)) } async function getView($: EngineInterface): Promise { return normalize(DEFAULT_VIEW, await read($, view)) } async function getRoster($: EngineInterface): Promise { const r = normalize(DEFAULT_ROSTER, await read($, roster)) return { architectTypes: listOf(r.architectTypes) } } /** A stored shape older than this build's: drop what cannot be read, keep the rest. */ async function migrate($: EngineInterface) { const m = await read($, meta) if ((m?.schemaVersion ?? 0) >= SCHEMA_VERSION) return await update($, log, list => normalizeLog(list)) await update($, agents, list => listOf(list).map(normalizeCard)) await update($, gate, g => normalizeGate(g)) await update($, meta, () => ({ schemaVersion: SCHEMA_VERSION })) } async function say($: EngineInterface, who: string, text: string, kind: LogLine['kind'] = 'info', agentId: string | null = null) { const line: LogLine = { at: await $.clock.now(), who, text, kind, agentId } await update($, log, list => [...normalizeLog(list), line].slice(-60)) } async function refreshStatus($: EngineInterface, cfg: Config) { if (!cfg.statusLine) return $.ui.status(undefined) const [u, a, g, cards] = await Promise.all([getUsage($), getArchitect($), getGate($), getCards($)]) const running = cards.filter(c => c.status === 'running').length const s = gateSummary(g) const ctxFill = contextFill(u) const parts = [ ctxFill.pct !== null ? `ctx ${Math.round(ctxFill.pct)}%` : null, cards.length > 0 ? `agents ${running}/${cards.length}` : null, a.consults.length > 0 || a.ids.length > 0 ? `${cfg.architectLabel.toLowerCase()} ${isAdvising(a) ? 'advising' : a.consults.length}` : null, s.deny > 0 ? `denied ${s.deny}` : null, ] // Only fields with something to say; with none, no status entry at all. const shown = parts.filter(Boolean) $.ui.status(shown.length > 0 ? shown.join(' · ') : undefined) } async function whoIs($: EngineInterface, agentId: string | undefined) { if (!agentId) return 'main' const card = (await getCards($)).find(c => c.id === agentId) return card ? shorten(cardTitle(card), 14) : 'agent' } async function consultStarted($: EngineInterface, cfg: Config, id: string, via: string) { const t = await getTurn($) const moment = momentOf(t) const at = await $.clock.now() await update($, architect, a => startConsult(normalize(DEFAULT_ARCHITECT, a), { id, at, moment, via })) if (moment === 'before done') await update($, turn, x => ({ ...normalize(DEFAULT_TURN, x), isReviewing: true })) await say($, cfg.architectLabel.toLowerCase(), cfg.moments ? `${moment} · ${via}` : `consulted · ${via}`, 'consult') await refreshStatus($, cfg) } async function consultEnded($: EngineInterface, cfg: Config, advice: string | null, id?: string) { const at = await $.clock.now() const first = advice?.split('\n').find(l => l.trim()) ?? null const text = first ? shorten(first.replace(/^[#>*\s-]+/, ''), 160) : null await update($, architect, a => endConsult(normalize(DEFAULT_ARCHITECT, a), at, text, id)) await update($, turn, t => ({ ...normalize(DEFAULT_TURN, t), isReviewing: false })) await say($, cfg.architectLabel.toLowerCase(), text ? `advice: ${shorten(text, 60)}` : 'advice returned', 'consult') await refreshStatus($, cfg) } async function noteAdvice($: EngineInterface, cfg: Config, advice: string) { await update($, architect, x => ({ ...normalize(DEFAULT_ARCHITECT, x), lastAdvice: advice })) await say($, cfg.architectLabel.toLowerCase(), `advice: ${shorten(advice, 60)}`, 'consult') } async function isArchitectType($: EngineInterface, cfg: Config, type: string) { return cfg.architect.test(type) || (await getRoster($)).architectTypes.includes(type) } async function openPane($: EngineInterface, cfg: Config) { // columns apply when docked beside the transcript, rows when seated inline above the prompt. return $.ui.open({ id: PANE, title: TITLE, columns: cfg.paneColumns, rows: 8 }) } async function resetAll($: EngineInterface) { await update($, main, m => ({ ...DEFAULT_MAIN, model: normalize(DEFAULT_MAIN, m).model, mode: normalize(DEFAULT_MAIN, m).mode })) await update($, architect, () => DEFAULT_ARCHITECT) await update($, gate, () => DEFAULT_GATE) await update($, agents, () => []) await update($, loops, () => []) await update($, log, () => []) await update($, turn, () => DEFAULT_TURN) await update($, receipt, () => null) await update($, view, () => DEFAULT_VIEW) // The context gauge waits for the next measurement rather than showing the pre-clear fill. await update($, usage, x => ({ ...normalize(DEFAULT_USAGE, x), pct: null, tokens: null })) } /** * Stores the compaction window for the model window `window`. A `summary` breakdown estimates locally * and sends no request; until the host gives one, the gauge stays on the model's window (afterCompactRead). */ async function readCompactWindow($: EngineInterface, window: number) { const r = await $.session.usage({ breakdown: 'summary' }).catch(() => null) await update($, usage, x => afterCompactRead(normalize(DEFAULT_USAGE, x), window, r?.context.breakdown)) } /** The session's cost read fresh, not from the last measurement: the receipt subtracts two of these. */ async function costNow($: EngineInterface): Promise { const u = await $.session.usage().catch(() => null) return u?.cost?.usd ?? null } async function noteMode($: EngineInterface, mode: string | undefined) { if (mode) await update($, main, m => (normalize(DEFAULT_MAIN, m).mode === mode ? normalize(DEFAULT_MAIN, m) : { ...normalize(DEFAULT_MAIN, m), mode })) } // ---------------------------------------------------------------- hooks export const register: Register = (on, options) => { const cfg = parseConfig(options) const C = PALETTES[cfg.palette] // tool.check carries no loop id; the tool.call around it does, keyed by the call's id. const callLoop = new Map() on('session.start', async ($, e, next) => { await $.command.register({ name: 'flightdeck', description: 'Flightdeck, the live agent dashboard: open, close, reset, or set the layout', argumentHint: '[open|close|reset|layout auto|compact|wide|mini]', }) await migrate($) // A host without usage (headless, an SDK host, a session not yet bound) just starts without it. const u = await $.session.usage().catch(() => null) // A resumed session has a fill to draw already: its compaction window is read now, not at its first // measurement. Any other start forgets the last reading (the setting may have changed) for that one to read. const resumedWindow = u && u.context.tokens !== undefined && u.context.window > 0 ? u.context.window : 0 if (u) { await update($, usage, x => ({ ...normalize(DEFAULT_USAGE, x), pct: u.context.percent ?? null, tokens: u.context.tokens ?? null, window: u.context.window, compactFor: resumedWindow, compactMisses: 0, costUsd: u.cost?.usd ?? null, limits: u.rateLimits.map(limitOf), })) } if (cfg.openOnStart) void openPane($, cfg).catch(() => undefined) await refreshStatus($, cfg) const done = await next(e) if (resumedWindow > 0) { await readCompactWindow($, resumedWindow) await refreshStatus($, cfg) } return done }) on('session.end', async ($, e, next) => { if (e.reason === 'clear') { await resetAll($) await refreshStatus($, cfg) } return next(e) }) on('command.run', { command: 'flightdeck' }, async ($, e) => { const [verb = 'open', arg = ''] = e.args.trim().split(/\s+/) if (verb === 'close') { await $.ui.close({ id: PANE }) return { text: 'Flightdeck closed.' } } if (verb === 'reset') { await resetAll($) await refreshStatus($, cfg) return { text: 'Flightdeck reset.' } } if (verb === 'layout') { const layout: Layout | null = arg === 'compact' || arg === 'wide' || arg === 'auto' || arg === 'mini' ? arg : null if (!layout) return { text: 'Usage: /flightdeck layout auto|compact|wide|mini' } await update($, view, v => ({ ...normalize(DEFAULT_VIEW, v), layout })) const opened = await openPane($, cfg) return { text: opened.isPlaced ? `Flightdeck layout: ${layout}.` : `Layout set to ${layout}; the pane is not shown yet: ${opened.reason}` } } const opened = await openPane($, cfg) if (!opened.isPlaced) return { text: `Flightdeck is not shown yet: ${opened.reason}` } return { text: 'Flightdeck opened. Focus it with ctrl+x tab; 1-6 expand cards, f/s/o open the gate rows.' } }) on('classic.UserPromptSubmit', async ($, e, next) => { await noteMode($, e.permission_mode) return next(e) }) on('agent.offer', async ($, e, next) => { const offered = await next(e) if (cfg.architect.test(e.agent) || (cfg.matchDescriptions && cfg.architect.test(e.description))) { await update($, roster, r => { const x = normalize(DEFAULT_ROSTER, r) return x.architectTypes.includes(e.agent) ? x : { architectTypes: [...listOf(x.architectTypes), e.agent].slice(-20) } }) } return offered }) on('turn.start', async ($, e, next) => { // A subagent's turn (its spawn, or a resume by SendMessage) is its card's, not the main turn's. if (e.agentId !== undefined) return next(e) const [now, cost] = await Promise.all([$.clock.now(), costNow($)]) await update($, turn, () => ({ ...DEFAULT_TURN, startedAt: now, costAtStart: cost })) await update($, main, m => ({ ...normalize(DEFAULT_MAIN, m), isRunning: true })) // A background architect's report reaches the main loop as the text opening this turn. The // SubagentHandback tool call (in tool.call) normally carries it first; this is the fallback. const back = e.text ? handbackOf(e.text) : null const a = back ? await getArchitect($) : null if (back && a && a.ids.includes(back.from)) { const advice = adviceLine(back.body) if (advice && advice !== a.lastAdvice) await noteAdvice($, cfg, advice) } else if (e.text) { const p = promptLine(e.text) await say($, p.who, p.text) } return next(e) }) on('turn.step', async function* ($, e, next) { // The main loop's model is known when its request starts; a long first request shouldn't read "—". if (!e.agentId) { let isNewModel = false await update($, main, m => { const x = normalize(DEFAULT_MAIN, m) isNewModel = x.model !== '' && x.model !== e.model return { ...x, model: e.model, effort: String(e.effort ?? x.effort), steps: x.steps + 1 } }) if (isNewModel) await update($, usage, x => forgetCompactRead(normalize(DEFAULT_USAGE, x))) // Time the response as it passes: its first piece that is neither the engine's envelope nor the // stop. One clock read in the stream and nothing stored until it has ended. const sentAt = await $.clock.now() let firstAt: number | null = null const s = next(e) for await (const c of s) { if (firstAt === null && c.kind !== 'engine' && c.kind !== 'stop') firstAt = await $.clock.now() yield c } const result = await s.result const speed = result.usage ? stepSpeed(sentAt, firstAt) : null if (speed) await update($, main, m => ({ ...normalize(DEFAULT_MAIN, m), speed })) return result } const id = e.agentId // A step from an agent whose card finished means it was resumed (SendMessage): it runs again. const before = (await getCards($)).find(c => c.id === id) if (before && before.status !== 'running') { // Flipped in the write: a resume its agent.spawn already logged is not logged again. let isResumed = false await update($, agents, list => listOf(list) .map(normalizeCard) .map(c => { if (c.id !== id || c.status === 'running') return c isResumed = true return resumeCard(c) }), ) if (isResumed) { await say($, await whoIs($, id), 'resumed', 'info', id) await refreshStatus($, cfg) } } const result = yield* next(e) const [cards, a] = await Promise.all([getCards($), getArchitect($)]) if (cards.some(c => c.id === id)) { const step = { model: e.model, usage: result.usage, stopReason: result.stopReason } await update($, agents, list => listOf(list).map(normalizeCard).map(c => (c.id === id ? applyStep(c, step) : c))) if (result.stopReason === 'max_tokens') await say($, await whoIs($, id), 'hit max_tokens', 'error', id) } else if (!a.ids.includes(id)) { const now = await $.clock.now() await update($, loops, l => stepLoop(listOf(l), id, now)) } return result }) on('session.measure', async ($, e, next) => { // The compaction window is read once per model window, not on every measurement (a breakdown // estimates every category each time): claimed here, in the same write, so no two reads overlap. let isStale = false await update($, usage, x => { const claim = claimCompactRead(normalize(DEFAULT_USAGE, x), e.context.window) isStale = claim.isClaimed return { ...claim.usage, pct: e.context.percent ?? null, tokens: e.context.tokens ?? null, window: e.context.window, costUsd: e.cost?.usd ?? null, limits: e.rateLimits.map(limitOf), } }) if (e.changed.includes('context')) await refreshStatus($, cfg) const done = await next(e) if (isStale) { await readCompactWindow($, e.context.window) await refreshStatus($, cfg) } return done }) on('session.compact', async ($, e, next) => { const done = await next(e) if (!e.agentId && e.trigger !== 'precompute') { const now = await $.clock.now() await update($, usage, x => { const u = normalize(DEFAULT_USAGE, x) return { ...u, compactions: u.compactions + 1, lastCompactAt: now } }) await say($, 'main', `context compacted (${e.trigger})`) } return done }) on('tool.check', async ($, e, next) => { const verdict = await next(e) if (e.tool_use_id) { const check: Check = { id: e.tool_use_id, tool: e.tool, bucket: bucketOf(e.tool), verdict: verdict.decision === 'allow' ? 'rule' : verdict.decision, inSubagent: Boolean(callLoop.get(e.tool_use_id)), detail: shorten(describeInput(e.tool, e.input), 90), at: await $.clock.now(), } await update($, gate, g => recordCheck(normalizeGate(g), check)) // The status line updates when the call settles; only a refusal ends here. if (verdict.decision === 'deny') { await say($, 'gate', `denied by rule · ${check.detail}`, 'error') await refreshStatus($, cfg) } } return verdict }) on('tool.call', async ($, e, next) => { callLoop.set(e.tool_use_id, e.agentId ?? null) const ran = await next(e).finally(() => callLoop.delete(e.tool_use_id)) const didRun = ran.deny === undefined // Settle this call's pending ask, if it had one; skip the write (and the redraw) otherwise. const g0 = await getGate($) const isSettled = settleCheck(g0, e.tool_use_id, didRun) !== g0 if (isSettled) await update($, gate, g => settleCheck(normalizeGate(g), e.tool_use_id, didRun)) // A background agent hands its report back through this tool; an architect's report is its advice. if (String(e.tool) === 'SubagentHandback') { const message = (e as unknown as { message?: unknown }).message const a = e.agentId ? await getArchitect($) : null if (a && e.agentId && a.ids.includes(e.agentId) && typeof message === 'string') { const advice = adviceLine(message) if (advice && advice !== a.lastAdvice) await noteAdvice($, cfg, advice) } return ran } if (e.tool === 'Agent') { if (isSettled) await refreshStatus($, cfg) return ran } const hasFailed = didRun && ran.isError === true const isEdit = !hasFailed && didRun && EDIT_TOOLS.has(e.tool) const t0 = await getTurn($) if (isEdit || hasFailed || (!e.agentId && t0.errorStreak > 0)) { await update($, turn, t => afterCall(normalize(DEFAULT_TURN, t), { inSubagent: Boolean(e.agentId), hasFailed, isEdit })) } const text = shorten(describeInput(e.tool, e), 64) if (e.agentId) { const id = e.agentId await update($, agents, list => listOf(list) .map(normalizeCard) .map(c => (c.id === id ? noteTool(c, { tool: e.tool, text, isError: hasFailed || ran.deny !== undefined }) : c)), ) } // The log keeps what is worth a glance: refusals, errors and edits; the rest is on the cards. if (ran.deny !== undefined) await say($, await whoIs($, e.agentId), `${text} denied`, 'error', e.agentId ?? null) else if (hasFailed) await say($, await whoIs($, e.agentId), `${text} ✗`, 'error', e.agentId ?? null) else if (isEdit) await say($, await whoIs($, e.agentId), text, 'info', e.agentId ?? null) if (isSettled || !didRun) await refreshStatus($, cfg) return ran }) // A server-side review tool never reaches tool.call: it shows only in the assistant's rows. on('session.append', async ($, e, next) => { if (!e.agentId && e.message.type === 'assistant') { const a = await getArchitect($) // Consults this row opened: their result may be in the same row, after the stale read above. const opened = new Set() for (const block of e.message.content as unknown as readonly ServerBlock[]) { if (block.type === 'server_tool_use' && block.name && block.id && cfg.architect.test(block.name)) { if (a.seen.includes(block.id)) continue const id = block.id await update($, architect, x => { const y = normalize(DEFAULT_ARCHITECT, x) return { ...y, seen: [...listOf(y.seen), id].slice(-60) } }) opened.add(id) await consultStarted($, cfg, id, `${block.name} tool`) } else if (block.type.endsWith('_tool_result') && block.tool_use_id) { const id = block.tool_use_id const isOpen = opened.has(id) || (await getArchitect($)).consults.some(c => c.id === id && c.endAt === null) if (isOpen) await consultEnded($, cfg, null, id) } } } return next(e) }) on('agent.spawn', async ($, e, next) => { const started = await next(e) if (!e.parentAgentId) await noteMode($, e.permissionMode) if (started.deny !== undefined || !started.agentId) return started const id = started.agentId if (await isArchitectType($, cfg, e.subagentType)) { await update($, architect, a => { const x = normalize(DEFAULT_ARCHITECT, a) return { ...x, ids: [...listOf(x.ids), id].slice(-40) } }) await update($, loops, l => listOf(l).filter(x => x.id !== id)) await consultStarted($, cfg, id, e.subagentType.split(':').pop() ?? 'agent') return started } const card: AgentCard = { ...normalizeCard({}), id, type: e.name ?? e.subagentType, model: started.model, description: e.description, spawnedAt: await $.clock.now(), } // Decided in the write: a resume whose first step already put its card back to running is not logged again. let isNew = true let isResumed = false await update($, agents, list => { const cards = listOf(list).map(normalizeCard) const known = cards.find(c => c.id === id) isNew = known === undefined isResumed = known !== undefined && known.status !== 'running' return addCard(cards, card) }) await update($, loops, l => listOf(l).filter(x => x.id !== id)) if (isNew || isResumed) await say($, shorten(cardTitle(card), 12), isNew ? `spawned · ${card.type}` : 'resumed', 'info', id) await refreshStatus($, cfg) return started }) on('turn.complete', async ($, e, next) => { const done = await next(e) const id = e.agentId const now = await $.clock.now() if (!id) { const [t, cards, cost] = await Promise.all([getTurn($), getCards($), costNow($)]) const r = receiptOf(t, { durationMs: e.durationMs, agentsSince: cards.filter(c => c.spawnedAt >= t.startedAt).length, costNow: cost, reason: e.reason, }) await update($, receipt, () => r) await update($, main, m => ({ ...normalize(DEFAULT_MAIN, m), isRunning: false })) await refreshStatus($, cfg) return done } if ((await getArchitect($)).ids.includes(id)) { await consultEnded($, cfg, e.answer, id) return done } const cards = await getCards($) if (cards.some(c => c.id === id)) { const status = e.reason === 'answer' ? 'done' : e.reason === 'aborted' ? 'stopped' : 'failed' await update($, agents, list => listOf(list) .map(normalizeCard) .map(c => (c.id === id ? { ...c, status, endedAt: now, answer: shorten(e.answer, 400) } : c)), ) // This run's own length: a resumed agent's card clock also spans the idle time between runs. const took = fmtDuration(e.durationMs) await say($, await whoIs($, id), status === 'done' ? `done · ${took}` : status, status === 'done' ? 'done' : 'error', id) } else { await update($, loops, l => listOf(l).map(x => (x.id === id ? { ...x, isDone: true, lastAt: now } : x))) } await refreshStatus($, cfg) return done }) // ---------------------------------------------------------------- drawing // After a surface module faults on a surface, this load draws it there as plain text; the other // module, and other surfaces, keep theirs. Keyed by faultKey: `desktop:rail.tsx`. const faulted = new Set() on('ui.fault', async ($, e, next) => { const key = faultKey(e.surface, e.module) if (e.requestId === PANE && !faulted.has(key)) { faulted.add(key) $.ui.log(`flightdeck: ${e.module} faulted on ${e.surface} (${e.phase}: ${e.reason}); drawing it without animation`) $.ui.invalidate('ui.render') } return next(e) }) on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => { const els = $.ui.resolve(e) const { Box, Text, Button } = els const hasClient = 'Client' in els const hasRail = hasClient && !faulted.has(faultKey(e.surface, 'rail.tsx')) const hasClock = hasClient && !faulted.has(faultKey(e.surface, 'elapsed.tsx')) const [m, u, a, g, cards, lp, lines, t, r, v, now] = await Promise.all([ getMain($), getUsage($), getArchitect($), getGate($), getCards($), getLoops($), getLog($), getTurn($), read($, receipt), getView($), $.clock.now(), ]) // The body's own width, down to the narrowest dock Claude Code seats (24): a wider drawing is cut off at the frame. const W = Math.max(24, e.props.bodyColumns) const layout = v.layout ?? cfg.layout const isWide = layout === 'wide' || (layout === 'auto' && W >= 110) const colW = isWide ? Math.floor((W - 2) / 2) : W const modelName = prettyModel(m.model) const viewed = e.props.view?.agentId ?? null const advising = isAdvising(a) const running = cards.filter(c => c.status === 'running') const showArchitect = a.consults.length > 0 || a.ids.length > 0 const motion = cfg.motion && hasRail // A panel with nothing to show yet takes no room: most sessions never spawn an agent. const isEmpty: Record = { main: false, architect: !showArchitect, gate: g.recent.length === 0 && gateSummary(g).total === 0, agents: cards.length === 0, loops: lp.length === 0, receipt: !m.isRunning && !r, log: false, } const panels = cfg.panels.filter(p => !isEmpty[p]) const decider = m.mode === 'auto' ? 'classifier' : 'you' // A connector between panels: animated while its flow is live, a dim line otherwise. const rail = (key: string, active: boolean, color: string, width: number, marks: number[] = [], isMerge = false) => motion ? ( ) : ( {'─'.repeat(Math.max(1, width))} ) // A start time of 0 is unknown (state saved before it was recorded): no clock, not decades. const clock = (key: string, since: number, endAt: number | null, color: string) => since <= 0 ? ( — ) : hasClock ? ( ) : ( {fmtTimer((endAt ?? now) - since)} ) const isTerminal = e.surface === 'terminal' // A row of marks (gate checks, loop dots), sized by `marksIn`. Should a surface draw them wider // still, the row is cut at its box rather than running past it. const markRow = (marks: RenderChildren, lead = 0) => ( {marks} ) // A terminal's border row stands apart from the text above it; a graphical surface draws the // border flush against it, so the legend keeps one row of air there. const legendGap = isTerminal ? 0 : 1 // A line of coloured pieces, drawn as a row of boxes the way the log draws its lines. A Text nesting // coloured Texts truncates as one line in a terminal, but the desktop app wraps each nested piece on its // own, lays truncating ones out as blocks of their own, and collapses runs of spaces. So pieces sit `gap` // cells apart (a piece's `lead` adds more), each keeps its width, and pieces that don't fit in `room` are // left out from the end; or a piece marked `fill` (one span) truncates to what the others leave. type Span = { text: string; color?: string; dim?: boolean; bold?: boolean } type Piece = { spans: Span[]; lead?: number; fill?: boolean } const styleOf = (s: Span) => ({ ...(s.color ? { color: s.color } : {}), ...(s.dim ? { dimColor: true } : {}), ...(s.bold ? { bold: true } : {}) }) const widthOf = (p: Piece) => p.spans.reduce((n, s) => n + cellWidth(s.text), 0) const line = (pieces: (Piece | null)[], room: number, gap = 1, justify?: 'center') => { const all = pieces.filter((p): p is Piece => p !== null) const shown = all.some(p => p.fill) ? all : all.slice(0, piecesThatFit(all.map(p => ({ width: widthOf(p), lead: p.lead })), room, gap)) return ( {shown.map(p => { const lead = p.lead ? { marginLeft: p.lead } : {} if (p.fill && p.spans[0]) { return ( {p.spans[0].text} ) } return ( {p.spans.length === 1 && p.spans[0] ? {p.spans[0].text} : {p.spans.map(s => {s.text})}} ) })} ) } // ---- main const effortN = { low: 1, medium: 2, high: 3, xhigh: 4, max: 4 }[m.effort] ?? 0 const ctxFill = contextFill(u) const ctxGauge = ctxFill.pct !== null ? gauge(ctxFill.pct, 10) : null const mainPanel = (w: number) => ( {modelName} · main {m.isRunning ? '● working' : '○ idle'} {line( [ { spans: [ { text: 'effort ', dim: true }, { text: `${'▮'.repeat(effortN) + '▯'.repeat(4 - effortN)} `, color: C.main }, { text: m.effort || '—', color: C.main, bold: true }, ], }, m.mode ? { spans: [{ text: `mode ${m.mode}`, dim: true }] } : null, { spans: [{ text: `${m.steps} req`, dim: true }] }, ], w - 4, 3, )} {ctxGauge ? line( [ { spans: [ { text: 'ctx ', dim: true }, { text: ctxGauge.on, color: ctxFill.pct !== null && ctxFill.pct >= 80 ? C.warn : C.main }, { text: ctxGauge.off, color: C.faint }, { text: ` ${Math.round(ctxFill.pct ?? 0)}%`, bold: true }, ], }, u.tokens !== null ? { spans: [{ text: `${kTokens(u.tokens)}/${kTokens(ctxFill.window)}`, dim: true }] } : null, u.compactions > 0 ? { spans: [{ text: `⟲${u.compactions}`, color: C.amber }], lead: 1 } : null, ], w - 4, ) : null} {u.costUsd !== null || u.limits.length > 0 ? line( [ u.costUsd !== null ? { spans: [{ text: fmtUsd(u.costUsd), color: C.text }] } : null, ...u.limits.slice(0, 2).map((l, i): Piece => { const lg = gauge(l.pct, 5) // The time to each reset where the line has room for it: both windows fit from 56 columns, // so a wide pane's half column (59 at 120) has them; a window that doesn't fit is left out. const resetIn = w >= 56 ? fmtResetIn(l.resetsAt, now) : '' return { lead: i > 0 ? 1 : u.costUsd !== null ? 2 : 0, spans: [ { text: `${limitLabel(l.kind)} `, dim: true }, { text: lg.on, color: l.pct >= 80 ? C.warn : C.main }, { text: lg.off, color: C.faint }, { text: ` ${Math.round(l.pct)}%${resetIn ? ` ↻${resetIn}` : ''}`, dim: true }, ], } }), ], w - 4, ) : null} {m.speed ? {`last req ${fmtSpeed(m.speed)}`} : null} ) // ---- architect const lastConsult = a.consults[a.consults.length - 1] const architectPanel = (w: number) => { // Both ends are anchors (the first consult, now): should it run past its box, it asks to lose the middle. const tl = consultTimeline(a, now, Math.max(8, w - 4)) return ( {cfg.architectLabel} · {advising ? 'advising' : 'on call'} consults {a.consults.length} {tl} {lastConsult ? ( {advising ? `consulting since ${fmtClock(lastConsult.at)}` : `last ${fmtDuration(now - (lastConsult.endAt ?? lastConsult.at))} ago · took ${fmtDuration((lastConsult.endAt ?? now) - lastConsult.at)}`} ) : ( not consulted yet )} {cfg.moments ? ( {(['before a plan', 'error repeats', 'before done'] as const).map(mo => { const isOn = lastConsult?.moment === mo return ( {isOn ? '◆' : '◇'} {mo} ) })} (inferred) ) : null} {a.lastAdvice ? ( » {a.lastAdvice} ) : null} ) } // ---- gate const s = gateSummary(g) const verdictColor = (c: Check) => c.verdict === 'rule' ? C.gate : c.verdict === 'cleared' ? C.cleared : c.verdict === 'ask' ? C.amber : C.warn const checkMark = (c: Check) => ( {c.verdict === 'deny' ? '✗' : '■'} ) const gatePanel = (w: number) => { const strip = g.recent.slice(-Math.max(8, marksIn(w - 4, isTerminal))) const open = v.gateOpen // The totals in words where they all fit, in numbers alone where they don't: the colours say which is which. const totalsOf = (isShort: boolean): Piece[] => [ { spans: [{ text: '■', color: C.gate }, { text: isShort ? ` ${s.rule}` : ` ${s.rule} allowed`, dim: true }] }, { spans: [{ text: '■', color: C.cleared }, { text: isShort ? ` ${s.cleared}` : ` ${s.cleared} ${decider}`, dim: true }] }, ...(s.ask > 0 ? [{ spans: [{ text: isShort ? `■ ${s.ask}` : `■ ${s.ask} pending`, color: C.amber }] }] : []), { spans: [{ text: isShort ? `✗ ${s.deny}` : `✗ ${s.deny} denied`, color: s.deny > 0 ? C.warn : C.dim }] }, ] const words = totalsOf(false) const isShort = piecesThatFit(words.map(p => ({ width: widthOf(p) })), w - 4, 2) < words.length const totals = isShort ? totalsOf(true) : words return ( {cfg.gateLabel} · permissions {`${s.total} checks`} {strip.length === 0 ? no checks yet : markRow(strip.map(checkMark))} {line([...totals, !isShort && w >= 80 && g.recent.some(c => c.inSubagent) ? { spans: [{ text: 'dim: in subagents', color: C.faint }] } : null], w - 4, 2)} {/* Below about 44 columns the three rows' buttons take two lines rather than run past the box. */} {(['file', 'shell', 'other'] as const).map((b: Bucket) => { const tl = g.totals[b] const n = tl.rule + tl.ask + tl.cleared + tl.deny return (