/mission opens a live map of the main agent, its subagents and every tool call.

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 162 lines1// Mission Control: /mission opens a pane showing the current turn's main agent, its
2// subagents and every tool call they make, live. A band line sums it up while Claude works.
3// (Upstream's code-map view, with its Haiku diff summaries and headless Chrome, is stripped.)
4import { atom, read, update } from 'claude-code'
5import type { EngineInterface, Register } from 'claude-code'
6
7import type { MissionNode, XAgent } from '../types'
8import { label, lines, summary } from './tree'
9
10const PANE = 'mission-control'
11const MAX_NODES = 300
12// Tools that are Claude Code's own bookkeeping, not work: kept out of the tree.
13const INTERNAL = new Set(['SubagentHandback', 'ToolSearch', 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'])
14
15// Held by the host, so a hot reload keeps the picture.
16const nodes = atom({ plugin: 'mission-control', key: 'nodes' } as const, [] as MissionNode[])
17const turn = atom({ plugin: 'mission-control', key: 'turn' } as const, 0)
18const now = atom({ plugin: 'mission-control', key: 'now' } as const, 0)
19
20// xagent's external agents (Codex, Copilot, Antigravity), read-only: leaves under main.
21const external = atom({ plugin: 'xagent', key: 'agents' } as const, [] as XAgent[])
22
23// The tree's nodes plus xagent's agents. With no main node yet they stand at the root.
24async function everyone($: EngineInterface): Promise<MissionNode[]> {
25 const own = await read($, nodes)
26 const parent = own.some(n => n.id === 'main') ? 'main' : null
27 const ext = (await read($, external).catch(() => [] as XAgent[])).map(x => ({
28 id: `xagent-${x.id}`, parent, kind: 'agent' as const, label: `[${x.backend}] ${x.task}`, family: 'agent', status: x.status, start: x.startedAt, end: x.endedAt, detail: x.last,
29 }))
30 return [...own, ...ext]
31}
32
33// Under the cap, the oldest tool calls go first; main and the agents always stay.
34export function cap(list: MissionNode[]) {
35 if (list.length <= MAX_NODES) return list
36 const tools = list.filter(n => n.kind === 'tool')
37 const room = Math.max(0, MAX_NODES - (list.length - tools.length))
38 const keep = new Set(room === 0 ? [] : tools.slice(-room))
39 return list.filter(n => n.kind !== 'tool' || keep.has(n))
40}
41
42// The first `max` characters, cut at a word.
43function words(text: string, max: number) {
44 const t = text.replace(/\s+/g, ' ').trim()
45 if (t.length <= max) return t
46 const cut = t.lastIndexOf(' ', max)
47 return `${t.slice(0, cut > max * 0.5 ? cut : max)}…`
48}
49
50export const register: Register = on => {
51 on('session.start', async ($, e, next) => {
52 const r = await next(e)
53 await $.command.register({ name: 'mission', description: 'Open the live map of agents and tool calls', immediate: true }).catch(() => {})
54 $.clock.every(1000, () => {
55 void (async () => {
56 const busy = (await everyone($)).some(n => n.status === 'running')
57 if (busy) await update($, now, () => Date.now())
58 })().catch(() => {})
59 })
60 return r
61 })
62
63 on('turn.start', async ($, e, next) => {
64 const t = (await read($, turn)) + 1
65 await update($, turn, () => t)
66 const main: MissionNode = { id: 'main', parent: null, kind: 'main', label: `main · ${words(e.text, 50)}`, family: 'main', status: 'running', start: Date.now() }
67 // A background agent still running from the last turn carries over, with its open calls.
68 await update($, nodes, list => {
69 const agents = list.filter(n => n.kind === 'agent' && n.status === 'running').map(a => ({ ...a, parent: 'main' }))
70 const ids = new Set(agents.map(a => a.id))
71 const open = list.filter(n => n.kind === 'tool' && n.status === 'running' && ids.has(n.parent ?? ''))
72 return [main, ...agents, ...open]
73 })
74 return next(e)
75 })
76
77 on('agent.spawn', async ($, e, next) => {
78 const r = await next(e)
79 if (r.agentId) {
80 const id = r.agentId
81 const agent: MissionNode = { id, parent: e.parentAgentId ?? 'main', kind: 'agent', label: e.description || e.subagentType, family: 'agent', status: 'running', start: Date.now() }
82 await update($, nodes, list => cap([...list, agent]))
83 }
84 return r
85 })
86
87 on('tool.call', async ($, e, next) => {
88 if (e.tool === 'Agent') return next(e) // its subagent shows up through agent.spawn
89 if (INTERNAL.has(e.tool)) return next(e) // bookkeeping, not work a person would follow
90 const args = e as unknown as Record<string, unknown>
91 const { label: text, family } = label(args)
92 const id = e.tool_use_id
93 await update($, nodes, list => {
94 // A call from an agent this tree does not know hangs off main rather than nowhere.
95 const parent = e.agentId && list.some(n => n.id === e.agentId) ? e.agentId : 'main'
96 return cap([...list, { id, parent, kind: 'tool', label: text, family, status: 'running', start: Date.now() }])
97 })
98
99 let failed = true
100 let r: Awaited<ReturnType<typeof next>>
101 try {
102 r = await next(e)
103 failed = r.deny !== undefined || r.isError === true
104 } finally {
105 await update($, nodes, list => list.map(n => (n.id === id ? { ...n, status: failed ? 'failed' : 'done', end: Date.now() } : n)))
106 }
107 return r
108 })
109
110 on('turn.complete', async ($, e, next) => {
111 const r = await next(e)
112 const id = e.agentId ?? 'main'
113 const failed = e.reason === 'error' || e.reason === 'aborted'
114 const isMain = !e.agentId
115 // The main turn ending closes everything it left marked running, so the clock can rest.
116 await update($, nodes, list => {
117 // Only main's own leftovers: a background agent still running keeps its open calls.
118 const live = new Set(list.filter(n => n.kind === 'agent' && n.status === 'running' && n.id !== id).map(n => n.id))
119 const close = (n: MissionNode) => n.id === id || (isMain && n.kind === 'tool' && !live.has(n.parent ?? ''))
120 return list.map(n => (close(n) && n.status === 'running' ? { ...n, status: failed ? 'failed' : 'done', end: Date.now() } : n))
121 })
122 return r
123 })
124
125 on('command.run', { command: 'mission' }, async $ => {
126 await $.ui.open({ id: PANE, title: 'Mission Control', focus: true })
127 const s = summary(await everyone($))
128 return { text: `Mission Control: ${s.agents} agents · ${s.tools} tool calls · q: close` }
129 })
130
131 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
132 const { Box, Text, Button } = $.ui.resolve(e)
133 await read($, now) // subscribes the pane to the clock
134 const rows = Math.max(6, (e.props.scroll?.bodyRows ?? 30) - 3)
135 const head = (
136 <Box flexDirection="row" gap={1}>
137 <Button key="close" label="Close" hotkey="q" role="dismiss" onPress={() => $.ui.close({ id: PANE })} />
138 </Box>
139 )
140
141 const at = Date.now()
142 const all = await everyone($)
143 const ls = lines(all, at)
144 const s = summary(all)
145 return (
146 <Box flexDirection="column">
147 {head}
148 <Text dimColor>{`${s.agents} agents (${s.running} running) · ${s.tools} tool calls${s.failed ? ` · ${s.failed} failed` : ''}`}</Text>
149 {ls.length === 0 && <Text dimColor>Nothing yet. Start a task and watch it fill in.</Text>}
150 {ls.slice(-rows + 1).map(l => (
151 <Text wrap="truncate-end">
152 <Text dimColor>{l.prefix}</Text>
153 <Text color={l.status === 'running' ? 'yellow' : l.status === 'failed' ? 'red' : l.node.kind === 'tool' ? undefined : 'green'} bold={l.node.kind !== 'tool'} dimColor={l.status === 'done' && l.node.kind === 'tool'}>
154 {l.text}
155 </Text>
156 </Text>
157 ))}
158 </Box>
159 )
160 })
161}
162hooks/tree.ts 81 lines1// The "who" view: main, its subagents and their tool calls, as tree lines.
2import type { MissionNode } from '../types'
3
4// A short label and a family for one tool call.
5export function label(e: Record<string, unknown>): { label: string; family: string } {
6 const tool = String(e.tool)
7 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
8 const file = (p: string) => p.split('/').slice(-2).join('/')
9 if (tool === 'Bash') return { label: s('description') || s('command').slice(0, 60), family: 'bash' }
10 if (tool === 'Edit' || tool === 'Write') return { label: file(s('file_path')), family: 'edit' }
11 if (tool === 'Read') return { label: file(s('file_path')), family: 'read' }
12 if (tool === 'Grep' || tool === 'Glob') return { label: s('pattern').slice(0, 40), family: 'search' }
13 if (tool.startsWith('mcp__')) {
14 const [, server = '', action = ''] = tool.split('__')
15 if (/playwright/.test(server)) return { label: action.replace(/^browser_/, ''), family: 'browser' }
16 return { label: action, family: `mcp:${server.replace(/^(plugin_|claude_ai_)/, '').replace(/_.*$/, '')}` }
17 }
18 return { label: tool, family: tool.toLowerCase() }
19}
20
21const ICON: Record<string, string> = { bash: '$', edit: '✎', read: '◉', search: '⌕', browser: '◎' }
22export const icon = (n: MissionNode) =>
23 n.kind === 'main' ? '◆' : n.kind === 'agent' ? '●' : (ICON[n.family] ?? (n.family.startsWith('mcp:') ? '⌁' : '·'))
24
25export function took(n: Pick<MissionNode, 'start' | 'end'>, at = Date.now()) {
26 const s = Math.max(0, ((n.end ?? at) - n.start) / 1000)
27 if (s < 10) return `${s.toFixed(1)}s`
28 if (s < 60) return `${Math.round(s)}s`
29 return `${Math.floor(s / 60)}m ${String(Math.round(s % 60)).padStart(2, '0')}s`
30}
31
32export type Line = { prefix: string; node: MissionNode; text: string; count: number; status: MissionNode['status'] }
33
34// Tree lines, depth first. Runs of the same tool family under one parent fold into one
35// line ("◎ browser: navigate → click → screenshot ×3") so a busy agent stays readable.
36export function lines(nodes: MissionNode[], at = Date.now()): Line[] {
37 const kids = new Map<string | null, MissionNode[]>()
38 for (const n of nodes) kids.set(n.parent, [...(kids.get(n.parent) ?? []), n])
39 const out: Line[] = []
40 const walk = (parent: string | null, prefix: string) => {
41 const groups: MissionNode[][] = []
42 for (const n of kids.get(parent) ?? []) {
43 const last = groups.at(-1)
44 const head = last?.[0]
45 if (last && head && n.kind === 'tool' && head.kind === 'tool' && head.family === n.family) last.push(n)
46 else groups.push([n])
47 }
48 groups.forEach((g, i) => {
49 const isLast = i === groups.length - 1
50 const n = g[0] as MissionNode
51 const branch = parent === null ? '' : isLast ? '└─ ' : '├─ '
52 const status = g.some(x => x.status === 'running') ? 'running' : g.some(x => x.status === 'failed') ? 'failed' : 'done'
53 const start = Math.min(...g.map(x => x.start))
54 const end = status === 'running' ? undefined : Math.max(...g.map(x => x.end ?? x.start))
55 let text: string
56 if (n.kind === 'tool') {
57 const name = n.family.startsWith('mcp:') ? n.family.slice(4) : n.family
58 const acts = g.map(x => x.label)
59 const shown = acts.length > 4 ? [...acts.slice(0, 3), '…', acts.at(-1)] : acts
60 const time = took({ start, end }, at)
61 text = `${icon(n)} ${name}: ${shown.join(' → ')}${g.length > 1 ? ` ×${g.length}` : ''}${time === '0.0s' ? '' : ` ${time}`}`
62 } else {
63 const tools = nodes.filter(x => x.parent === n.id && x.kind === 'tool').length
64 text = `${icon(n)} ${n.label} ${took(n, at)}${n.kind === 'agent' ? ` · ${n.detail ?? `${tools} tools`}` : ''}`
65 }
66 out.push({ prefix: prefix + branch, node: n, text, count: g.length, status })
67 if (n.kind !== 'tool') walk(n.id, parent === null ? '' : prefix + (isLast ? ' ' : '│ '))
68 })
69 }
70 walk(null, '')
71 return out
72}
73
74export function summary(nodes: MissionNode[]) {
75 const agents = nodes.filter(n => n.kind === 'agent')
76 const running = agents.filter(n => n.status === 'running').length
77 const tools = nodes.filter(n => n.kind === 'tool').length
78 const failed = nodes.filter(n => n.kind === 'tool' && n.status === 'failed').length
79 return { agents: agents.length, running, tools, failed }
80}
81types/index.d.ts 38 lines1// One box in the "who" tree: the main loop, a subagent, or one tool call.
2export type MissionNode = {
3 id: string
4 parent: string | null
5 kind: 'main' | 'agent' | 'tool'
6 label: string
7 family: string // for tools: 'bash', 'edit', 'read', 'browser', 'mcp:<server>', ...
8 status: 'running' | 'done' | 'failed'
9 start: number
10 end?: number
11 detail?: string // what an external agent is doing now (xagent's `last`)
12}
13
14declare module 'claude-code' {
15 interface PluginState {
16 'mission-control': {
17 nodes: MissionNode[]
18 turn: number
19 now: number
20 }
21 xagent: { agents: XAgent[] }
22 }
23}
24
25// xagent's agents, as xagent writes them (mods/xagent/types: XAgent). Read-only here.
26export type XAgent = {
27 id: string
28 backend: 'codex' | 'copilot' | 'agy'
29 model: string
30 effort: string
31 task: string
32 status: 'running' | 'done' | 'failed'
33 startedAt: number
34 endedAt?: number
35 last: string
36 quota: string
37}
38