A pixel-art crab for every running subagent, dressed for its role, showing what it is working on.

A small collection of Claude Code mods.
/plugin install <mod> --marketplace Arkboi-GG/claude-mods
The bars stack above the prompt, so you can install any combination.
skins mod, run /skin band off once so its own usage band doesn't show twice.git; gh is optional).web=5173,api=3000).checks) and shows pass/fail with an error count.LOCAL badge that turns red PROD when a tool call matches your production patterns, and toasts when it happens.Name: , e.g. Olivia: rewrite welcome email; map names to outfits with the outfits option.hooks/register.tsx 254 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Crab } from '../types'
5
6const crabsAtom = atom({ plugin: 'agent-crabs', key: 'crabs' } as const, {} as Record<string, Crab>)
7const tickAtom = atom({ plugin: 'agent-crabs', key: 'tick' } as const, 0)
8
9const ORANGE = '#fb923c'
10
11// Pixel art. The crab is a 16 x 11 grid; every letter is a colour in PALETTE, a dot is empty.
12const W = 16
13const H = 11
14const SCALE = 2
15
16const PALETTE: Record<string, string> = {
17 o: '#e2724a', // body
18 d: '#b4512f', // legs
19 c: '#f4977a', // claws
20 w: '#ffffff',
21 k: '#1a1a1a',
22 R: '#dc2626',
23 B: '#3b82f6',
24 b: '#3b82f6',
25 g: '#9ca3af',
26 N: '#1e3a8a',
27 K: '#1f2937',
28 Y: '#facc15',
29 y: '#facc15',
30 h: '#94a3b8',
31 l: '#67e8f9',
32 T: '#b08968',
33}
34
35// The grey a failed crab turns.
36const GREY: Record<string, string> = { o: '#6b7280', d: '#4b5563', c: '#9ca3af' }
37
38type Rows = Record<number, string>
39
40const BODY: Rows = {
41 4: '..oooooooooooo..',
42 5: '.oowkooooowkooo.',
43 6: '.oowwooooowwooo.',
44 7: '.oooooooooooooo.',
45 8: '..oooooooooooo..',
46}
47
48const LEGS: Rows[] = [
49 { 9: '..d.d.d..d.d.d..', 10: '.d.d.d....d.d.d.' },
50 { 9: '.d.d.d....d.d.d.', 10: '..d.d.d..d.d.d..' },
51]
52
53const CLAW_LEFT_UP: Rows = { 1: 'c.c.............', 2: 'ccc.............', 3: '.c..............', 4: '.c..............' }
54const CLAW_RIGHT_UP: Rows = { 1: '.............c.c', 2: '.............ccc', 3: '..............c.', 4: '..............c.' }
55const CLAW_LEFT_DOWN: Rows = { 5: 'c.c.............', 6: 'ccc.............', 7: '.c..............' }
56const CLAW_RIGHT_DOWN: Rows = { 5: '.............c.c', 6: '.............ccc', 7: '..............c.' }
57
58const OUTFITS: Record<string, Rows> = {
59 plain: {},
60 beret: { 1: '........R.......', 2: '.....RRRRRR.....', 3: '....RRRRRRRR....' },
61 hoodie: { 2: '.....BBBBBB.....', 3: '....BBBBBBBB....', 7: '.....gggggg.....', 8: '.....gggggg.....' },
62 postman: { 2: '.....NNNNNN.....', 3: '...NNNNNNNNNN...', 7: '......wwww......', 8: '......wddw......' },
63 grad: { 2: '..KKKKKKKKKKKK..', 3: '.....KKKKKK.Y...', 4: '............Y...' },
64 headset: { 2: '.....hhhhhh.....', 3: '....h......h....', 4: '...h........h...', 5: '...h........h...' },
65 magnifier: {
66 2: '......TTTT......',
67 3: '.....TTTTTT.....',
68 6: '.......ll.......',
69 7: '......l..l......',
70 8: '......l..l......',
71 9: '.......ll.......',
72 10: '..........T.....',
73 },
74 megaphone: { 7: '...........yy...', 8: '...........yyy..', 9: '............yyy.' },
75 hardhat: { 1: '.......YY.......', 2: '.....YYYYYY.....', 3: '...YYYYYYYYYY...', 7: '..........bb....', 8: '..........bw....' },
76 explorer: { 1: '......TTTT......', 2: '.....TTTTTT.....', 3: '...TTTTTTTTTT...' },
77}
78
79// Agent name (lowercase) or agent type to outfit. Anything else gets a plain crab.
80const DEFAULT_OUTFITS: Record<string, string> = {
81 olivia: 'beret',
82 jack: 'hoodie',
83 achilles: 'postman',
84 dexter: 'grad',
85 erica: 'headset',
86 paul: 'magnifier',
87 joanne: 'megaphone',
88 simon: 'hardhat',
89 explore: 'explorer',
90 plan: 'hardhat',
91}
92
93// One crab as an SVG: claws alternate with the frame, both raised when it has finished.
94function crabSvg(outfit: string, frame: number, state: Crab['state']): string {
95 const grid: string[][] = Array.from({ length: H }, () => Array(W).fill('.'))
96 const paint = (rows: Rows) => {
97 for (const [y, row] of Object.entries(rows)) {
98 for (let x = 0; x < W; x++) if (row[x] && row[x] !== '.') grid[Number(y)][x] = row[x]
99 }
100 }
101 paint(BODY)
102 paint(LEGS[frame % 2])
103 if (state === 'done') {
104 paint(CLAW_LEFT_UP)
105 paint(CLAW_RIGHT_UP)
106 } else if (frame % 2 === 0) {
107 paint(CLAW_LEFT_UP)
108 paint(CLAW_RIGHT_DOWN)
109 } else {
110 paint(CLAW_LEFT_DOWN)
111 paint(CLAW_RIGHT_UP)
112 }
113 paint(OUTFITS[outfit] ?? OUTFITS.plain)
114
115 const colour = (ch: string) => (state === 'failed' && GREY[ch] ? GREY[ch] : PALETTE[ch])
116 let rects = ''
117 for (let y = 0; y < H; y++) {
118 let x = 0
119 while (x < W) {
120 const ch = grid[y][x]
121 if (ch === '.' || !PALETTE[ch]) {
122 x++
123 continue
124 }
125 let run = 1
126 while (x + run < W && grid[y][x + run] === ch) run++
127 rects += `<rect x="${x}" y="${y}" width="${run}" height="1" fill="${colour(ch)}"/>`
128 x += run
129 }
130 }
131 return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * SCALE}" height="${H * SCALE}" viewBox="0 0 ${W} ${H}" shape-rendering="crispEdges">${rects}</svg>`
132}
133
134const mmss = (ms: number) => {
135 const s = Math.max(0, Math.floor(ms / 1000))
136 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
137}
138
139// 'Olivia: rewrite welcome email' -> name Olivia, task 'rewrite welcome email'.
140// The brain's router puts the agent's name in front of every named dispatch.
141const NAMED = /^([A-Za-z][\w.-]{0,24}):\s+(.+)$/
142
143export const register: Register = (on, options) => {
144 const lingerMs = Math.max(1, Number(options.lingerSeconds ?? 6)) * 1000
145 const maxShown = Math.max(1, Number(options.maxShown ?? 4))
146
147 const outfits: Record<string, string> = { ...DEFAULT_OUTFITS }
148 for (const pair of String(options.outfits ?? '').split(',')) {
149 const [name, outfit] = pair.split('=').map(s => s.trim())
150 if (name && outfit) outfits[name.toLowerCase()] = outfit
151 }
152
153 const describe = (a: { id: string; description: string; type: string }) => {
154 const m = a.type === 'general-purpose' ? NAMED.exec(a.description) : null
155 const name = m ? m[1] : a.type === 'general-purpose' ? '' : a.type
156 const task = m ? m[2] : a.description
157 const outfit = outfits[name.toLowerCase()] ?? outfits[a.type.toLowerCase()] ?? 'plain'
158 return { id: a.id, name, outfit, task }
159 }
160
161 // Crabs that have already left. The engine keeps a finished agent in its list for a while,
162 // so without this a crab would come back each time it was cleared away.
163 const gone = new Set<string>()
164
165 on('session.start', async ($, e, next) => {
166 const poll = async () => {
167 try {
168 const list = await $.agent.list()
169 const now = await $.clock.now()
170 const prev = await read($, crabsAtom)
171 const out: Record<string, Crab> = {}
172 const seen = new Set<string>()
173 for (const a of list) {
174 if (a.type === 'teammate') continue
175 seen.add(a.id)
176 const state: Crab['state'] =
177 a.status === 'completed' ? 'done' : a.status === 'failed' || a.status === 'killed' ? 'failed' : 'running'
178 if (gone.has(a.id)) {
179 // Only an agent that runs again (resumed) brings its crab back.
180 if (state !== 'running') continue
181 gone.delete(a.id)
182 }
183 const old = prev[a.id]
184 const base: Crab = old ?? { ...describe(a), startedAt: now, state }
185 out[a.id] = { ...base, state, endedAt: state === 'running' ? undefined : (old?.endedAt ?? now) }
186 }
187 // An agent the engine has dropped from the list has finished.
188 for (const [id, c] of Object.entries(prev)) {
189 if (!seen.has(id)) out[id] = c.state === 'running' ? { ...c, state: 'done', endedAt: now } : c
190 }
191 for (const [id, c] of Object.entries(out)) {
192 if (c.endedAt !== undefined && now - c.endedAt > (c.state === 'failed' ? lingerMs * 3 : lingerMs)) {
193 delete out[id]
194 gone.add(id)
195 }
196 }
197 await update($, crabsAtom, p => (JSON.stringify(p) === JSON.stringify(out) ? p : out))
198 // Once a second while there are crabs, so claws move and the timers count.
199 if (Object.keys(out).length > 0) await update($, tickAtom, n => n + 1)
200 } catch {
201 // the agent list is not ready: keep what is on screen
202 }
203 }
204 $.clock.every(1000, poll)
205 return next(e)
206 })
207
208 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
209 const below = await next(e)
210 if (e.props.hasSurvey) return below
211 const crabs = Object.values(await read($, crabsAtom)).sort((a, b) => a.startedAt - b.startedAt)
212 const tick = await read($, tickAtom)
213 if (crabs.length === 0) return below
214
215 const now = await $.clock.now()
216 const { Box, Text, Svg } = $.ui.resolve(e)
217 const shown = crabs.slice(-maxShown)
218 const hidden = crabs.length - shown.length
219
220 const rows = shown.map(c => {
221 const label = c.name || 'agent'
222 const stamp = mmss((c.endedAt ?? now) - c.startedAt)
223 const status =
224 c.state === 'running' ? <Text dimColor>{stamp}</Text> : c.state === 'done' ? <Text color="#22c55e">✓ {stamp}</Text> : <Text color="#ef4444">✗ failed</Text>
225 return (
226 <Box paddingX={1} alignItems="center" gap={1}>
227 <Box flexShrink={0}>
228 <Svg source={crabSvg(c.outfit, tick, c.state)} alt={`${label} crab`} />
229 </Box>
230 <Box flexShrink={0}>
231 <Text bold color={ORANGE}>{label}</Text>
232 </Box>
233 <Box flexGrow={1} flexShrink={1} minWidth={0}>
234 <Text dimColor wrap="truncate-end">{c.task}</Text>
235 </Box>
236 <Box flexShrink={0}>{status}</Box>
237 </Box>
238 )
239 })
240
241 const zone = (
242 <Box key="agent-crabs" flexDirection="column">
243 {rows}
244 {hidden > 0 ? (
245 <Box paddingX={1}>
246 <Text dimColor>+{hidden} more</Text>
247 </Box>
248 ) : null}
249 </Box>
250 )
251 return below ? <Box flexDirection="column">{zone}{below}</Box> : zone
252 })
253}
254types/index.d.ts 16 lines1export type Crab = {
2 id: string
3 name: string
4 outfit: string
5 task: string
6 startedAt: number
7 endedAt?: number
8 state: 'running' | 'done' | 'failed'
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'agent-crabs': { crabs: Record<string, Crab>; tick: number }
14 }
15}
16