Live map of the session's agent tree: who spawned whom and what each is doing

Seven mods for Claude Code: a pet that grows while Claude works, live panes for agents and files, a boss fight against failing tests, a chime for long turns, and a model router. They work in any project and any language.
| Mod | What it does | Where | Command |
|---|---|---|---|
| cute-feed | A pet that hatches, grows and reacts to what Claude does, over a live emoji feed | pane | /feed, /pat |
| dashboard | Project, git, session cost, context, rate limits, subagents and recent activity | pane | /dashboard |
| swarm | Live tree of subagents: who spawned whom, what each is running right now | pane | /swarm |
| repo-radar | Map of the repo that lights up as files are read, edited or fail | pane | /radar |
| boss-fight | Failing lint, tests or typechecks spawn a boss; each fix damages it | above the prompt | /boss |
| chime | Sound + notification when a long turn finishes or Claude waits on you | sound | /chime |
| model-router | Sends routine prompts and read-only subagents to a cheaper model | status line | /route |
Requires a Claude Code build with mods (function hooks). In a terminal session, install any mod by name:
/plugin install cute-feed --marketplace rabanderlipe/claude-code-mods
Answer y to add the marketplace, then pick a scope (user scope = every project). Repeat with dashboard, swarm, repo-radar, boss-fight, chime or model-router.
Panes open as tabs beside the transcript. One the mod opens on its own needs a terminal at least 144 columns wide; its slash command opens it at any width.
Every setting is a row in /config, or under pluginConfigs.<mod> in your settings.
cute-feed
autoOpen (true) — open the pane when a session startspetName ("Mochi"), species ("cat") — for a brand-new pet: cat, chick, dog, slime, bunny, hamster, penguin, frogdangerPatterns ("") — extra ;-separated regexes for commands that make the pet panic (it never blocks them)dashboard
autoOpen (false), refreshSeconds (5)swarm
autoOpen (true) — open when the first subagent startsrepo-radar
autoOpen (false), depth (2) — folder levels always shownboss-fight
enabledSuites ("lint,unit,int,db,e2e,typecheck")extraPatterns ("") — your own commands, suite=regex;suite=regex, e.g. unit=\bjust test\b;e2e=\bmake smoke\bRecognised out of the box: npm / pnpm / yarn / bun / make scripts (test, test:int, e2e, lint, typecheck, …), eslint, oxlint, biome, ruff, flake8, pylint, rubocop, golangci-lint, clippy, vitest, jest, mocha, ava, pytest, go test, cargo test, rspec, phpunit, dotnet test, mix test, pg_prove, playwright, cypress, tsc, vue-tsc, mypy, pyright, flow.
chime
thresholdSeconds (45), notify (true), waitingSound (true)model-router
lightModel ("claude-haiku-5-5"), standardModel ("claude-sonnet-5-5")maxMessages (40) — past this, switching models costs a prompt-cache rebuild, so it stops routinglightAgents ("Explore,claude-code-guide,statusline-setup")routeMainThread (true), routeSubagents (true)Prefix a prompt with !haiku, !sonnet or !opus to force a model for that turn.
/pat, /feed name <name>, /feed species <name>, /feed feed.claude --plugin-dir plugins/cute-feed # load one mod from disk, hot-reloaded on save
claude plugin validate plugins/cute-feed
claude plugin test plugins/cute-feed
Mods are not sandboxed: read the source before installing any mod, these included.
MIT
hooks/register.tsx 257 lines1import { atom, read, update } from "claude-code";
2import type { EngineInterface, Register, Timer } from "claude-code";
3
4import type { AgentNode } from "../types";
5import { buildTree, prune } from "./tree";
6
7const PANE = "swarm";
8const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
10const nodes = atom({ plugin: "swarm", key: "nodes" } as const, []);
11const main = atom({ plugin: "swarm", key: "main" } as const, {
12 model: "",
13 tools: 0,
14});
15const frame = atom({ plugin: "swarm", key: "frame" } as const, 0);
16const peak = atom({ plugin: "swarm", key: "peak" } as const, 0);
17
18const ICONS: Record<string, string> = {
19 Read: "📖",
20 Edit: "✏️",
21 Write: "📝",
22 Bash: "💻",
23 Grep: "🔍",
24 Glob: "🗂️",
25 Agent: "🐣",
26 WebFetch: "🌐",
27 WebSearch: "🔭",
28 Skill: "🎓",
29};
30
31let ticker: Timer | null = null;
32let isOpened = false;
33
34const short = (s: string, n: number) =>
35 s.length > n ? `${s.slice(0, n - 1)}…` : s;
36
37const shortModel = (model: string) => model.replace(/^claude-/, "");
38
39const ago = (ms: number) => {
40 const s = Math.max(0, Math.round(ms / 1000));
41 return s < 60
42 ? `${s}s`
43 : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
44};
45
46// What a call is doing, in a few words: icon, tool and its file, command or pattern.
47const activity = (tool: string, input: Record<string, unknown>) => {
48 const pick = (k: string) =>
49 typeof input[k] === "string" ? (input[k] as string) : undefined;
50 const path = pick("file_path") ?? pick("path");
51 const subject =
52 pick("command") ??
53 (path ? path.split("/").slice(-2).join("/") : undefined) ??
54 pick("pattern") ??
55 pick("description") ??
56 pick("url") ??
57 "";
58 const icon = ICONS[tool] ?? (tool.startsWith("mcp__") ? "🔌" : "🔧");
59 return short(`${icon} ${tool} ${subject}`.trim(), 40);
60};
61
62// Runs the spinner's timer while any agent runs, and stops it when none does.
63async function syncTicker($: EngineInterface): Promise<void> {
64 const running = (await read($, nodes)).filter(
65 (n) => n.status === "running",
66 ).length;
67 if (running > 0 && ticker === null) {
68 ticker = $.clock.every(
69 400,
70 () => void update($, frame, (n) => (n + 1) % SPIN.length),
71 );
72 } else if (running === 0 && ticker !== null) {
73 ticker.cancel();
74 ticker = null;
75 }
76}
77
78async function setCurrent(
79 $: EngineInterface,
80 agentId: string | undefined,
81 current: string | undefined,
82 toolDelta: number,
83): Promise<void> {
84 if (agentId === undefined) {
85 await update($, main, (m) => ({
86 ...m,
87 current,
88 tools: m.tools + toolDelta,
89 }));
90 return;
91 }
92 await update($, nodes, (list) =>
93 list.map((n) =>
94 n.id === agentId ? { ...n, current, tools: n.tools + toolDelta } : n,
95 ),
96 );
97}
98
99async function openPane($: EngineInterface): Promise<void> {
100 isOpened = true;
101 await $.ui.open({ id: PANE, title: "Swarm" });
102}
103
104export const register: Register = (on, options) => {
105 const isAutoOpen = options.autoOpen !== false;
106 on("session.start", async ($, e, next) => {
107 await $.command.register({
108 name: "swarm",
109 description: "Open the live map of this session’s agents",
110 });
111 const model = await $.session.model();
112 await update($, main, (m) => ({ ...m, model }));
113 await syncTicker($);
114 return next(e);
115 });
116
117 on("command.run", { command: "swarm" }, async ($) => {
118 await openPane($);
119 return { text: "Swarm map opened." };
120 });
121
122 on("agent.spawn", async ($, e, next) => {
123 const started = await next(e);
124 if (started.deny !== undefined || started.agentId === undefined)
125 return started;
126
127 const node: AgentNode = {
128 id: started.agentId,
129 parentId: e.parentAgentId,
130 type: e.subagentType,
131 label: e.name ?? e.description,
132 model: started.model,
133 startedAt: await $.clock.now(),
134 tools: 0,
135 status: "running",
136 runId: e.workflow?.runId,
137 agentIndex: e.workflow?.agentIndex,
138 teammate: started.teammateId,
139 };
140 const list = await update($, nodes, (all) => [...all, node].slice(-200));
141 const running = list.filter((n) => n.status === "running").length;
142 await update($, peak, (p) => Math.max(p, running));
143 await syncTicker($);
144 if (isAutoOpen && !isOpened) void openPane($).catch(() => undefined);
145
146 return started;
147 }).catch(($, e, next) => next(e));
148
149 on("tool.call", async ($, e, next) => {
150 await setCurrent(
151 $,
152 e.agentId,
153 activity(e.tool, e as unknown as Record<string, unknown>),
154 1,
155 );
156 const ran = await next(e);
157 await setCurrent($, e.agentId, undefined, 0);
158 return ran;
159 }).catch(($, e, next) => next(e));
160
161 on("turn.complete", async ($, e, next) => {
162 const id = e.agentId;
163 if (id !== undefined) {
164 const status = e.isAborted ? ("aborted" as const) : ("done" as const);
165 await update($, nodes, (list) =>
166 list.map((n) =>
167 n.id === id
168 ? { ...n, status, durationMs: e.durationMs, current: undefined }
169 : n,
170 ),
171 );
172 await syncTicker($);
173 } else {
174 await update($, main, (m) => ({ ...m, current: undefined }));
175 }
176 return next(e);
177 });
178
179 on("ui.render", { component: "Pane", requestId: PANE }, async ($, e) => {
180 const { Box, Text } = $.ui.resolve(e);
181 const [all, m, f, p, now] = await Promise.all([
182 read($, nodes),
183 read($, main),
184 read($, frame),
185 read($, peak),
186 $.clock.now(),
187 ]);
188 const { kept, hidden } = prune(all);
189 const rows = buildTree(kept, hidden);
190 const running = all.filter((n) => n.status === "running").length;
191 const done = all.length - running;
192 const spin = SPIN[f % SPIN.length];
193
194 return (
195 <Box flexDirection="column">
196 <Text>
197 <Text color="yellow">{running} running</Text> ·{" "}
198 <Text color="green">{done} done</Text> · {all.length} total
199 <Text dimColor> · peak {p}</Text>
200 </Text>
201 <Text>
202 <Text bold color="cyan">
203 ◆ main
204 </Text>{" "}
205 <Text dimColor>
206 {shortModel(m.model)} · {m.tools} tools
207 </Text>
208 {m.current ? <Text> · {m.current}</Text> : null}
209 </Text>
210 {rows.map((row) => {
211 if (row.kind === "more")
212 return (
213 <Text dimColor>
214 {row.prefix}+{row.count} earlier
215 </Text>
216 );
217 if (row.kind === "group") {
218 return (
219 <Text>
220 <Text dimColor>{row.prefix}</Text>
221 <Text color="magenta">⚙ workflow {row.runId.slice(0, 12)}</Text>
222 <Text dimColor> · {row.count} agents</Text>
223 </Text>
224 );
225 }
226 const n = row.node;
227 const isRunning = n.status === "running";
228 const glyph = isRunning ? spin : n.status === "done" ? "✓" : "✗";
229 const color = isRunning
230 ? "yellow"
231 : n.status === "done"
232 ? "green"
233 : "red";
234 const elapsed = isRunning ? now - n.startedAt : (n.durationMs ?? 0);
235 return (
236 <Text dimColor={!isRunning}>
237 <Text dimColor>{row.prefix}</Text>
238 <Text color={color}>{glyph} </Text>
239 {n.runId !== undefined && n.agentIndex !== undefined
240 ? `#${n.agentIndex} `
241 : ""}
242 <Text bold={isRunning}>{n.type}</Text> "{short(n.label, 32)}"
243 {n.teammate ? <Text color="cyan"> @{n.teammate}</Text> : null}
244 <Text dimColor>
245 {" "}
246 {shortModel(n.model)} · {ago(elapsed)} · {n.tools} tools
247 </Text>
248 {isRunning && n.current ? <Text> · {n.current}</Text> : null}
249 </Text>
250 );
251 })}
252 {rows.length === 0 && <Text dimColor>No subagents yet.</Text>}
253 </Box>
254 );
255 });
256};
257hooks/tree.ts 116 lines1import type { AgentNode } from "../types";
2
3export type Row =
4 | { kind: "agent"; prefix: string; node: AgentNode }
5 | { kind: "group"; prefix: string; runId: string; count: number }
6 | { kind: "more"; prefix: string; count: number };
7
8export const KEEP_FINISHED = 20;
9
10type Item =
11 | { kind: "agent"; node: AgentNode }
12 | { kind: "group"; runId: string; nodes: AgentNode[] };
13
14const hasRunning = (
15 node: AgentNode,
16 byParent: Map<string, AgentNode[]>,
17): boolean =>
18 node.status === "running" ||
19 (byParent.get(node.id) ?? []).some((c) => hasRunning(c, byParent));
20
21// Drops the oldest finished agents past `keep`, never one with a running descendant.
22export const prune = (nodes: readonly AgentNode[], keep = KEEP_FINISHED) => {
23 const byParent = childrenOf(nodes);
24 const finished = nodes.filter((n) => !hasRunning(n, byParent));
25 const dropped = new Set(
26 finished.slice(0, Math.max(0, finished.length - keep)).map((n) => n.id),
27 );
28 const kept = nodes
29 .filter((n) => !dropped.has(n.id))
30 // A kept agent whose parent was dropped hangs off main.
31 .map((n) =>
32 n.parentId && dropped.has(n.parentId) ? { ...n, parentId: undefined } : n,
33 );
34 return { kept, hidden: dropped.size };
35};
36
37const childrenOf = (nodes: readonly AgentNode[]) => {
38 const ids = new Set(nodes.map((n) => n.id));
39 const byParent = new Map<string, AgentNode[]>();
40 for (const n of nodes) {
41 const parent = n.parentId && ids.has(n.parentId) ? n.parentId : "main";
42 byParent.set(parent, [...(byParent.get(parent) ?? []), n]);
43 }
44 return byParent;
45};
46
47// Workflow agents of one run sit together under a group, in agentIndex order.
48const itemsOf = (children: readonly AgentNode[]): Item[] => {
49 const items: Item[] = [];
50 const groups = new Map<string, AgentNode[]>();
51 for (const n of children) {
52 if (n.runId === undefined) {
53 items.push({ kind: "agent", node: n });
54 continue;
55 }
56 const group = groups.get(n.runId);
57 if (group) group.push(n);
58 else {
59 const fresh = [n];
60 groups.set(n.runId, fresh);
61 items.push({ kind: "group", runId: n.runId, nodes: fresh });
62 }
63 }
64 for (const g of groups.values())
65 g.sort((a, b) => (a.agentIndex ?? 0) - (b.agentIndex ?? 0));
66 return items;
67};
68
69// The rows under main, depth first, each with its tree connectors.
70export const buildTree = (nodes: readonly AgentNode[], hidden = 0): Row[] => {
71 const byParent = childrenOf(nodes);
72 const rows: Row[] = [];
73
74 const walk = (parentId: string, indent: string, extra: Row | null) => {
75 const items = itemsOf(byParent.get(parentId) ?? []);
76 const total = items.length + (extra ? 1 : 0);
77 if (extra)
78 rows.push({
79 ...extra,
80 prefix: `${indent}${total === 1 ? "└─ " : "├─ "}`,
81 });
82 items.forEach((item, i) => {
83 const isLast = i + (extra ? 1 : 0) === total - 1;
84 const prefix = `${indent}${isLast ? "└─ " : "├─ "}`;
85 const childIndent = `${indent}${isLast ? " " : "│ "}`;
86 if (item.kind === "agent") {
87 rows.push({ kind: "agent", prefix, node: item.node });
88 walk(item.node.id, childIndent, null);
89 } else {
90 rows.push({
91 kind: "group",
92 prefix,
93 runId: item.runId,
94 count: item.nodes.length,
95 });
96 item.nodes.forEach((n, j) => {
97 const isLastInGroup = j === item.nodes.length - 1;
98 rows.push({
99 kind: "agent",
100 prefix: `${childIndent}${isLastInGroup ? "└─ " : "├─ "}`,
101 node: n,
102 });
103 walk(n.id, `${childIndent}${isLastInGroup ? " " : "│ "}`, null);
104 });
105 }
106 });
107 };
108
109 walk(
110 "main",
111 "",
112 hidden > 0 ? { kind: "more", prefix: "", count: hidden } : null,
113 );
114 return rows;
115};
116types/index.d.ts 31 lines1export type AgentStatus = "running" | "done" | "aborted";
2
3export type AgentNode = {
4 id: string;
5 parentId?: string;
6 type: string;
7 label: string;
8 model: string;
9 startedAt: number;
10 durationMs?: number;
11 tools: number;
12 status: AgentStatus;
13 current?: string;
14 runId?: string;
15 agentIndex?: number;
16 teammate?: string;
17};
18
19export type MainNode = { model: string; current?: string; tools: number };
20
21declare module "claude-code" {
22 interface PluginState {
23 swarm: {
24 nodes: AgentNode[];
25 main: MainNode;
26 frame: number;
27 peak: number;
28 };
29 }
30}
31