A pane listing the subagents launched this session with their status and duration

A pane listing the subagents launched during the session, with status and duration.
type: description.⏳ running, ✓ done (its turn ended with an answer) or ✗ failed (interrupted, API error, refusal...), with its duration (42s, 3m05s).| Command | Effect |
|---|---|
/agent-tracker | Open the pane |
/agent-tracker close | Close the pane |
/agent-tracker clear | Remove finished agents, keep running ones |
claude --plugin-dir /path/to/ModsTools/mods/agent-tracker
⏳.claude plugin validate mods/agent-tracker
claude plugin test mods/agent-tracker # 9 testshooks/register.tsx 85 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { finish, label, rowText } from './rows'
5
6const PANE = 'agent-tracker'
7const TITLE = 'Agents'
8const rows = atom({ plugin: 'agent-tracker', key: 'rows' } as const, [])
9
10export const register: Register = on => {
11 on('session.start', async ($, e, next) => {
12 await $.command.register({
13 name: 'agent-tracker',
14 description: 'Show the subagents launched this session (open | close | clear)',
15 })
16
17 return next(e)
18 })
19
20 on('command.run', { command: 'agent-tracker' }, async ($, e) => {
21 const arg = e.args.trim()
22 if (arg === 'close') {
23 await $.ui.close({ id: PANE })
24 return { text: 'Agents pane closed.' }
25 }
26 if (arg === 'clear') {
27 await update($, rows, list => list.filter(row => row.status === 'running'))
28 return { text: 'Finished agents cleared.' }
29 }
30 await $.ui.open({ id: PANE, title: TITLE })
31 const count = (await read($, rows)).length
32
33 return { text: `Agents pane opened (${count} agent${count === 1 ? '' : 's'}).` }
34 })
35
36 // agent.spawn resolves once the subagent started, foreground or background alike;
37 // its end is the subagent's own turn.complete (below).
38 on('agent.spawn', async ($, e, next) => {
39 const started = await next(e)
40 if (started.deny !== undefined || started.agentId === undefined || e.isTeammate) return started
41 const agentId = started.agentId
42 const startedAt = await $.clock.now()
43 await update($, rows, list => [
44 ...list,
45 { id: agentId, label: label(e.description, e.subagentType), status: 'running' as const, startedAt },
46 ])
47
48 return started
49 })
50
51 on('turn.complete', async ($, e, next) => {
52 const id = e.agentId
53 if (id !== undefined) {
54 const now = await $.clock.now()
55 await update($, rows, list => finish(list, id, e.reason, now))
56 }
57
58 return next(e)
59 })
60
61 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
62 const { Box, Text } = $.ui.resolve(e)
63 const list = await read($, rows)
64 const now = await $.clock.now()
65 const room = Math.max(1, (e.viewport?.rows ?? 24) - 4)
66 const running = list.filter(row => row.status === 'running').length
67
68 return (
69 <Box flexDirection="column">
70 <Text bold>
71 {list.length} agent{list.length === 1 ? '' : 's'}
72 <Text dimColor> · {running} running</Text>
73 </Text>
74 {list.length === 0 && <Text dimColor>No subagent launched yet.</Text>}
75 {list
76 .slice(-room)
77 .reverse()
78 .map(row => (
79 <Text key={`agent:${row.id}`}>{rowText(row, now)}</Text>
80 ))}
81 </Box>
82 )
83 })
84}
85hooks/rows.ts 23 lines1import type { AgentRow } from '../types'
2
3const ICONS = { running: '⏳', done: '✓', failed: '✗' } as const
4
5export function formatDuration(ms: number): string {
6 const total = Math.max(0, Math.round(ms / 1000))
7 if (total < 60) return `${total}s`
8
9 return `${Math.floor(total / 60)}m${String(total % 60).padStart(2, '0')}s`
10}
11
12export const label = (description: string, type: string) => (description.trim() === '' ? type : `${type}: ${description.trim()}`)
13
14export const rowText = (row: AgentRow, now: number) =>
15 `${ICONS[row.status]} ${row.label} · ${formatDuration((row.endedAt ?? now) - row.startedAt)}`
16
17// A turn that ended in anything but an answer (interrupt, API error, refusal) failed.
18export const statusOf = (reason: string): AgentRow['status'] => (reason === 'answer' ? 'done' : 'failed')
19
20export function finish(rows: AgentRow[], id: string, reason: string, now: number): AgentRow[] {
21 return rows.map(row => (row.id === id ? { ...row, status: statusOf(reason), endedAt: now } : row))
22}
23types/index.d.ts 14 lines1export type AgentRow = {
2 id: string
3 label: string
4 status: 'running' | 'done' | 'failed'
5 startedAt: number
6 endedAt?: number
7}
8
9declare module 'claude-code' {
10 interface PluginState {
11 'agent-tracker': { rows: AgentRow[] }
12 }
13}
14