From 74cb03423ec15293fa97d43f6e1cf3b2f467a703 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Sat, 13 Jun 2026 21:56:02 +0530 Subject: [PATCH] =?UTF-8?q?opentui(v6):=20background-activity=20P2=20?= =?UTF-8?q?=E2=80=94=20de-crowd=20the=20agents=20dashboard=20+=20typed=20t?= =?UTF-8?q?race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agents dashboard (rplj pain) dumped each subagent's full multi-line prompt into the master list, wrapping it into a wall of text and squeezing the trace. Now each master row is ONE line: status + a width-budgeted truncated goal + model. The detail pane still shows the full goal (the inspect half) and renders the activity as a TYPED transcript instead of flat lines: SubagentInfo.trace is now TraceEntry[] ({kind:'start'|'tool'|'progress'|'summary'}), drawn with per-kind glyph+color (▶ start / ⚡ tool accent / · progress muted / ✓ summary green). No foregrounding (kept subagent UX unchanged — inspection only, per the brainstorm). TUI-only. Gate green; new agentsDashboard.test.tsx asserts one-line truncation + typed-trace render; store.test trace assertion updated to the typed shape. --- ui-opentui/src/logic/store.ts | 20 ++++-- ui-opentui/src/test/agentsDashboard.test.tsx | 51 +++++++++++++++ ui-opentui/src/test/store.test.ts | 9 ++- ui-opentui/src/view/agentsTray.tsx | 2 +- .../src/view/overlays/agentsDashboard.tsx | 64 +++++++++++++++---- 5 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 ui-opentui/src/test/agentsDashboard.test.tsx diff --git a/ui-opentui/src/logic/store.ts b/ui-opentui/src/logic/store.ts index d9974827759..e49c02d9df3 100644 --- a/ui-opentui/src/logic/store.ts +++ b/ui-opentui/src/logic/store.ts @@ -153,6 +153,13 @@ export interface CompletionItem { meta: string } +/** One typed entry in a subagent's activity trace — `kind` drives glyph + color + * in the dashboard so the trace reads like a transcript, not flat dumped lines. */ +export interface TraceEntry { + kind: 'start' | 'tool' | 'progress' | 'summary' + text: string +} + /** A delegated subagent, tracked from the `subagent.*` event stream (agents dashboard). */ export interface SubagentInfo { id: string @@ -163,8 +170,8 @@ export interface SubagentInfo { parentId?: string summary?: string lastTool?: string - /** Live activity trace (item 15) — tool/progress/summary lines, newest last. */ - trace?: string[] + /** Live activity trace (item 15) — typed entries, newest last; rendered by kind. */ + trace?: TraceEntry[] /** Latest thinking text (transient; not appended to the trace to avoid flooding). */ thought?: string } @@ -971,10 +978,11 @@ export function createSessionStore(options?: SessionStoreOptions) { // 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'}`) + if (event.type === 'subagent.start') trace.push({ kind: 'start', text: goal ?? sa.goal ?? 'started' }) + else if (event.type === 'subagent.tool' && tool) + trace.push({ kind: 'tool', text: text ? `${tool} — ${text}` : tool }) + else if (event.type === 'subagent.progress' && text) trace.push({ kind: 'progress', text }) + else if (event.type === 'subagent.complete') trace.push({ kind: 'summary', text: 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) }) diff --git a/ui-opentui/src/test/agentsDashboard.test.tsx b/ui-opentui/src/test/agentsDashboard.test.tsx new file mode 100644 index 00000000000..e9d6ef5204f --- /dev/null +++ b/ui-opentui/src/test/agentsDashboard.test.tsx @@ -0,0 +1,51 @@ +/** + * Agents dashboard (P2 de-crowd) — the master list is ONE line per subagent + * (long goals truncate, no multi-line prompt dump) and the detail pane renders + * the TYPED trace by kind (⚡ tool / · progress / ✓ summary). + */ +import { describe, expect, test } from 'vitest' + +import { createSessionStore } from '../logic/store.ts' +import { App } from '../view/App.tsx' +import { ThemeProvider } from '../view/theme.tsx' +import { captureFrame } from './lib/render.ts' + +const LONG_GOAL = + 'Poll the current UTC time 10 times with a 3-second sleep between each poll, run date -u and record each result, then report all ten timestamps as a timing exercise' + +function dash() { + const store = createSessionStore() + store.apply({ type: 'gateway.ready' }) + store.apply({ + type: 'subagent.start', + payload: { subagent_id: 'a1', goal: LONG_GOAL, model: 'anthropic/claude-opus-4-8', depth: 0 } + }) + store.apply({ type: 'subagent.tool', payload: { subagent_id: 'a1', tool_name: 'terminal', text: 'date -u' } }) + store.apply({ type: 'subagent.progress', payload: { subagent_id: 'a1', text: 'poll 4 of 10 recorded' } }) + store.apply({ type: 'subagent.complete', payload: { subagent_id: 'a1', summary: 'all ten timestamps collected' } }) + store.openDashboard() + return () => ( + store.state.theme}> + + + ) +} + +describe('agents dashboard de-crowd (P2)', () => { + test('a long goal is truncated to one line in the master list (no full-prompt wall)', async () => { + const frame = await captureFrame(dash(), { until: 'Agents', width: 116, height: 30 }) + // The master row truncates to one line — the head shows with an ellipsis. + // (The detail pane below still shows the full goal; that's the inspect half.) + expect(frame).toContain('Poll the current UTC time') + expect(frame).toContain('…') // ellipsis proves the master row is one-line, not a wrapped wall + }) + + test('the detail pane renders the typed trace by kind (tool ⚡, summary ✓)', async () => { + const frame = await captureFrame(dash(), { until: 'Agents', width: 116, height: 30 }) + expect(frame).toContain('⚡') // tool entry glyph + expect(frame).toContain('terminal — date -u') // tool entry text + expect(frame).toContain('✓') // summary entry glyph + expect(frame).toContain('all ten timestamps collected') // summary text (detail, not master) + expect(frame).toContain('poll 4 of 10 recorded') // progress entry + }) +}) diff --git a/ui-opentui/src/test/store.test.ts b/ui-opentui/src/test/store.test.ts index 5216a392c54..01ab30c3129 100644 --- a/ui-opentui/src/test/store.test.ts +++ b/ui-opentui/src/test/store.test.ts @@ -335,9 +335,14 @@ describe('session store — subagents (Phase 5e agents dashboard)', () => { 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 + // thinking text is transient (not in the trace), the rest is a concise TYPED log expect(sa.thought).toBe('considering options') - expect(sa.trace).toEqual(['▶ crunch data', '⚡ web_search — opentui', 'found 3 hits', '✓ done crunching']) + expect(sa.trace).toEqual([ + { kind: 'start', text: 'crunch data' }, + { kind: 'tool', text: 'web_search — opentui' }, + { kind: 'progress', text: 'found 3 hits' }, + { kind: 'summary', text: 'done crunching' } + ]) }) test('clearTranscript also clears subagents', () => { diff --git a/ui-opentui/src/view/agentsTray.tsx b/ui-opentui/src/view/agentsTray.tsx index ea66be51bab..820defbe61f 100644 --- a/ui-opentui/src/view/agentsTray.tsx +++ b/ui-opentui/src/view/agentsTray.tsx @@ -175,7 +175,7 @@ function TrayRows(props: { agents: SubagentInfo[]; selected: number; firstSeen: {(sa, i) => { const active = () => i() === props.selected - const last = () => sa.trace?.at(-1) ?? sa.thought + const last = () => sa.trace?.at(-1)?.text ?? sa.thought const secs = () => (tick(), elapsedSeconds(props.firstSeen.get(sa.id) ?? Date.now())) return ( ): string return c.warn } +/** Keep the head of a string, ellipsizing when it must clip (one-line master rows). */ +function truncRight(s: string, max: number): string { + if (max <= 1) return s.length > max ? '…' : s + return s.length <= max ? s : s.slice(0, max - 1) + '…' +} + +/** Per-kind glyph + color for a trace entry — makes the detail pane read like a + * transcript (tool calls pop, progress is quiet, the summary is the payoff). */ +function traceGlyph(kind: TraceEntry['kind']): string { + return kind === 'tool' ? '⚡' : kind === 'summary' ? '✓' : kind === 'start' ? '▶' : '·' +} +function traceColor(kind: TraceEntry['kind'], theme: ReturnType): string { + const c = theme().color + return kind === 'tool' ? c.accent : kind === 'summary' ? c.ok : kind === 'start' ? c.label : c.muted +} + export function AgentsDashboard(props: { subagents: SubagentInfo[] onClose: () => void @@ -33,6 +50,7 @@ export function AgentsDashboard(props: { preselect?: string | undefined }) { const theme = useTheme() + const dims = useDimensions() const [sel, setSel] = createSignal(0) let rootRef: BoxRenderable | undefined let traceBox: ScrollBoxRenderable | undefined @@ -86,18 +104,33 @@ export function AgentsDashboard(props: { when={count() > 0} fallback={No subagents yet — delegate a task to spawn one.} > + {/* ONE line per row (de-crowd, glitch 2026-06-13): indent + select caret + + status + a TRUNCATED goal + model — the full prompt no longer wraps + the master list into a wall of text. The detail pane shows the rest. */} - {(sa, i) => ( - setSel(i())}> - {' '.repeat(Math.max(0, sa.depth))} - - {i() === selected() ? '▸ ' : ' '} - - {`● ${sa.status}`} - {` ${sa.goal || sa.id}`} - {sa.lastTool ? ` ⚡${sa.lastTool}` : ''} - - )} + {(sa, i) => { + const indent = () => ' '.repeat(Math.max(0, sa.depth)) + // budget the goal into the leftover row width so it never wraps: + // total − border/pad − indent − caret(2) − `● status `(status+3) − model tail. + const goalMax = () => { + const modelTail = sa.model ? sa.model.length + 3 : 0 + const used = 4 + indent().length + 2 + (sa.status.length + 3) + modelTail + return Math.max(8, dims().width - used) + } + return ( + setSel(i())}> + {indent()} + + {i() === selected() ? '▸ ' : ' '} + + {`● ${sa.status} `} + + {truncRight(sa.goal || sa.id, goalMax())} + + {sa.model ? ` · ${sa.model}` : ''} + + ) + }} @@ -133,9 +166,12 @@ export function AgentsDashboard(props: { fallback={(no activity yet)} > - {line => ( + {entry => ( - {line} + {`${traceGlyph(entry.kind)} `} + + {entry.text} + )}