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}
+
)}