SLOPSHOPPER

dashboard

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

newpaneguardcommandprocesstimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · dashboard
│ ┃ Dashboard ✕ › fix the failing auth test and add an audit log call │ ┃ app ⎇ feat/auth-refresh │ ┃ 2 changed · no upstream · 0 worktrees ⏺ Read(src/auth.ts) │ ┃ a1b2c3d fix: refresh expired tokens (2 ⎿ Read 6 lines │ ┃ minutes ago) 9f8e7d6 feat: add audit log (3 ⏺ Update(src/auth.ts) │ ┃ hours ago) ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ Session ⎿ 3 pass, 1 fail │ ┃ opus-5-5 · 1 turns · up 30m00s · $0.42 │ ┃ ctx ██████████░░░░░░░░░░ 49% ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ five_hou ████░░░░░░░░ 31% │ ┃ ✻ Worked for 42s · done 4:20 PM │ ┃ Agents (0 running) │ ┃ No subagents yet. › /dashboard │ ┃ ⎿ dashboard: Dashboard opened. │ ┃ Activity │ ┃ Read [main] │ ┃ Grep [main] │ ┃ Edit [main] │ ┃ Write [main] │ ┃ Write [main] │ ┃ Bash [main] │ ┃ Bash [main] │ ┃ Bash [main] │ ┃ Bash [main] │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Dashboard
app ⎇ feat/auth-refresh 2 changed · no upstream · 0 worktrees a1b2c3d fix: refresh expired tokens (2 minutes ago) 9f8e7d6 feat: add audit log (3 hours ago) Session opus-5-5 · 1 turns · up 30m00s · $0.42 ctx ██████████░░░░░░░░░░ 49% five_hou ████░░░░░░░░ 31% Agents (0 running) No subagents yet. Activity Read [main] Grep [main] Edit [main] Write [main] Write [main] Bash [main] Bash [main] Bash [main] Bash [main]
README

claude-code-mods

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.

ModWhat it doesWhereCommand
cute-feedA pet that hatches, grows and reacts to what Claude does, over a live emoji feedpane/feed, /pat
dashboardProject, git, session cost, context, rate limits, subagents and recent activitypane/dashboard
swarmLive tree of subagents: who spawned whom, what each is running right nowpane/swarm
repo-radarMap of the repo that lights up as files are read, edited or failpane/radar
boss-fightFailing lint, tests or typechecks spawn a boss; each fix damages itabove the prompt/boss
chimeSound + notification when a long turn finishes or Claude waits on yousound/chime
model-routerSends routine prompts and read-only subagents to a cheaper modelstatus line/route

Install

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.

Settings

Every setting is a row in /config, or under pluginConfigs.<mod> in your settings.

cute-feed

  • autoOpen (true) — open the pane when a session starts
  • petName ("Mochi"), species ("cat") — for a brand-new pet: cat, chick, dog, slime, bunny, hamster, penguin, frog
  • dangerPatterns ("") — 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 starts

repo-radar

  • autoOpen (false), depth (2) — folder levels always shown

boss-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\b

Recognised 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 routing
  • lightAgents ("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.

Using the pet

  • It hatches from an egg after 3 turns, then grows baby → kid → teen → adult (level 7). The adult form depends on how it was raised: Golden, Chonk, Scholar, Gremlin, Shepherd, Owl, Hacker or Classic.
  • It eats the lines Claude writes, gets hungry over time (also between sessions), gets sick after repeated failures, panics at destructive commands, gets stuffed as context fills up.
  • Buttons in the pane: Pat ♥, Feed 🍙, Play 🎾. Commands: /pat, /feed name <name>, /feed species <name>, /feed feed.

Develop

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.

License

MIT

Source 2 files
hooks/register.tsx 293 lines
1import { 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};
293
types/index.d.ts 51 lines
1export 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