Live dashboard pane: subagents, activity, project, git and session usage

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 293 lines1import { atom, read, update } from "claude-code";
2import type { EngineInterface, Register } from "claude-code";
3
4import type { Activity, AgentRun, Project, Usage } from "../types";
5
6const PANE = "dashboard";
7
8const agents = atom({ plugin: "dashboard", key: "agents" } as const, []);
9const activity = atom({ plugin: "dashboard", key: "activity" } as const, []);
10const project = atom({ plugin: "dashboard", key: "project" } as const, null);
11const usage = atom({ plugin: "dashboard", key: "usage" } as const, null);
12
13const git = async ($: EngineInterface, cwd: string, ...args: string[]) => {
14 const ran = await $.process.run(["git", ...args], { cwd });
15 return ran.exitCode === 0 ? ran.stdout.trim() : "";
16};
17
18const loadProject = async ($: EngineInterface): Promise<Project | null> => {
19 const cwd = await $.session.cwd();
20 const root = await git($, cwd, "rev-parse", "--show-toplevel").catch(
21 () => "",
22 );
23 if (root === "") return null;
24
25 const [branch, status, counts, lastCommit, worktrees] = await Promise.all([
26 git($, cwd, "rev-parse", "--abbrev-ref", "HEAD"),
27 git($, cwd, "status", "--porcelain"),
28 git($, cwd, "rev-list", "--left-right", "--count", "@{upstream}...HEAD"),
29 git($, cwd, "log", "-1", "--format=%h %s (%cr)"),
30 git($, cwd, "worktree", "list", "--porcelain"),
31 ]);
32 const [behind, ahead] = counts.split(/\s+/).map(Number);
33 const hasUpstream =
34 counts !== "" && Number.isFinite(behind) && Number.isFinite(ahead);
35
36 return {
37 name: root.split("/").pop() ?? root,
38 branch,
39 dirty: status === "" ? 0 : status.split("\n").length,
40 hasUpstream,
41 ahead: hasUpstream ? (ahead ?? 0) : 0,
42 behind: hasUpstream ? (behind ?? 0) : 0,
43 lastCommit,
44 worktrees: worktrees.split("\n").filter((l) => l.startsWith("worktree "))
45 .length,
46 };
47};
48
49const loadUsage = async ($: EngineInterface): Promise<Usage> => {
50 const [u, model, turns, now] = await Promise.all([
51 $.session.usage(),
52 $.session.model(),
53 $.session.turns(),
54 $.clock.now(),
55 ]);
56 return {
57 model,
58 startedAt: u.startedAt,
59 contextPercent: u.context.percent,
60 costUsd: u.cost?.usd,
61 limits: u.rateLimits.map((r) => ({
62 kind: r.kind,
63 percentUsed: r.percentUsed,
64 })),
65 turns,
66 now,
67 };
68};
69
70const refresh = async ($: EngineInterface) => {
71 const [p, u] = await Promise.all([loadProject($), loadUsage($)]);
72 await update($, project, () => p);
73 await update($, usage, () => u);
74};
75
76const ago = (ms: number) => {
77 const s = Math.max(0, Math.round(ms / 1000));
78 if (s < 60) return `${s}s`;
79 const m = Math.floor(s / 60);
80 return m < 60
81 ? `${m}m${String(s % 60).padStart(2, "0")}s`
82 : `${Math.floor(m / 60)}h${m % 60}m`;
83};
84
85const bar = (percent: number, width = 20) => {
86 const filled = Math.round(
87 (Math.min(100, Math.max(0, percent)) / 100) * width,
88 );
89 return "█".repeat(filled) + "░".repeat(width - filled);
90};
91
92const shortModel = (model: string) => model.replace(/^claude-/, "");
93
94export const register: Register = (on, options) => {
95 const isAutoOpen = options.autoOpen === true;
96 const refreshMs = Math.max(1, Number(options.refreshSeconds) || 5) * 1000;
97
98 on("session.start", async ($, e, next) => {
99 await $.command.register({
100 name: "dashboard",
101 description: "Open the agent & project dashboard pane",
102 });
103 void refresh($).catch(() => {});
104 $.clock.every(refreshMs, () => void refresh($).catch(() => {}));
105 if (isAutoOpen) void $.ui.open({ id: PANE, title: "Dashboard" }).catch(() => {});
106
107 return next(e);
108 });
109
110 on("command.run", { command: "dashboard" }, async ($) => {
111 await refresh($);
112 await $.ui.open({ id: PANE, title: "Dashboard" });
113
114 return { text: "Dashboard opened." };
115 });
116
117 on("agent.spawn", async ($, e, next) => {
118 const started = await next(e);
119 if (started.deny !== undefined || started.agentId === undefined)
120 return started;
121
122 const run: AgentRun = {
123 id: started.agentId,
124 type: e.subagentType,
125 description: e.name ?? e.description,
126 model: started.model,
127 startedAt: await $.clock.now(),
128 tools: 0,
129 status: "running",
130 };
131 await update($, agents, (list) => [...list, run].slice(-30));
132
133 return started;
134 }).catch(($, e, next) => next(e));
135
136 on("turn.complete", async ($, e, next) => {
137 if (e.agentId !== undefined) {
138 const id = e.agentId;
139 await update($, agents, (list) =>
140 list.map((a) =>
141 a.id === id
142 ? {
143 ...a,
144 status: e.isAborted ? ("aborted" as const) : ("done" as const),
145 durationMs: e.durationMs,
146 }
147 : a,
148 ),
149 );
150 } else {
151 void refresh($).catch(() => {});
152 }
153
154 return next(e);
155 });
156
157 on("tool.call", async ($, e, next) => {
158 const id = e.tool_use_id ?? `${e.tool}-${await $.clock.now()}`;
159 const agentId = e.agentId;
160 const label =
161 agentId === undefined
162 ? "main"
163 : ((await read($, agents)).find((a) => a.id === agentId)?.type ??
164 "agent");
165 const item: Activity = {
166 id,
167 at: await $.clock.now(),
168 tool: e.tool,
169 agent: label,
170 isDone: false,
171 };
172
173 await update($, activity, (list) => [...list, item].slice(-50));
174 if (agentId !== undefined) {
175 await update($, agents, (list) =>
176 list.map((a) => (a.id === agentId ? { ...a, tools: a.tools + 1 } : a)),
177 );
178 }
179
180 const ran = await next(e);
181 await update($, activity, (list) =>
182 list.map((a) => (a.id === id ? { ...a, isDone: true } : a)),
183 );
184
185 return ran;
186 }).catch(($, e, next) => next(e));
187
188 on("ui.render", { component: "Pane", requestId: PANE }, async ($, e) => {
189 const { Box, Text } = $.ui.resolve(e);
190 const [runs, recent, p, u] = await Promise.all([
191 read($, agents),
192 read($, activity),
193 read($, project),
194 read($, usage),
195 ]);
196 const now = u?.now ?? (await $.clock.now());
197 const rows = e.viewport?.rows ?? 30;
198 const running = runs.filter((a) => a.status === "running");
199 const finished = runs.filter((a) => a.status !== "running").slice(-4);
200 const room = Math.max(3, rows - 22 - running.length - finished.length);
201
202 return (
203 <Box flexDirection="column" gap={1}>
204 <Box flexDirection="column">
205 <Text bold>
206 {p ? (
207 <>
208 {p.name} <Text color="cyan">⎇ {p.branch}</Text>
209 </>
210 ) : (
211 "📁 not a git repo"
212 )}
213 </Text>
214 {p && (
215 <Text dimColor>
216 {p.dirty} changed ·{" "}
217 {p.hasUpstream ? `↑${p.ahead} ↓${p.behind}` : "no upstream"} ·{" "}
218 {p.worktrees}{" "}
219 worktree{p.worktrees === 1 ? "" : "s"}
220 </Text>
221 )}
222 {p?.lastCommit && <Text dimColor>{p.lastCommit}</Text>}
223 </Box>
224
225 {u && (
226 <Box flexDirection="column">
227 <Text bold>Session</Text>
228 <Text>
229 {shortModel(u.model)} · {u.turns} turns · up{" "}
230 {ago(now - u.startedAt)}
231 {u.costUsd !== undefined ? ` · $${u.costUsd.toFixed(2)}` : ""}
232 </Text>
233 {u.contextPercent !== undefined && (
234 <Text>
235 ctx{" "}
236 <Text
237 color={
238 u.contextPercent > 80
239 ? "red"
240 : u.contextPercent > 60
241 ? "yellow"
242 : "green"
243 }
244 >
245 {bar(u.contextPercent)}
246 </Text>{" "}
247 {Math.round(u.contextPercent)}%
248 </Text>
249 )}
250 {u.limits.map((l) => (
251 <Text dimColor>
252 {l.kind.padEnd(8).slice(0, 8)} {bar(l.percentUsed, 12)}{" "}
253 {Math.round(l.percentUsed)}%
254 </Text>
255 ))}
256 </Box>
257 )}
258
259 <Box flexDirection="column">
260 <Text bold>
261 Agents <Text dimColor>({running.length} running)</Text>
262 </Text>
263 {runs.length === 0 && <Text dimColor>No subagents yet.</Text>}
264 {running.map((a) => (
265 <Text>
266 <Text color="yellow">● </Text>
267 {a.type} <Text dimColor>{a.description}</Text> · {a.tools} tools ·{" "}
268 {ago(now - a.startedAt)}{" "}
269 <Text dimColor>{shortModel(a.model)}</Text>
270 </Text>
271 ))}
272 {finished.map((a) => (
273 <Text dimColor>
274 {a.status === "done" ? "✓" : "✗"} {a.type} {a.description} ·{" "}
275 {a.tools} tools · {ago(a.durationMs ?? 0)}
276 </Text>
277 ))}
278 </Box>
279
280 <Box flexDirection="column">
281 <Text bold>Activity</Text>
282 {recent.length === 0 && <Text dimColor>No tool calls yet.</Text>}
283 {recent.slice(-room).map((a) => (
284 <Text dimColor={a.isDone}>
285 {a.isDone ? " " : "›"} {a.tool} <Text dimColor>[{a.agent}]</Text>
286 </Text>
287 ))}
288 </Box>
289 </Box>
290 );
291 });
292};
293types/index.d.ts 51 lines1export type AgentRun = {
2 id: string;
3 type: string;
4 description: string;
5 model: string;
6 startedAt: number;
7 durationMs?: number;
8 tools: number;
9 status: "running" | "done" | "aborted";
10};
11
12export type Activity = {
13 at: number;
14 tool: string;
15 agent: string;
16 isDone: boolean;
17 id: string;
18};
19
20export type Project = {
21 name: string;
22 branch: string;
23 hasUpstream: boolean;
24 dirty: number;
25 ahead: number;
26 behind: number;
27 lastCommit: string;
28 worktrees: number;
29};
30
31export type Usage = {
32 model: string;
33 startedAt: number;
34 contextPercent?: number;
35 costUsd?: number;
36 limits: { kind: string; percentUsed: number }[];
37 turns: number;
38 now: number;
39};
40
41declare module "claude-code" {
42 interface PluginState {
43 dashboard: {
44 agents: AgentRun[];
45 activity: Activity[];
46 project: Project | null;
47 usage: Usage | null;
48 };
49 }
50}
51