A live side pane that shows every sub-agent with status, progress, context usage, current activity, prompt and result.

A Claude Code mod that opens a live side pane showing every sub-agent in your session.
When one large agent orchestrates many sub-agents, you can't see what each one is doing. This pane shows it as it happens.
For each sub-agent:
The view is read-only. There are no stop or message controls yet.
/plugin install agent-dashboard --marketplace leowei31/agent-dashboard-mod
Then start a new session. The pane opens at session start. Run /dashboard to open it again.
A sub-agent has no known total amount of work, so the work bar can't be a true percentage. It fills as the agent makes tool calls and is full at 20 calls, or when the agent finishes. The context bar is exact. It uses the token counts of the agent's latest model request against the context window of your session's model.
Run claude plugin test ./agent-dashboard to run the tests. Run claude plugin validate ./agent-dashboard to validate the manifest.
hooks/register.tsx 225 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Row } from '../types'
5
6const PANE = 'agent-dashboard'
7const MAX_ROWS = 100
8const TOOL_BAR_FULL = 20
9const BAR_WIDTH = 16
10const CLIP = 600
11
12const rows = atom({ plugin: 'agent-dashboard', key: 'rows' } as const, [])
13const windowSize = atom({ plugin: 'agent-dashboard', key: 'window' } as const, 200000)
14
15const blank = (id: string, now: number): Row => ({
16 id,
17 description: '',
18 type: '',
19 status: 'running',
20 startedAt: now,
21 endedAt: 0,
22 toolCalls: 0,
23 activity: '',
24 contextTokens: 0,
25 prompt: '',
26 result: '',
27 isOpen: false,
28})
29
30// Applies `change` to the row `id`, adding the row first when an agent was not seen to spawn.
31const patch = (list: readonly Row[], id: string, now: number, change: (row: Row) => Partial<Row>) => {
32 const base = list.some(row => row.id === id) ? list : [...list, blank(id, now)]
33
34 return base.map(row => (row.id === id ? { ...row, ...change(row) } : row)).slice(-MAX_ROWS)
35}
36
37const bar = (fraction: number) => {
38 const filled = Math.round(Math.min(1, Math.max(0, fraction)) * BAR_WIDTH)
39
40 return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
41}
42
43const clip = (text: string) => (text.length > CLIP ? `${text.slice(0, CLIP)}...` : text)
44
45const compact = (tokens: number) =>
46 tokens >= 1_000_000 ? `${(tokens / 1_000_000).toFixed(1)}M` : tokens >= 1000 ? `${Math.round(tokens / 1000)}k` : `${tokens}`
47
48const seconds = (ms: number) => {
49 const total = Math.max(0, Math.round(ms / 1000))
50
51 return total >= 60 ? `${Math.floor(total / 60)}m${String(total % 60).padStart(2, '0')}s` : `${total}s`
52}
53
54const COLORS = { running: 'yellow', done: 'green', failed: 'red', stopped: 'gray' } as const
55const DOTS = { running: '●', done: '✓', failed: '✗', stopped: '■' } as const
56
57export const register: Register = on => {
58 on('session.start', async ($, e, next) => {
59 await $.command.register({
60 name: 'dashboard',
61 description: 'Open the live sub-agent dashboard pane',
62 })
63 void $.ui.open({ id: PANE, title: 'Sub-agents' })
64
65 return next(e)
66 })
67
68 on('command.run', { command: 'dashboard' }, async $ => {
69 await $.ui.open({ id: PANE, title: 'Sub-agents' })
70
71 return { text: 'Sub-agent dashboard opened.' }
72 })
73
74 on('agent.spawn', async ($, e, next) => {
75 const out = await next(e)
76 const id = out.agentId
77
78 if (id !== undefined) {
79 const now = await $.clock.now()
80 await update($, rows, list =>
81 patch(list, id, now, () => ({
82 description: e.description,
83 type: e.subagentType,
84 prompt: e.prompt,
85 status: 'running',
86 })),
87 )
88 }
89
90 return out
91 })
92
93 on('turn.start', async ($, e, next) => {
94 const id = e.agentId
95
96 if (id !== undefined) {
97 const now = await $.clock.now()
98 await update($, rows, list =>
99 patch(list, id, now, row => ({
100 status: 'running',
101 prompt: e.text === '' ? row.prompt : e.text,
102 })),
103 )
104 }
105
106 return next(e)
107 })
108
109 on('tool.call', async ($, e, next) => {
110 const id = e.agentId
111
112 if (id !== undefined) {
113 const now = await $.clock.now()
114 await update($, rows, list =>
115 patch(list, id, now, row => ({ toolCalls: row.toolCalls + 1, activity: e.tool })),
116 )
117 }
118
119 return next(e)
120 })
121
122 on('turn.step', async function* ($, e, next) {
123 const out = yield* next(e)
124 const id = e.agentId
125 const usage = out.usage
126
127 if (id !== undefined && usage !== null && usage !== undefined) {
128 const now = await $.clock.now()
129 const contextTokens =
130 usage.input_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens + usage.output_tokens
131 await update($, rows, list => patch(list, id, now, () => ({ contextTokens })))
132 }
133
134 return out
135 })
136
137 on('turn.complete', async ($, e, next) => {
138 const id = e.agentId
139
140 if (id !== undefined) {
141 const now = await $.clock.now()
142 const status = e.reason === 'answer' ? 'done' : e.reason === 'aborted' ? 'stopped' : 'failed'
143 await update($, rows, list =>
144 patch(list, id, now, () => ({ status, endedAt: now, activity: '', result: e.answer })),
145 )
146 }
147
148 return next(e)
149 })
150
151 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
152 const { Box, Text, Button } = $.ui.resolve(e)
153 const list = await read($, rows)
154 const now = await $.clock.now()
155 const usage = await $.session.usage()
156 const window = usage.context.window > 0 ? usage.context.window : await read($, windowSize)
157 const running = list.filter(row => row.status === 'running').length
158 const sorted = [...list].sort((a, b) =>
159 a.status === 'running' && b.status !== 'running'
160 ? -1
161 : b.status === 'running' && a.status !== 'running'
162 ? 1
163 : b.startedAt - a.startedAt,
164 )
165
166 if (list.length === 0) {
167 return (
168 <Box flexDirection="column">
169 <Text dimColor>No sub-agents yet.</Text>
170 <Text dimColor>They appear here as soon as an agent spawns one.</Text>
171 </Box>
172 )
173 }
174
175 return (
176 <Box flexDirection="column" gap={1}>
177 <Text bold>
178 {running} running, {list.length - running} finished
179 </Text>
180 {sorted.map(row => {
181 const elapsed = seconds((row.endedAt > 0 ? row.endedAt : now) - row.startedAt)
182 const toolFraction = row.status === 'done' ? 1 : row.toolCalls / TOOL_BAR_FULL
183 const contextFraction = window > 0 ? row.contextTokens / window : 0
184
185 return (
186 <Box key={row.id} flexDirection="column">
187 <Box gap={1}>
188 <Text color={COLORS[row.status]}>
189 {DOTS[row.status]} {row.status}
190 </Text>
191 <Text bold>{row.description === '' ? row.id : row.description}</Text>
192 <Text dimColor>
193 {row.type} {elapsed}
194 </Text>
195 </Box>
196 <Text color={COLORS[row.status]}>
197 work {bar(toolFraction)} {row.toolCalls} tools
198 </Text>
199 <Text>
200 ctx {bar(contextFraction)} {compact(row.contextTokens)}/{compact(window)} {Math.round(contextFraction * 100)}%
201 </Text>
202 {row.activity !== '' && <Text dimColor>now: {row.activity}</Text>}
203 <Button
204 key={`toggle-${row.id}`}
205 label={row.isOpen ? 'Hide prompt and result' : 'Show prompt and result'}
206 onPress={() =>
207 update($, rows, current => current.map(r => (r.id === row.id ? { ...r, isOpen: !r.isOpen } : r)))
208 }
209 />
210 {row.isOpen && (
211 <Box flexDirection="column">
212 <Text bold>Prompt</Text>
213 <Text dimColor>{clip(row.prompt) || '(none seen)'}</Text>
214 <Text bold>Result</Text>
215 <Text dimColor>{clip(row.result) || '(not finished)'}</Text>
216 </Box>
217 )}
218 </Box>
219 )
220 })}
221 </Box>
222 )
223 })
224}
225types/index.d.ts 21 lines1export type Row = {
2 id: string
3 description: string
4 type: string
5 status: 'running' | 'done' | 'failed' | 'stopped'
6 startedAt: number
7 endedAt: number
8 toolCalls: number
9 activity: string
10 contextTokens: number
11 prompt: string
12 result: string
13 isOpen: boolean
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'agent-dashboard': { rows: Row[]; window: number }
19 }
20}
21