From 37b74f4df332e62a32d5a9a69af32e170f08d899 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Mon, 8 Jun 2026 18:09:32 +0000 Subject: [PATCH] opentui(v2): live agent trace + /tools navigable overlay (items 9, 15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item 15 — "/agents doesn't let me look into an agent trace live": - store accumulates a concise per-subagent trace from the subagent.* stream (▶ start / ⚡ tool — preview / progress text / ✓ summary), capped at 200 lines; thinking deltas update a transient `thought` (not appended — they'd flood). - AgentsDashboard is now master-detail: ↑/↓ select a subagent (▸ + accent), and the bottom pane shows the selected agent's goal · status · model, its latest thought, and a sticky-bottom (live) trace scrollbox. PgUp/PgDn scroll the trace. Item 9 — /tools wired to a deliberate navigable overlay (fetch the roster via slash.exec → pager) instead of incidental fallthrough; /skills already opens the native picker. Tests: store trace accumulation + dashboard render (trace line + footer). 71 pass. Live-smoked: /tools → tool roster pager; /skills → picker; a real delegation (spawn a subagent → reply PURPLE) → /agents showed the subagent with its goal · completed · model, 🧠 PURPLE thought, and ▶/✓ trace lines. --- ui-tui-opentui-v2/src/logic/slash.ts | 12 ++ ui-tui-opentui-v2/src/logic/store.ts | 18 +++ ui-tui-opentui-v2/src/test/render.test.tsx | 9 +- ui-tui-opentui-v2/src/test/store.test.ts | 13 ++ .../src/view/overlays/agentsDashboard.tsx | 121 ++++++++++++------ 5 files changed, 132 insertions(+), 41 deletions(-) diff --git a/ui-tui-opentui-v2/src/logic/slash.ts b/ui-tui-opentui-v2/src/logic/slash.ts index 188d7f94989..7543fd88b34 100644 --- a/ui-tui-opentui-v2/src/logic/slash.ts +++ b/ui-tui-opentui-v2/src/logic/slash.ts @@ -224,6 +224,17 @@ const skillsCmd: ClientHandler = async (_arg, ctx) => { }) } +/** `/tools` — fetch the tool roster from the gateway and show it in the pager (navigable). */ +const toolsCmd: ClientHandler = async (arg, ctx) => { + const command = arg.trim() ? `tools ${arg.trim()}` : 'tools' + try { + const r = await ctx.request('slash.exec', { command, session_id: ctx.sessionId() }) + ctx.openPager('Tools', readStr(r, 'output') || '(no tool info)') + } catch (error) { + ctx.pushSystem(`/tools: ${error instanceof Error ? error.message : 'failed'}`) + } +} + /** The TUI-only client commands (run in-process, never hit the gateway). */ const CLIENT: Record = { agents: (_arg, ctx) => ctx.openDashboard(), @@ -236,6 +247,7 @@ const CLIENT: Record = { skills: skillsCmd, switch: openSwitcher, tasks: (_arg, ctx) => ctx.openDashboard(), + tools: toolsCmd, help: async (_arg, ctx) => { // Prefer the live catalog; fall back to the client list if it's unavailable. try { diff --git a/ui-tui-opentui-v2/src/logic/store.ts b/ui-tui-opentui-v2/src/logic/store.ts index 2cdea4f2d47..4e954991eba 100644 --- a/ui-tui-opentui-v2/src/logic/store.ts +++ b/ui-tui-opentui-v2/src/logic/store.ts @@ -103,8 +103,15 @@ export interface SubagentInfo { parentId?: string summary?: string lastTool?: string + /** Live activity trace (item 15) — tool/progress/summary lines, newest last. */ + trace?: string[] + /** Latest thinking text (transient; not appended to the trace to avoid flooding). */ + thought?: string } +/** Cap on a subagent's retained trace lines. */ +const SUBAGENT_TRACE_LIMIT = 200 + /** * Live session chrome (the status bar — item 14). Sourced from the `session.info` * event (and the `session.create`/`resume` result's `info`), refreshed whenever @@ -562,6 +569,17 @@ export function createSessionStore() { const tool = readStr(event.payload, 'tool_name') if (tool) sa.lastTool = tool sa.status = readStr(event.payload, 'status') ?? subagentStatusFor(event.type) + + // Live trace (item 15): a concise per-subagent activity log. Thinking + // deltas update a transient `thought` (not appended — they'd flood). + const text = readStr(event.payload, 'text') + const trace = (sa.trace ??= []) + if (event.type === 'subagent.start') trace.push(`▶ ${goal ?? sa.goal ?? 'started'}`) + else if (event.type === 'subagent.tool' && tool) trace.push(`⚡ ${tool}${text ? ` — ${text}` : ''}`) + else if (event.type === 'subagent.progress' && text) trace.push(text) + else if (event.type === 'subagent.complete') trace.push(`✓ ${summary ?? 'done'}`) + else if (event.type === 'subagent.thinking' && text) sa.thought = text + if (trace.length > SUBAGENT_TRACE_LIMIT) trace.splice(0, trace.length - SUBAGENT_TRACE_LIMIT) }) ) break diff --git a/ui-tui-opentui-v2/src/test/render.test.tsx b/ui-tui-opentui-v2/src/test/render.test.tsx index 0756817c1e7..56bda3ea04c 100644 --- a/ui-tui-opentui-v2/src/test/render.test.tsx +++ b/ui-tui-opentui-v2/src/test/render.test.tsx @@ -213,7 +213,7 @@ describe('App render (Phase 1, themed)', () => { type: 'subagent.start', payload: { subagent_id: 'a1', goal: 'research the topic', model: 'haiku', depth: 0 } }) - store.apply({ type: 'subagent.tool', payload: { subagent_id: 'a1', tool_name: 'web_search' } }) + store.apply({ type: 'subagent.tool', payload: { subagent_id: 'a1', tool_name: 'web_search', text: 'opentui' } }) store.openDashboard() const frame = await captureFrame( @@ -222,12 +222,13 @@ describe('App render (Phase 1, themed)', () => { ), - { until: 'Agents', width: 72, height: 18 } + { until: 'Agents', width: 72, height: 24 } ) expect(frame).toContain('Agents') // dashboard header - expect(frame).toContain('research the topic') // subagent goal - expect(frame).toContain('web_search') // its last tool + expect(frame).toContain('research the topic') // subagent goal (list + detail header) + expect(frame).toContain('web_search') // last tool + live trace line (item 15) + expect(frame).toContain('select') // footer hint "↑↓ select" expect(frame).not.toContain('parent turn') // transcript replaced by the dashboard }) }) diff --git a/ui-tui-opentui-v2/src/test/store.test.ts b/ui-tui-opentui-v2/src/test/store.test.ts index d7bf348cafc..55eca6ee10e 100644 --- a/ui-tui-opentui-v2/src/test/store.test.ts +++ b/ui-tui-opentui-v2/src/test/store.test.ts @@ -191,6 +191,19 @@ describe('session store — subagents (Phase 5e agents dashboard)', () => { expect(store.state.subagents[0]).toMatchObject({ status: 'complete', summary: 'found it' }) }) + test('accumulates a live trace per subagent (item 15) + transient thought', () => { + const store = createSessionStore() + store.apply({ type: 'subagent.start', payload: { subagent_id: 'a1', goal: 'crunch data' } }) + store.apply({ type: 'subagent.thinking', payload: { subagent_id: 'a1', text: 'considering options' } }) + store.apply({ type: 'subagent.tool', payload: { subagent_id: 'a1', tool_name: 'web_search', text: 'opentui' } }) + store.apply({ type: 'subagent.progress', payload: { subagent_id: 'a1', text: 'found 3 hits' } }) + store.apply({ type: 'subagent.complete', payload: { subagent_id: 'a1', summary: 'done crunching' } }) + const sa = store.state.subagents[0]! + // thinking text is transient (not in the trace), the rest is a concise log + expect(sa.thought).toBe('considering options') + expect(sa.trace).toEqual(['▶ crunch data', '⚡ web_search — opentui', 'found 3 hits', '✓ done crunching']) + }) + test('clearTranscript also clears subagents', () => { const store = createSessionStore() store.apply({ type: 'subagent.start', payload: { subagent_id: 'a1', goal: 'g' } }) diff --git a/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx b/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx index ad84bbd085c..4f956619c6c 100644 --- a/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx +++ b/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx @@ -1,17 +1,21 @@ /** - * AgentsDashboard — the delegation/subagents view (spec §2b; Ink `agentsOverlay`). - * Full-height overlay (replaces transcript+composer) listing the subagents tracked - * from the `subagent.*` event stream, indented by `depth` (a flat tree). Scroll via - * useKeyboard→scrollBy/scrollTo; Esc/q close. §8 #2 scrollbox gotchas. + * AgentsDashboard — the delegation/subagents view (spec §2b; Ink `agentsOverlay`, + * item 15 "look into an agent trace live"). Master-detail: + * - top: the subagents tracked from the `subagent.*` stream, indented by depth; + * ↑/↓ SELECT a row (highlighted). + * - bottom: the SELECTED subagent's live trace (goal · status · model, latest + * thought, and the tool/progress/summary log) — sticky-bottom so it follows + * live; PgUp/PgDn scroll it. + * Esc/q close. §8 #2 scrollbox gotchas (minHeight:0, sticky bottom). */ import { type ScrollBoxRenderable } from '@opentui/core' import { useKeyboard } from '@opentui/solid' -import { For, Show } from 'solid-js' +import { createSignal, For, Show } from 'solid-js' import type { SubagentInfo } from '../../logic/store.ts' import { useTheme } from '../theme.tsx' -const PAGE = 10 +const PAGE = 8 function statusColor(status: string, theme: ReturnType): string { const c = theme().color @@ -23,20 +27,22 @@ function statusColor(status: string, theme: ReturnType): string export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: () => void }) { const theme = useTheme() - let box: ScrollBoxRenderable | undefined + const [sel, setSel] = createSignal(0) + let traceBox: ScrollBoxRenderable | undefined + + const count = () => props.subagents.length + const selected = () => Math.min(sel(), Math.max(0, count() - 1)) + const current = () => props.subagents[selected()] useKeyboard(key => { if (key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c')) { props.onClose() return } - if (!box) return - if (key.name === 'up') box.scrollBy(-1) - else if (key.name === 'down') box.scrollBy(1) - else if (key.name === 'pageup') box.scrollBy(-PAGE) - else if (key.name === 'pagedown') box.scrollBy(PAGE) - else if (key.name === 'home') box.scrollTo(0) - else if (key.name === 'end') box.scrollTo({ x: 0, y: box.scrollHeight }) + if (key.name === 'up') setSel(s => Math.max(0, s - 1)) + else if (key.name === 'down') setSel(s => Math.min(Math.max(0, count() - 1), s + 1)) + else if (key.name === 'pageup') traceBox?.scrollBy(-PAGE) + else if (key.name === 'pagedown') traceBox?.scrollBy(PAGE) }) return ( @@ -44,34 +50,75 @@ export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: () - ⛓ Agents · {props.subagents.length} subagent{props.subagents.length === 1 ? '' : 's'} + ⛓ Agents · {count()} subagent{count() === 1 ? '' : 's'} - - (box = el)} style={{ flexGrow: 1, minHeight: 0 }}> - 0} - fallback={No subagents yet — delegate a task to spawn one.} - > - - {sa => ( - - {' '.repeat(Math.max(0, sa.depth))} - {`● ${sa.status}`} - {` ${sa.goal || sa.id}`} - - {sa.model ? ` (${sa.model})` : ''} - {sa.lastTool ? ` ⚡${sa.lastTool}` : ''} - - - )} - - - + + {/* master: the subagent list (↑/↓ select) */} + + 0} + fallback={No subagents yet — delegate a task to spawn one.} + > + + {(sa, i) => ( + setSel(i())}> + {' '.repeat(Math.max(0, sa.depth))} + + {i() === selected() ? '▸ ' : ' '} + + {`● ${sa.status}`} + {` ${sa.goal || sa.id}`} + {sa.lastTool ? ` ⚡${sa.lastTool}` : ''} + + )} + + + + {/* detail: the selected subagent's live trace */} + + }> + {sa => ( + <> + + + {sa().goal || sa().id} + {` · ${sa().status}`} + {sa().model ? ` · ${sa().model}` : ''} + + + + + + {`🧠 ${sa().thought}`} + + + + + (traceBox = el)} style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom"> + 0} + fallback={(no activity yet)} + > + + {line => ( + + {line} + + )} + + + + + + )} + + + - Esc/q close · ↑↓/PgUp/PgDn scroll + Esc/q close · ↑↓ select · PgUp/PgDn scroll trace )