One live line above the prompt per running subagent: time, tool count and what it's doing. /radar shows every agent and its messages.

My Claude Code setup: mods, plugins, skills and settings, so a new machine is one clone away.
git clone git@github-personal:aidan-nunn-sys/claude-config.git ~/Work/personal/claude-config
~/Work/personal/claude-config/install.sh
Then:
<SET-ME> values install.sh reports in ~/.claude.json (MCP secrets are never committed).~/.config/mise/config.toml: "npm:optimaizr" = { version = "0.11.0", allow_low_downloads = true }Mods and skills are symlinked from this repo, so edits land here directly. Settings changed through Claude Code live in ~/.claude; run ./capture.sh and review git diff to bring them back.
| Path | What |
|---|---|
mods/ | Mods loaded through CLAUDE_CODE_PLUGIN_DIRS (built by install.sh from this folder) |
plugins/plugins.json | Marketplaces and which marketplace plugins are enabled |
skills/ | Personal skills, symlinked into ~/.claude/skills |
settings/settings.json | ~/.claude/settings.json minus plugin dirs, plugins and hooks; ${HOME} for paths |
settings/mcp.json | User MCP servers, secrets blanked |
settings/rulebook.json | Generic example rules (copied only when the machine has none; not captured) |
docs/specs/ | Design specs |
| Mod | Source | Local changes |
|---|---|---|
| hud | own | prompt hint, context weather bar, cache countdown, kaomoji mascot |
| rulebook | own | hard rules in ~/.claude/rulebook.json, /rules, mcp__rulebook__add_rule |
| xagent | own | dispatch/route tools for Codex, Copilot, Antigravity |
| handoff | own | /handoff [next] writes a handoff doc to ~/.claude/handoffs/ |
| agent-radar | hamzafer/claude-code-mods @ 87f108479bbf | /radar runs mid-turn (immediate) and toggles closed; shows xagent's external agents |
| mission-control | hamzafer/claude-code-mods @ 87f108479bbf | shows xagent's external agents; code-map view (Haiku diff summaries, headless Chrome) stripped |
| blast-radius | hamzafer/claude-code-mods @ 87f108479bbf | 60 s auto-cancel |
| review-watch | hamzafer/claude-code-mods @ 87f108479bbf | none |
| where-am-i | hamzafer/claude-code-mods @ 87f108479bbf | shows only "next" |
| secret-box | ray-amjad/claude-code-mods @ 467b20300d8e | none |
| task-board | MarcoCarnevali/claude-code-mods @ a3e528dd00ce | /board (not /tasks), no auto-open, mid-turn + toggle |
Upstream commits are the repos' HEAD on 2026-10-14, when this repo was made; installs were from around then. Read upstream changes before pulling them in.
~/.claude/.credentials.json and MCP secrets.diagnose-crash, omarchy, symlinks into /usr/share/omarchy), themes/omarchy.json, the herdr SessionStart hook..claude-plugin/types/ in each mod: SDK types Claude Code writes itself.hooks/register.tsx 220 lines1// Agent Radar: what are my subagents cooking?
2// The band above the prompt has one live line per running subagent: status, time, tool
3// count and what it is doing right now; a finished one shows a check for 30 s. A toast
4// says when each finishes. /radar opens a pane with every agent and its messages.
5import { atom, read, update } from 'claude-code'
6import type { EngineInterface, Register } from 'claude-code'
7
8import type { RadarAgent, XAgent } from '../types'
9
10const PANE = 'agent-radar'
11const SHOW_DONE_MS = 30_000 // a finished agent stays in the band this long
12const BAND_ROWS = 4
13
14// Held by the host, so the radar survives a hot reload of this file.
15const agents = atom({ plugin: 'agent-radar', key: 'agents' } as const, [] as RadarAgent[])
16const selected = atom({ plugin: 'agent-radar', key: 'selected' } as const, null as string | null)
17const now = atom({ plugin: 'agent-radar', key: 'now' } as const, 0)
18
19// xagent's external agents (Codex, Copilot, Antigravity), read-only: shown like subagents.
20const external = atom({ plugin: 'xagent', key: 'agents' } as const, [] as XAgent[])
21
22async function everyone($: EngineInterface): Promise<RadarAgent[]> {
23 const own = await read($, agents)
24 const ext = (await read($, external).catch(() => [] as XAgent[])).map(x => ({
25 id: x.id, description: `[${x.backend}] ${x.task}`, type: x.backend, status: x.status, startedAt: x.startedAt, endedAt: x.endedAt, tools: 0, last: x.last,
26 }))
27 return [...ext, ...own]
28}
29
30export const register: Register = on => {
31 on('session.start', async ($, e, next) => {
32 const r = await next(e)
33 await $.command.register({ name: 'radar', description: 'Show what each subagent is doing, in the Agent Radar pane', immediate: true }).catch(() => {}) // a name Claude Code already has is refused: start anyway
34 // A clock for the elapsed times, ticking only while something runs.
35 $.clock.every(1000, () => {
36 void (async () => {
37 // A few seconds past the 30 s too, so the redraw that drops a finished line happens.
38 if ((await everyone($)).some(a => isShown(a, Date.now() - 3_000))) await update($, now, () => Date.now())
39 })().catch(() => {})
40 })
41 return r
42 })
43
44 on('agent.spawn', async ($, e, next) => {
45 const r = await next(e)
46 if (r.agentId) {
47 const id = r.agentId
48 const one: RadarAgent = { id, description: e.description || e.name || e.subagentType, type: e.subagentType, status: 'running', startedAt: Date.now(), tools: 0, last: 'starting' }
49 await update($, agents, list => [one, ...list.filter(a => a.id !== id)].slice(0, 30))
50 }
51 return r
52 })
53
54 on('tool.call', async ($, e, next) => {
55 if (e.agentId) {
56 const id = e.agentId
57 const line = describe(e as unknown as Record<string, unknown>)
58 await update($, agents, list => list.map(a => (a.id === id ? { ...a, tools: a.tools + 1, last: line } : a)))
59 }
60 return next(e)
61 })
62
63 on('turn.complete', async ($, e, next) => {
64 const r = await next(e)
65 if (e.agentId) {
66 const id = e.agentId
67 const failed = e.reason === 'error' || e.reason === 'aborted'
68 const list = await update($, agents, all =>
69 all.map(a => (a.id === id ? { ...a, status: failed ? 'failed' : 'done', endedAt: Date.now(), last: failed ? `stopped: ${e.reason}` : 'finished' } : a)),
70 )
71 const one = list.find(a => a.id === id)
72 if (one) $.ui.toast(`${failed ? '✗' : '✓'} ${one.description} ${failed ? 'failed' : 'done'} (${elapsed(one)} · ${one.tools} tools)`)
73 }
74 return r
75 })
76
77 on('command.run', { command: 'radar' }, async $ => {
78 // Run again to close.
79 if ((await $.ui.panes()).some(p => p.id === PANE)) {
80 await $.ui.close({ id: PANE })
81 return { text: 'Agent Radar closed.' }
82 }
83 await adopt($)
84 await update($, selected, () => null)
85 await $.ui.open({ id: PANE, title: 'Agent Radar', focus: true })
86 const list = await everyone($)
87 const running = list.filter(a => a.status === 'running').length
88 return { text: `${running} running, ${list.length - running} finished` }
89 })
90
91 // The band: one live line per running agent, and a ✓ for 30 s once one finishes.
92 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
93 const rest = await next(e) // what other mods and Claude Code draw here stays
94 await read($, now) // subscribes the band to the clock
95 const shown = (await everyone($)).filter(a => isShown(a))
96 if (e.props.hasSurvey || shown.length === 0) return rest
97 const { Box, Text } = $.ui.resolve(e)
98 return (
99 <Box flexDirection="column">
100 {shown.slice(0, BAND_ROWS).map(a => (
101 <Text wrap="truncate-end">
102 <Text color={color(a)} bold>{` ${icon(a)} ${a.description}`}</Text>
103 <Text dimColor>{` ${elapsed(a)} · ${a.tools} tools`}</Text>
104 {a.status === 'running' && <Text>{` · ${a.last}`}</Text>}
105 {a.status === 'failed' && <Text color="red">{` · ${a.last}`}</Text>}
106 </Text>
107 ))}
108 {shown.length > BAND_ROWS && <Text dimColor>{` +${shown.length - BAND_ROWS} more · /radar`}</Text>}
109 {rest}
110 </Box>
111 )
112 })
113
114 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
115 const { Box, Text, Button } = $.ui.resolve(e)
116 await read($, now) // subscribes the pane to the clock
117 const list = await everyone($)
118 const pick = await read($, selected)
119 const one = list.find(a => a.id === pick)
120
121 if (one) {
122 const found = await $.session.messages({ agentId: one.id })
123 const messages = 'deny' in found ? [] : found.slice(-6)
124 return (
125 <Box flexDirection="column">
126 <Text>
127 <Text color={color(one)} bold>{`${icon(one)} ${one.description}`}</Text>
128 <Text dimColor>{` ${one.type} · ${elapsed(one)} · ${one.tools} tools`}</Text>
129 </Text>
130 <Text dimColor wrap="truncate-end">{`now: ${one.last}`}</Text>
131 <Text dimColor>{'─'.repeat(Math.max(10, e.props.bodyColumns - 2))}</Text>
132 {messages.length === 0 && <Text dimColor>No messages yet.</Text>}
133 {messages.map(m => (
134 <Text wrap="truncate-end">
135 <Text color={m.role === 'assistant' ? 'cyan' : undefined} bold>{m.role === 'assistant' ? 'agent: ' : 'task: '}</Text>
136 <Text>{m.text.replace(/\s+/g, ' ').slice(0, 400) || '(tool calls)'}</Text>
137 </Text>
138 ))}
139 <Box flexDirection="row" gap={1}>
140 <Button key="back" label="Back" hotkey="b" variant="primary" onPress={() => update($, selected, () => null)} />
141 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
142 </Box>
143 </Box>
144 )
145 }
146
147 const running = list.filter(a => a.status === 'running').length
148 return (
149 <Box flexDirection="column">
150 <Text bold>{`${running} running · ${list.length - running} finished`}</Text>
151 {list.length === 0 && <Text dimColor>No subagents yet this session.</Text>}
152 {list.slice(0, 9).map((a, i) => (
153 <Button
154 key={`agent-${a.id}`}
155 plain
156 hotkey={String(i + 1)}
157 dimColor={a.status !== 'running'}
158 label={`${icon(a)} ${a.description} · ${elapsed(a)} · ${a.tools} tools${a.status === 'done' ? '' : ` · ${a.last}`}`}
159 onPress={() => update($, selected, () => a.id)}
160 />
161 ))}
162 {list.length > 9 && <Text dimColor>{`… ${list.length - 9} older`}</Text>}
163 <Box flexDirection="row" gap={1}>
164 <Button key="clear" label="Clear finished" hotkey="c" onPress={() => update($, agents, all => all.filter(x => x.status === 'running'))} />
165 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
166 </Box>
167 </Box>
168 )
169 })
170}
171
172// Agents that started before this mod loaded: take them from the engine's own list.
173async function adopt($: EngineInterface) {
174 const known = new Set((await read($, agents)).map(a => a.id))
175 const fresh = (await $.agent.list())
176 .filter(a => !known.has(a.id))
177 .map((a): RadarAgent => ({
178 id: a.id,
179 description: a.description,
180 type: a.type,
181 status: a.status === 'running' ? 'running' : a.status === 'completed' ? 'done' : 'failed',
182 startedAt: Date.now(),
183 tools: 0,
184 last: a.status === 'running' ? 'running (started before the radar)' : a.status,
185 }))
186 if (fresh.length > 0) await update($, agents, list => [...list, ...fresh])
187}
188
189// Running, or finished in the last 30 s.
190export function isShown(a: RadarAgent, at = Date.now()) {
191 return a.status === 'running' || (a.endedAt !== undefined && at - a.endedAt < SHOW_DONE_MS)
192}
193
194function icon(a: RadarAgent) {
195 return a.status === 'running' ? '●' : a.status === 'done' ? '✓' : '✗'
196}
197
198function color(a: RadarAgent) {
199 return a.status === 'running' ? 'yellow' : a.status === 'done' ? 'green' : 'red'
200}
201
202export function elapsed(a: Pick<RadarAgent, 'startedAt' | 'endedAt'>, at = Date.now()) {
203 const s = Math.max(0, Math.round(((a.endedAt ?? at) - a.startedAt) / 1000))
204 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
205}
206
207// A short label for one tool call: what a person would say the agent is doing.
208export function describe(e: Record<string, unknown>): string {
209 const tool = String(e.tool)
210 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
211 const file = (p: string) => p.split('/').slice(-2).join('/')
212 if (tool === 'Bash') return `running ${s('description') || s('command').slice(0, 50)}`
213 if (tool === 'Edit' || tool === 'Write') return `editing ${file(s('file_path'))}`
214 if (tool === 'Read') return `reading ${file(s('file_path'))}`
215 if (tool === 'Grep' || tool === 'Glob') return `searching ${s('pattern').slice(0, 30)}`
216 if (tool.includes('playwright')) return `browser: ${tool.split('__').pop()?.replace(/^browser_/, '')}`
217 if (tool.startsWith('mcp__')) return `using ${tool.split('__').slice(1).join(' ')}`
218 return `using ${tool}`
219}
220types/index.d.ts 32 lines1export type RadarAgent = {
2 id: string
3 description: string
4 type: string
5 status: 'running' | 'done' | 'failed'
6 startedAt: number
7 endedAt?: number
8 tools: number
9 last: string
10}
11
12declare module 'claude-code' {
13 interface PluginState {
14 'agent-radar': { agents: RadarAgent[]; selected: string | null; now: number }
15 xagent: { agents: XAgent[] }
16 }
17}
18
19// xagent's agents, as xagent writes them (mods/xagent/types: XAgent). Read-only here.
20export type XAgent = {
21 id: string
22 backend: 'codex' | 'copilot' | 'agy'
23 model: string
24 effort: string
25 task: string
26 status: 'running' | 'done' | 'failed'
27 startedAt: number
28 endedAt?: number
29 last: string
30 quota: string
31}
32