Above the prompt, one animated pixel Clawd per running subagent, acting out what it is doing now.

A few mods for Claude Code that I use every day. They work in the terminal and in the desktop app.
From a shell:
claude plugin marketplace add dante32683/claude-code-mods
claude plugin install agent-crabs@dante32683-mods
Swap agent-crabs for any mod below. In the desktop app, click + next to the prompt box, then Plugins, add the marketplace dante32683/claude-code-mods, and pick the mods you want.
Mods load when a chat starts, so open a new chat after installing.
A little Clawd above the prompt for each subagent that's running, with what it's doing right now. The crab acts it out: reading a book, walking around with a magnifying glass while it searches, writing on paper while it edits, jogging while a command runs, holding a globe while it's on the web, and thinking in between. It throws confetti when the subagent finishes and sits under a rain cloud if it fails. /crabs-demo plays every animation.
The animated crabs show in the desktop app; the terminal gets a small text crab.
A pane with your plan's 5-hour and weekly usage, how fast you're using it, how much per hour or day you can spend and still make the reset, and what's filling the context window. While the pane is closed, the usage bars sit above the prompt. /meter shows or hides the pane. It saves the last reading to statusline-limits.json in your Claude folder.
Better compaction. Before each compaction it saves the whole conversation to a searchable file in ~/.claude/compact-history/ and tells Claude where it is, so details the summary drops can still be found. Your own messages are kept word for word, the summary is written as a handoff, instruction files are re-read afterward, and when a turn ends close to the auto-compact limit it compacts right away instead of mid-task. History older than 30 days is deleted. Options: turnEndPercent and verbatimUserTokens.
Tells Claude when other Claude chats are working in the same repo (same checkout or another worktree) and which files they've edited, so it can coordinate before work collides. Windows only for now.
/aside <task> hands a side task to a background worker while your chat keeps going. Unlike /btw, the worker has tools. It sees the last few messages for context, and its answer opens in a pane where you can send it to the chat or dismiss it. Haiku by default; /aside opus: <task> uses Opus.
Windows Git Bash only. It fixes a few ways Claude's Bash tool breaks there: very long commands get cut off or mangled, rg patterns starting with / get turned into Windows paths, sed -i doesn't say when nothing changed, and rm "${DIR:?}"/... keeps asking for approval.
agent-crabs takes its idea from hamzafer's agent-radar (MIT), and Clawd's shape from Codrops' article on reverse-engineering Claude's mascot animations. Clawd is Anthropic's mascot; this is a fan project, not affiliated with Anthropic. compact-plus's summary style is modeled on OpenAI's Codex CLI.
MIT
hooks/register.tsx 244 lines1// agent-crabs: above the prompt, one animated Clawd per running subagent and what it
2// is doing now (reading, searching, editing, running a command, on the web). The crab
3// throws confetti when its subagent finishes and goes gray under a rain cloud if it
4// fails; finished rows leave after 20 s. Activity comes from the subagents' own tool
5// calls; whether one is still running comes from $.agent.list(), so a missed end event
6// never leaves a row stuck. /crabs-demo plays every animation with two pretend crabs.
7//
8// Each pose is an interactive Svg, which draws in its own frame. Changing a frame's
9// source flashes it blank while it reloads, so a new pose loads hidden beside the one
10// showing and takes over after WARM_MS. Only those two are mounted, because the app
11// refuses a drawing over 100,000 characters.
12import { atom, read, update } from 'claude-code'
13import type { EngineInterface, Register, Timer } from 'claude-code'
14
15import type { Activity, Crab } from '../types'
16import { crabGlyph, crabSvg } from './sprite'
17
18const SHOW_DONE_MS = 20_000
19const WARM_MS = 900 // a new pose loads hidden this long before it shows
20const DEMO_STEP_MS = 3_000
21const REST_MS = 2_500 // with no tool running this long, the subagent is thinking again
22const ROWS = 4
23const KEEP = 20
24const LIVE = new Set(['pending', 'running', 'waiting'])
25export const ACTIVITIES: Activity[] = ['think', 'read', 'search', 'edit', 'run', 'web', 'done', 'failed']
26const SVGS = Object.fromEntries(ACTIVITIES.map(a => [a, crabSvg(a)])) as Record<Activity, string>
27
28// A new activity; its pose shows once it has loaded (see tick).
29export function setActivity(c: Crab, activity: Activity, at: number): Crab {
30 return activity === c.activity ? c : { ...c, activity, changedAt: at }
31}
32
33const crabs = atom({ plugin: 'agent-crabs', key: 'crabs' } as const, [] as Crab[])
34const now = atom({ plugin: 'agent-crabs', key: 'now' } as const, 0)
35
36export const register: Register = on => {
37 let timers: Timer[] = []
38
39 on('session.start', async ($, e, next) => {
40 const r = await next(e)
41 for (const t of timers) t.cancel()
42 // One tick a second while a row shows: elapsed times, the glyph crab's steps, and a
43 // check against the engine's own agent list.
44 timers = [$.clock.every(1000, () => void tick($).catch(() => {}))]
45 await $.command.register({ name: 'crabs-demo', description: 'Play every agent-crabs animation with two pretend subagents' }).catch(() => {})
46 return r
47 })
48
49 on('command.run', { command: 'crabs-demo' }, async $ => {
50 const t = Date.now()
51 const demo = (n: number, offset: number): Crab => ({ id: `demo-${n}-${t}`, label: `Demo crab ${n}`, activity: 'think', shown: 'think', changedAt: t, detail: 'demo: think', tools: 0, startedAt: t - offset })
52 await update($, crabs, list => [demo(1, 0), demo(2, DEMO_STEP_MS / 2), ...list].slice(0, KEEP))
53 await update($, now, () => t)
54 return { text: `Two pretend crabs play each animation for ${DEMO_STEP_MS / 1000} s.` }
55 })
56
57 on('agent.spawn', async ($, e, next) => {
58 const r = await next(e)
59 if (r.agentId) {
60 const crab: Crab = {
61 id: r.agentId,
62 label: e.description || e.subagentType || 'subagent',
63 activity: 'think',
64 shown: 'think',
65 changedAt: Date.now(),
66 detail: 'starting',
67 tools: 0,
68 startedAt: Date.now(),
69 }
70 await update($, crabs, list => [crab, ...list.filter(c => c.id !== crab.id)].slice(0, KEEP)).catch(() => {})
71 }
72 return r
73 })
74
75 // A subagent's tool call sets what its crab is doing; the main loop's calls pass by.
76 on('tool.call', async ($, e, next) => {
77 const id = e.agentId
78 if (!id) return next(e)
79 const { activity, detail } = describe(e as unknown as Record<string, unknown>)
80 // Drawing must never fail a tool call, so these updates swallow their errors.
81 await update($, crabs, list =>
82 list.map(c =>
83 c.id === id && !c.endedAt
84 ? { ...setActivity(c, activity, Date.now()), detail, tools: c.tools + 1, busy: (c.busy ?? 0) + 1, lastToolAt: Date.now() }
85 : c,
86 ),
87 ).catch(() => {})
88 try {
89 return await next(e)
90 } finally {
91 await update($, crabs, list =>
92 list.map(c => (c.id === id ? { ...c, busy: Math.max(0, (c.busy ?? 1) - 1), lastToolAt: Date.now() } : c)),
93 ).catch(() => {})
94 }
95 }).catch(($, e, next) => next(e))
96
97 on('turn.complete', async ($, e, next) => {
98 const r = await next(e)
99 if (e.agentId) await finish($, e.agentId, e.reason === 'error' || e.reason === 'aborted')
100 return r
101 })
102
103 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
104 const rest = await next(e)
105 const at = await read($, now) // subscribes the band to the tick
106 const shown = (await read($, crabs)).filter(c => isShown(c, at || Date.now()))
107 if (e.props.hasSurvey || shown.length === 0) return rest
108 const isDesktop = e.surface !== 'terminal'
109 const els = $.ui.resolve(e)
110 const { Box, Text } = els
111 const Svg = 'Svg' in els ? els.Svg : undefined
112 const frame = Math.floor((at || Date.now()) / 1000)
113 const rows = shown.slice(0, ROWS).map(c => (
114 <Box key={`crab-${c.id}`} flexDirection="row" alignItems="center" gap={1}>
115 {isDesktop && Svg ? (
116 <Box key={`art-${c.id}`} width={6} height={2}>
117 {[...new Set([c.shown, c.activity])].map(a => (
118 <Box key={`pose-${a}`} display={a === c.shown && (at || Date.now()) - c.startedAt >= WARM_MS ? 'flex' : 'none'}>
119 <Svg key={`sprite-${a}`} source={SVGS[a]} alt={`crab ${a}`} width={48} height={28} isInteractive />
120 </Box>
121 ))}
122 </Box>
123 ) : (
124 <Text color={c.activity === 'failed' ? 'gray' : '#D77757'}>{crabGlyph(c.activity, frame)}</Text>
125 )}
126 <Text wrap="truncate-end">
127 <Text bold dimColor={c.activity === 'failed'}>{c.label}</Text>
128 <Text dimColor>{` ${c.detail} · ${elapsed(c, at)}${c.tools ? ` · ${c.tools} tool${c.tools === 1 ? '' : 's'}` : ''}`}</Text>
129 </Text>
130 </Box>
131 ))
132 const mine = (
133 <Box key="agent-crabs" flexDirection="column" paddingX={1}>
134 {rows}
135 {shown.length > ROWS && <Text dimColor>{` +${shown.length - ROWS} more`}</Text>}
136 </Box>
137 )
138 return rest ? (
139 <Box flexDirection="column">
140 {mine}
141 {rest}
142 </Box>
143 ) : (
144 mine
145 )
146 })
147}
148
149async function tick($: EngineInterface) {
150 const list = await read($, crabs)
151 const t = Date.now()
152 if (!list.some(c => isShown(c, t - 2_000))) {
153 if (list.length) await update($, crabs, () => []) // all gone: stop redrawing
154 return
155 }
156 // A running crab whose agent the engine no longer lists as live has ended.
157 if (list.some(c => !c.endedAt)) {
158 const agents = await $.agent.list().catch(() => null)
159 if (agents) {
160 const status = new Map(agents.map(a => [a.id, a.status]))
161 for (const c of list) {
162 const s = status.get(c.id)
163 if (!c.endedAt && s !== undefined && !LIVE.has(s)) await finish($, c.id, s !== 'completed' && s !== 'idle')
164 }
165 }
166 }
167 // Pretend crabs step through every animation, then the first finishes and the second fails.
168 if (list.some(c => c.id.startsWith('demo-') && !c.endedAt)) {
169 await update($, crabs, all => all.map(c => (c.id.startsWith('demo-') && !c.endedAt ? demoStep(c, t) : c)))
170 }
171 // A subagent with no tool running for a while is thinking about its next step.
172 if (list.some(c => settle(c, t) !== c)) await update($, crabs, all => all.map(c => settle(c, t)))
173 // A pose that has had time to load takes over from the one showing.
174 if (list.some(c => c.activity !== c.shown)) {
175 await update($, crabs, all => all.map(c => (c.activity !== c.shown && t - c.changedAt >= WARM_MS ? { ...c, shown: c.activity } : c)))
176 }
177 await update($, now, () => t)
178}
179
180export function demoStep(c: Crab, at: number): Crab {
181 const i = Math.floor((at - c.startedAt) / DEMO_STEP_MS)
182 const steps = ACTIVITIES.slice(0, 6)
183 if (i < steps.length) {
184 const activity = steps[i]!
185 return activity === c.activity ? c : { ...setActivity(c, activity, at), detail: `demo: ${activity}`, tools: i }
186 }
187 const failed = c.label.endsWith('2')
188 return { ...setActivity(c, failed ? 'failed' : 'done', at), detail: failed ? 'stopped' : 'done', endedAt: at }
189}
190
191export function settle(c: Crab, t: number): Crab {
192 if (c.endedAt || c.busy || c.activity === 'think' || c.id.startsWith('demo-')) return c
193 return t - (c.lastToolAt ?? c.changedAt) >= REST_MS ? setActivity(c, 'think', t) : c
194}
195
196async function finish($: EngineInterface, id: string, failed: boolean) {
197 await update($, crabs, list =>
198 list.map(c =>
199 c.id === id && !c.endedAt
200 ? { ...setActivity(c, failed ? 'failed' : 'done', Date.now()), detail: failed ? 'stopped' : 'done', endedAt: Date.now() }
201 : c,
202 ),
203 )
204}
205
206export function isShown(c: Crab, at: number) {
207 return !c.endedAt || at - c.endedAt < SHOW_DONE_MS
208}
209
210export function elapsed(c: Pick<Crab, 'startedAt' | 'endedAt'>, at: number) {
211 const s = Math.max(0, Math.round(((c.endedAt ?? (at || Date.now())) - c.startedAt) / 1000))
212 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
213}
214
215// What a person would say a tool call is doing, and which animation fits it.
216export function describe(e: Record<string, unknown>): { activity: Activity; detail: string } {
217 const tool = String(e.tool)
218 const s = (k: string) => (typeof e[k] === 'string' ? (e[k] as string) : '')
219 const file = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p
220 const short = (t: string, n = 48) => (t.length > n ? `${t.slice(0, n - 1)}…` : t)
221 switch (tool) {
222 case 'Read':
223 return { activity: 'read', detail: `reading ${file(s('file_path'))}` }
224 case 'Grep':
225 case 'Glob':
226 return { activity: 'search', detail: `searching ${short(s('pattern'), 32)}` }
227 case 'Edit':
228 case 'Write':
229 case 'NotebookEdit':
230 return { activity: 'edit', detail: `editing ${file(s('file_path') || s('notebook_path'))}` }
231 case 'Bash':
232 case 'PowerShell':
233 return { activity: 'run', detail: short(s('description') || s('command')) }
234 case 'WebSearch':
235 return { activity: 'web', detail: `searching the web: ${short(s('query'), 40)}` }
236 case 'WebFetch': {
237 const host = /^https?:\/\/([^/]+)/.exec(s('url'))?.[1] ?? 'a page'
238 return { activity: 'web', detail: `reading ${host}` }
239 }
240 }
241 if (tool.startsWith('mcp__')) return { activity: 'think', detail: `using ${tool.split('__').slice(1).join(' ').replace(/_/g, ' ')}` }
242 return { activity: 'think', detail: `using ${tool}` }
243}
244hooks/sprite.ts 368 lines1// Clawd, the Claude Code crab, as frame-by-frame pixel sprites, one loop per activity.
2// Proportions are the official mascot's (Codrops' rect-by-rect rebuild, 2026-05-05):
3// on a 2-pixel unit, a 16x12 body, 4x4 hands, 2x2 eyes and four 2x4 legs. Like the
4// official clips, each activity has its own prop and pose: a book, a magnifier, a
5// pencil and paper, a headband for a run, a spinning globe, thought dots, confetti, a
6// rain cloud. Each SVG holds every frame of its loop and shows one at a time with
7// discrete SMIL, which the Desktop app runs in an interactive (sandboxed) Svg.
8import type { Activity } from '../types'
9
10export const W = 38
11export const H = 22
12const OX = 4 // Clawd's left edge on the canvas
13const OY = 6 // Clawd's top edge; the ground is at H
14
15const PALETTE: Record<string, string> = {
16 O: '#DD775B', // body
17 g: '#8A8A8A', // body, failed
18 K: '#000000', // eyes
19 W: '#F3EEE6', // paper
20 G: '#A39C94', // ink lines
21 B: '#3F6FC4', // book cover
22 Y: '#F2C14E', // pencil
23 P: '#E88FA3', // eraser
24 D: '#3A3A3A', // pencil tip, magnifier handle
25 M: '#D3D9DF', // magnifier ring
26 H: '#9A6A3C', // magnifier handle
27 L: '#8FCBEA', // lens
28 N: '#2F4C86', // headband
29 F: '#8A8F96', // speed lines, dust
30 U: '#4A90D9', // globe sea
31 E: '#5DBB63', // globe land
32 C: '#B7BDC4', // cloud
33 R: '#6FB3E8', // rain
34 T: '#BDBDBD', // thought dots
35 S: '#7FC8F0', // sweat
36 '1': '#E07A5F',
37 '2': '#F2C14E',
38 '3': '#6FA8DC',
39 '4': '#C77DFF',
40 '5': '#7BD389',
41}
42
43type Eyes = 'open' | 'closed' | 'happy' | 'x' | 'squint'
44
45// One frame: Clawd's pose plus props, held for `hold` seconds.
46type Frame = {
47 hold: number
48 dx?: number
49 dy?: number
50 eyes?: Eyes
51 look?: [number, number]
52 armL?: number
53 armR?: number
54 lift?: [number, number, number, number]
55 air?: boolean
56 headband?: boolean
57 body?: string
58 props?: Sprite[]
59}
60
61type Sprite = { x: number; y: number; rows: string[] }
62
63// A pixel canvas: later paints cover earlier ones.
64class Canvas {
65 px = new Map<string, string>()
66 set(x: number, y: number, c: string) {
67 if (x >= 0 && y >= 0 && x < W && y < H) this.px.set(`${x},${y}`, c)
68 }
69 rect(x: number, y: number, w: number, h: number, c: string) {
70 for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) this.set(x + i, y + j, c)
71 }
72 sprite(s: Sprite) {
73 s.rows.forEach((row, j) => [...row].forEach((ch, i) => ch !== '.' && this.set(s.x + i, s.y + j, ch)))
74 }
75 // One path per color: runs of a color on a row, merged down the rows while they
76 // line up, become rectangles. Keeps a frame to a few hundred characters.
77 toPaths(): string {
78 const runs = new Map<string, { x: number; y: number; n: number; h: number }[]>()
79 const open = new Map<string, { x: number; y: number; n: number; h: number }>()
80 for (let y = 0; y < H; y++) {
81 let x = 0
82 while (x < W) {
83 const c = this.px.get(`${x},${y}`)
84 if (!c) {
85 x++
86 continue
87 }
88 let n = 1
89 while (this.px.get(`${x + n},${y}`) === c) n++
90 const key = `${c}|${x}|${n}`
91 const above = open.get(key)
92 if (above && above.y + above.h === y) above.h++
93 else {
94 const r = { x, y, n, h: 1 }
95 open.set(key, r)
96 runs.set(c, [...(runs.get(c) ?? []), r])
97 }
98 x += n
99 }
100 }
101 let out = ''
102 for (const [c, rs] of runs) out += `<path fill="${PALETTE[c] ?? c}" d="${rs.map(r => `M${r.x} ${r.y}h${r.n}v${r.h}h-${r.n}z`).join('')}"/>`
103 return out
104 }
105}
106
107const EYES: Record<Eyes, string[]> = {
108 open: ['KK', 'KK'],
109 closed: ['..', 'KK'],
110 squint: ['K.', '.K'],
111 happy: ['.K.', 'K.K'],
112 x: ['K.K', '.K.', 'K.K'],
113}
114
115function drawFrame(f: Frame): string {
116 const c = new Canvas()
117 const ox = OX + (f.dx ?? 0)
118 const oy = OY + (f.dy ?? 0)
119 const body = f.body ?? 'O'
120 const lift = f.lift ?? [0, 0, 0, 0]
121 ;[4, 8, 14, 18].forEach((lx, i) => {
122 const top = oy + 12
123 const h = (f.air ? 4 : H - top) - lift[i]!
124 if (h > 0) c.rect(ox + lx, top, 2, h, body)
125 })
126 c.rect(ox + 4, oy, 16, 12, body)
127 c.rect(ox, oy + 4 + (f.armL ?? 0), 4, 4, body)
128 c.rect(ox + 20, oy + 4 + (f.armR ?? 0), 4, 4, body)
129 if (f.headband) {
130 c.rect(ox + 4, oy + 1, 16, 1, 'N')
131 // The knot's tails stream back behind a runner.
132 c.set(ox + 3, oy + 1, 'N')
133 c.rect(ox + 1, oy + 2 + ((f.dy ?? 0) < 0 ? -1 : 0), 2, 1, 'N')
134 }
135 const eyes = EYES[f.eyes ?? 'open']
136 const [lx, ly] = f.look ?? [0, 0]
137 const shift = eyes[0]!.length === 3 ? -1 : 0
138 for (const ex of [6, 16]) c.sprite({ x: ox + ex + lx + (eyes[0]!.length === 3 && ex === 6 ? 0 : shift), y: oy + 2 + ly + (eyes.length === 3 ? -1 : 0), rows: eyes })
139 for (const p of f.props ?? []) c.sprite(p)
140 return c.toPaths()
141}
142
143// ---------------------------------------------------------------- props
144
145const dots = (n: number): Sprite[] =>
146 [
147 { x: 29, y: 5, rows: ['T'] },
148 { x: 31, y: 2, rows: ['TT', 'TT'] },
149 { x: 34, y: 0, rows: ['TTT', 'TTT'] },
150 ].slice(0, n)
151
152// An open book like the emoji: blue cover at the sides and bottom, pages dipping to the
153// spine, lines of text that change with the page.
154function book(page: number, flip: 0 | 1 | 2): Sprite[] {
155 const text = page % 2 ? ['BGGGGWWWW', 'WGGGGGWWW', 'WGGGWWWWW'] : ['BGGGGGWWW', 'WGGGWWWWW', 'WGGGGGWWW']
156 const left = [
157 '..WWWW...',
158 'WWWWWWWW.',
159 text[0]!,
160 'BWWWWWWWW',
161 text[1]!.replace(/^W/, 'B'),
162 'BWWWWWWWW',
163 text[2]!.replace(/^W/, 'B'),
164 'BBBBWWWWW',
165 '....BBBBB',
166 ]
167 const spine = ['.', '.', 'G', 'G', 'G', 'G', 'G', 'W', 'B']
168 const rows = left.map((r, j) => r + spine[j] + [...left[j]!].reverse().join(''))
169 const out: Sprite[] = [{ x: OX + 3, y: OY + 5, rows }]
170 // A page lifting off the right half and laying over the left.
171 if (flip === 1) out.push({ x: OX + 13, y: OY + 1, rows: ['WW', 'WW', 'WW', 'WW'] })
172 if (flip === 2) out.push({ x: OX + 6, y: OY + 2, rows: ['.WWWWW', 'WWWWWW', '..WWWW'] })
173 return out
174}
175
176// A magnifying glass like the emoji, held up in front: a silver ring, a blue lens with
177// a shine, and a wooden handle running down into Clawd's raised hand.
178const GLASS = [
179 '.....MMMMM..',
180 '....MLLLLLM.',
181 '...MLWWLLLLM',
182 '...MLWLLLLLM',
183 '...MLLLLLLLM',
184 '...MLLLLLLLM',
185 '...MLLLLLLLM',
186 '....MLLLLLM.',
187 '...HHMMMMM..',
188 '..HHH.......',
189 '.HHH........',
190 'HH..........',
191]
192
193function glass(dx: number, dy: number, glint: boolean): Sprite[] {
194 const lens: Sprite = { x: OX + dx + 19, y: OY - 6 + dy, rows: GLASS }
195 // The sparkle sits off the lens's upper left, where the canvas has room for it.
196 return glint ? [lens, { x: OX + dx + 19, y: OY - 6 + dy, rows: ['.W.', 'WWW', '.W.'] }] : [lens]
197}
198
199function pencilAndPaper(len: number[]): Sprite[] {
200 const paper = Array.from({ length: 9 }, () => 'WWWWWWW')
201 len.forEach((n, k) => {
202 const r = 2 + k * 2
203 paper[r] = 'W' + 'G'.repeat(n) + 'W'.repeat(6 - n)
204 })
205 const k = len.length ? len.length - 1 : 0
206 const tx = 30 + (len[k] ?? 0)
207 const ty = 15 + k * 2
208 const pencil: Sprite[] = [
209 { x: tx, y: ty, rows: ['D'] },
210 { x: tx - 1, y: ty - 1, rows: ['Y'] },
211 { x: tx - 2, y: ty - 2, rows: ['Y'] },
212 { x: tx - 3, y: ty - 3, rows: ['Y'] },
213 { x: tx - 4, y: ty - 4, rows: ['P'] },
214 ]
215 return [{ x: 29, y: 13, rows: paper }, ...pencil]
216}
217
218// Speed lines streaming back past a running Clawd (who runs in place, 5 right of usual).
219function speedLines(k: number): Sprite[] {
220 return [
221 [OY + 3, 4, 0],
222 [OY + 9, 4, 5],
223 ].map(([y, n, phase]) => ({ x: ((phase! - 2 * k) % 9 + 9) % 9 - 2, y: y!, rows: ['F'.repeat(n!)] }))
224}
225
226const LAND = ['......EE......', '..EE..EEE...E.', '.EEEE..EE..EEE', '..EEE...E..EE.', '...E.......E..', '..........EE..', '..............']
227const DISC = ['..UUU..', '.UUUUU.', 'UUUUUUU', 'UUUUUUU', 'UUUUUUU', '.UUUUU.', '..UUU..']
228
229function globe(turn: number, dy: number): Sprite {
230 const rows = DISC.map((r, j) =>
231 [...r].map((ch, i) => (ch === 'U' && LAND[j]![(i + turn) % 14] === 'E' ? 'E' : ch)).join(''),
232 )
233 return { x: 30, y: 0 + dy, rows }
234}
235
236// Confetti bursting from above Clawd, frame by frame.
237function confetti(t: number, fromX: number): Sprite[] {
238 const bits = [
239 [-6, -5, '1'], [-3, -7, '2'], [0, -8, '3'], [3, -7, '4'], [6, -5, '5'], [-5, -3, '3'], [5, -3, '1'], [1, -6, '5'],
240 ] as const
241 if (t === 0) return []
242 return bits.map(([vx, vy, col]) => {
243 const x = Math.round(fromX + (vx * t) / 3)
244 const y = Math.round(6 + (vy * t) / 2 + (t * t) / 2.2)
245 return { x, y, rows: [col] }
246 })
247}
248
249const cloud = (shift: number): Sprite[] => [
250 { x: OX + 6, y: 0, rows: ['..CCCC..', '.CCCCCCC', 'CCCCCCCC'] },
251 { x: OX + 7, y: 3 + shift, rows: ['R...R...'] },
252 { x: OX + 9, y: 4 - shift, rows: ['R...R'] },
253]
254
255// ---------------------------------------------------------------- loops
256
257const A: [number, number, number, number] = [1, 0, 1, 0]
258const B: [number, number, number, number] = [0, 1, 0, 1]
259
260export const LOOPS: Record<Activity, Frame[]> = {
261 think: [
262 { hold: 0.42, look: [1, -1], props: dots(1) },
263 { hold: 0.42, look: [1, -1], props: dots(2) },
264 { hold: 0.68, look: [1, -1], props: dots(3) },
265 { hold: 0.1, look: [1, -1], eyes: 'closed', props: dots(3) },
266 { hold: 0.5, look: [0, 0] },
267 ],
268 read: [
269 { hold: 0.55, look: [-1, 1], armL: 3, armR: 3, props: book(0, 0) },
270 { hold: 0.55, look: [1, 1], armL: 3, armR: 3, props: book(0, 0) },
271 { hold: 0.12, look: [1, 1], armL: 3, armR: 1, props: book(0, 1) },
272 { hold: 0.12, look: [0, 1], armL: 3, armR: 2, props: book(1, 2) },
273 { hold: 0.55, look: [-1, 1], armL: 3, armR: 3, props: book(1, 0) },
274 { hold: 0.12, look: [-1, 1], eyes: 'closed', armL: 3, armR: 3, props: book(1, 0) },
275 { hold: 0.55, look: [1, 1], armL: 3, armR: 3, props: book(1, 0) },
276 ],
277 // Peering through the glass, it steps forward, pauses (the lens glints), backs up.
278 search: [
279 { hold: 0.2, dx: 0, lift: A, look: [1, -1], armR: -2, props: glass(0, 0, false) },
280 { hold: 0.2, dx: 1, lift: B, look: [1, -1], armR: -1, props: glass(1, 1, false) },
281 { hold: 0.2, dx: 2, lift: A, look: [1, -1], armR: -2, props: glass(2, 0, false) },
282 { hold: 0.5, dx: 2, look: [1, -1], armR: -2, props: glass(2, 0, true) },
283 { hold: 0.2, dx: 2, look: [1, 0], armR: -1, props: glass(2, 1, false) },
284 { hold: 0.2, dx: 1, lift: B, look: [1, 0], armR: -2, props: glass(1, 0, false) },
285 { hold: 0.2, dx: 0, lift: A, look: [1, -1], armR: -1, props: glass(0, 1, false) },
286 { hold: 0.2, dx: -1, lift: B, look: [1, -1], armR: -2, props: glass(-1, 0, false) },
287 { hold: 0.5, dx: -1, look: [1, -1], armR: -2, props: glass(-1, 0, true) },
288 ],
289 edit: [
290 { hold: 0.16, look: [1, 1], armR: 1, props: pencilAndPaper([1]) },
291 { hold: 0.16, look: [1, 1], armR: 0, props: pencilAndPaper([3]) },
292 { hold: 0.16, look: [1, 1], armR: 1, props: pencilAndPaper([5]) },
293 { hold: 0.16, look: [1, 1], armR: 2, props: pencilAndPaper([5, 2]) },
294 { hold: 0.16, look: [1, 1], armR: 3, props: pencilAndPaper([5, 5]) },
295 { hold: 0.16, look: [1, 1], armR: 4, props: pencilAndPaper([5, 5, 3]) },
296 { hold: 0.5, look: [1, 1], eyes: 'happy', armR: 4, props: pencilAndPaper([5, 5, 5]) },
297 { hold: 0.25, look: [0, 0], armR: 0, props: pencilAndPaper([]) },
298 ],
299 // A steady run in place: legs step in pairs, a small bob between strides, arms swing
300 // against the legs, and two speed lines drift back.
301 run: [0, 1, 2, 3, 4, 5, 6, 7].map((k): Frame => {
302 const stride = k % 4 // 0 and 2: one pair of legs is up; 1 and 3: passing
303 return {
304 hold: 0.14,
305 dx: 5,
306 dy: stride % 2 ? -1 : 0,
307 headband: true,
308 look: [1, 0],
309 lift: stride === 0 ? [2, 0, 2, 0] : stride === 2 ? [0, 2, 0, 2] : [0, 0, 0, 0],
310 armL: stride === 0 ? -1 : stride === 2 ? 1 : 0,
311 armR: stride === 0 ? 1 : stride === 2 ? -1 : 0,
312 props: speedLines(k),
313 }
314 }),
315 web: [0, 1, 2, 3, 4, 5, 6].map((k): Frame => ({
316 hold: 0.2,
317 look: [1, -1],
318 lift: k % 2 ? [0, 0, 0, 1] : [0, 0, 0, 0],
319 armR: k % 4 < 2 ? -2 : -3,
320 props: [globe(k * 2, k % 4 < 2 ? 0 : 1)],
321 })),
322 done: [0, 1, 2, 3, 4, 5, 6, 7].map((t): Frame => {
323 const side = t % 4
324 return {
325 hold: 0.145,
326 eyes: 'happy',
327 dx: side === 1 ? -1 : side === 3 ? 1 : 0,
328 armL: side === 1 ? -5 : 0,
329 armR: side === 3 ? -5 : 0,
330 lift: side === 1 ? [0, 0, 1, 1] : side === 3 ? [1, 1, 0, 0] : [0, 0, 0, 0],
331 props: confetti(t, t < 4 ? 9 : 27),
332 }
333 }),
334 failed: [
335 { hold: 0.3, dy: 1, body: 'g', eyes: 'x', props: cloud(0) },
336 { hold: 0.3, dy: 1, body: 'g', eyes: 'x', props: cloud(1) },
337 ],
338}
339
340// Every frame in one SVG, each visible for its slot of the loop.
341export function crabSvg(activity: Activity): string {
342 const frames = LOOPS[activity]
343 const total = frames.reduce((s, f) => s + f.hold, 0)
344 let t = 0
345 const groups = frames.map((f, i) => {
346 const a = t / total
347 t += f.hold
348 const b = t / total
349 if (frames.length === 1) return `<g>${drawFrame(f)}</g>`
350 const r = (n: number) => Number(n.toFixed(4))
351 const anim =
352 i === 0
353 ? `values="visible;hidden" keyTimes="0;${r(b)}"`
354 : i === frames.length - 1
355 ? `values="hidden;visible" keyTimes="0;${r(a)}"`
356 : `values="hidden;visible;hidden" keyTimes="0;${r(a)};${r(b)}"`
357 return `<g visibility="hidden">${drawFrame(f)}<animate attributeName="visibility" ${anim} calcMode="discrete" dur="${r(total)}s" repeatCount="indefinite"/></g>`
358 })
359 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" shape-rendering="crispEdges">${groups.join('')}</svg>`
360}
361
362// The terminal has no Svg: a small glyph crab instead, its legs stepping by frame.
363export function crabGlyph(activity: Activity, frame: number): string {
364 if (activity === 'failed') return '▐▀█▀▌'
365 if (activity === 'done') return frame % 2 ? '▐▛█▜▌' : '▝▜█▛▘'
366 return frame % 2 ? '▐▛█▜▌' : '▗▛█▜▖'
367}
368types/index.d.ts 28 lines1// What a subagent is doing, which picks its crab's animation.
2export type Activity = 'think' | 'read' | 'search' | 'edit' | 'run' | 'web' | 'done' | 'failed'
3
4// One subagent this session started. `id` is its agentId.
5export type Crab = {
6 id: string
7 label: string
8 activity: Activity
9 detail: string
10 tools: number
11 startedAt: number
12 endedAt?: number
13 // The pose on screen. A new activity's pose loads hidden beside it and takes over
14 // once it has had WARM_MS to load (changedAt), so a swap never shows a blank frame.
15 shown: Activity
16 changedAt: number
17 // Its tool calls in flight, and when one last started or ended: a crab with none
18 // running for a while goes back to thinking.
19 busy?: number
20 lastToolAt?: number
21}
22
23declare module 'claude-code' {
24 interface PluginState {
25 'agent-crabs': { crabs: Crab[]; now: number }
26 }
27}
28