SLOPSHOPPER

working-clawd

While Claude works, Clawd walks above the prompt: across a starry sky in the desktop app, Ghostty and kitty, stopping to look around, wave and jump, and back…

newbandguardtimeragents
v0.2.1no licenseupdated 2026-10-08cprentice9/ccmods/working-clawd
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · working-clawd
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▀▀▀▄▀▀▀▀▀▀▄▀▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
▄▄▄▄▄▄▄▄▄▄▄▄ ▄▄▀▀▀▄▀▀▀▀▀▀▄▀▄▄ ▀▀▀▀▀▀▀▀▀▀▀▀
README

working-clawd

While Claude works on a turn, Clawd walks above the prompt. He walks to one end, turns around and walks back. Every so often he stops to look around, wave or jump. He leaves when the turn ends, and the band goes away once no subagents are left running.

Each running subagent gets a smaller Clawd. It hops out of him when the subagent starts and walks beside him until the subagent finishes. Up to eight minis show at once.

Where it draws

  • The Claude desktop app. He walks the full width of the prompt box against a dark starry sky, in the moves the claude.dev blog animates him with.
  • Ghostty and kitty. The same scene, sent as a picture through the kitty graphics protocol.
  • Other terminals. A simpler Clawd in half-block characters walks back and forth. Above him go the checklist progress, the task in progress and the latest command.

It doesn't draw in editors that run Claude Code in their own agent panel, such as Zed.

Install

claude plugin marketplace add cprentice9/byname
claude plugin install working-clawd@cprentice9

Start a new session after installing. It needs a Claude Code build that loads function-hook mods.

To update:

claude plugin marketplace update cprentice9
claude plugin update working-clawd@cprentice9

Checking it

From the ccmods folder:

claude plugin validate working-clawd
claude plugin test working-clawd

Credits

Clawd is Anthropic's mascot. The walking, turning, looking, waving and jumping frames are sampled from the animations on Anthropic's claude.dev blog.

Source 4 files
hooks/register.tsx 658 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { WorkingTask, WorkingTasks, WorkingTurn } from '../types'
5
6import { CLIP_HEIGHT, CLIP_WIDTH, CLIPS } from './clips'
7import type { ClipName } from './clips'
8import { encodePng } from './png'
9
10const KEY = 'clawd'
11const BIG = 18
12const MINI = 8
13// Frames each mini trails behind: even, so the minis hop in step with him.
14const FOLLOW = 16
15const SPACING = 10
16const MAX_MINIS = 8
17const MIN_COLUMNS = 24
18const MAX_COLUMNS = 512
19const HEIGHT = 6
20const ROWS = HEIGHT / 2
21// The clips' own rate, 12 frames a second; the terminal walker steps every
22// other frame.
23const TICK_MS = 83
24// The desktop band, in CSS pixels: a half pixel of Clawd, and a rough width
25// of one of the band's columns, which sets how fast he crosses it.
26const HALF = 2.5
27const COLUMN = 6
28// The band shows the clips from this row down: the top of his raised arm in
29// the wave. The jump rises higher, so it hops only as high as the wave.
30const TOP = 9
31// The minis' size next to Clawd.
32const MINI_SCALE = 0.75
33// The sky's colors, from the claude.dev page, and its clusters' dot spacing.
34const SKY = '#151515'
35const STAR = '#8c8c8c'
36const GRID = 2.4
37// Half pixels of sky drawn, wider than any window; the app cuts it to the band.
38const SKY_WIDTH = 1000
39const BAND_ROWS = CLIP_HEIGHT - TOP
40const BAND_HEIGHT = BAND_ROWS * HALF
41// The same scene in kitty and Ghostty, as a picture three rows tall, one
42// image pixel to a half pixel. A cell is about twice as tall as it is wide,
43// so each column takes half as many pixels as each row.
44const PICTURE_ROWS = 3
45const PICTURE_SCALE = 4
46const pictureWidth = (columns: number) => Math.round((columns * BAND_ROWS) / PICTURE_ROWS / 2)
47const working = atom({ plugin: 'working-clawd', key: 'turnId' } as const, null as WorkingTurn)
48const agents = atom({ plugin: 'working-clawd', key: 'agents' } as const, [] as string[])
49const tasks = atom({ plugin: 'working-clawd', key: 'tasks' } as const, {} as WorkingTasks)
50const commands = atom({ plugin: 'working-clawd', key: 'commands' } as const, [] as string[])
51const MAX_COMMANDS = 8
52// Every 2 seconds the minis are checked against the engine's agent list.
53const PRUNE_TICKS = 24
54const ENDED = ['completed', 'failed', 'killed']
55
56const CLEAR = -1
57const DEFAULT = 0x01000000
58const ORANGE = 0xd77757
59const mod = (n: number, m: number) => ((n % m) + m) % m
60
61// Where the walk puts a sprite's center at frame t: one column per frame,
62// turning around at each end, and which way it faces.
63const path = (t: number, columns: number) => {
64  const range = columns - BIG
65  const step = mod(t, 2 * range)
66  const center = BIG / 2 + (step <= range ? step : 2 * range - step)
67  return { center, facing: step < range ? 1 : -1 }
68}
69
70// One frame's pixels, `columns` wide and HEIGHT tall. Clawd, drawn as the CLI
71// logo draws him, walks the path; every other frame he hops, feet splayed, his
72// eyes look the way he is walking, and he blinks every 16th frame. Each mini,
73// one per running subagent, walks the same path a set number of frames behind,
74// so they follow him around each turn and hop in step.
75const pixels = (t: number, columns: number, minis: number, hasLeader: boolean) => {
76  const px = new Int32Array(columns * HEIGHT).fill(CLEAR)
77  const sprite = (at: number, width: number) => {
78    const { center, facing } = path(at, columns)
79    const left = center - width / 2
80    const isHop = mod(at, 2) === 1
81    const set = (x: number, y: number, color = ORANGE) => (px[y * columns + left + x] = color)
82    return { facing, isHop, set }
83  }
84
85  for (let i = Math.min(minis, MAX_MINIS) - 1; i >= 0; i--) {
86    const { isHop, set } = sprite(t - FOLLOW - SPACING * i, MINI)
87    const top = isHop ? 1 : 2
88    for (let row = 0; row < 3; row++) {
89      for (let x = 1; x < 7; x++) set(x, top + row)
90    }
91    set(0, top + 2)
92    set(7, top + 2)
93    set(2, top + 1, CLEAR)
94    set(5, top + 1, CLEAR)
95    for (const x of [1, 6]) set(x, isHop ? 4 : 5)
96    if (isHop) for (const x of [0, 7]) set(x, 5)
97  }
98
99  if (hasLeader) {
100    const { facing, isHop, set } = sprite(t, BIG)
101    const top = isHop ? 0 : 1
102    for (let row = 0; row < 4; row++) {
103      for (let x = 3; x < 15; x++) set(x, top + row)
104    }
105    for (const x of [1, 2, 15, 16]) set(x, top + 2)
106    if (mod(t, 16) !== 15) {
107      set(5 + facing, top + 1, CLEAR)
108      set(12 + facing, top + 1, CLEAR)
109    }
110    // Legs start at the edges of his body; a hop splays the feet.
111    for (const x of [3, 5, 12, 14]) set(x, isHop ? 4 : 5)
112    if (isHop) for (const x of [2, 6, 11, 15]) set(x, 5)
113  }
114  return px
115}
116
117// A frame as Raster cells for the terminal. Two pixels per cell: the upper
118// half block takes the top pixel as its color and the bottom one as its
119// background.
120export const frame = (t: number, columns: number, minis = 0, hasLeader = true) => {
121  const px = pixels(t, columns, minis, hasLeader)
122  const words = new Uint32Array(columns * ROWS * 3)
123  for (let row = 0; row < ROWS; row++) {
124    for (let x = 0; x < columns; x++) {
125      const upper = px[2 * row * columns + x]!
126      const lower = px[(2 * row + 1) * columns + x]!
127      const cell = [0x20, DEFAULT, DEFAULT]
128      if (upper !== CLEAR) cell.splice(0, 3, 0x2580, upper, lower === CLEAR ? DEFAULT : lower)
129      else if (lower !== CLEAR) cell.splice(0, 3, 0x2584, lower, DEFAULT)
130      words.set(cell, (row * columns + x) * 3)
131    }
132  }
133  return new Uint8Array(words.buffer).toBase64()
134}
135
136// The checklist line: how many tasks are done, and the one in progress.
137// Nothing while the list is empty.
138export const checklist = (list: WorkingTasks) => {
139  const all = Object.values(list)
140  if (all.length === 0) return undefined
141  const done = all.filter(task => task.status === 'completed').length
142  const active = all.find(task => task.status === 'in_progress')
143  return { count: `${done} of ${all.length}`, now: active && (active.activeForm ?? active.subject) }
144}
145
146// "Bash npm test", "Edit register.tsx": the tool and its main argument, a file
147// by its name, cut to one short line.
148export const describeCommand = (call: Record<string, unknown>) => {
149  const main = [call.command, call.file_path, call.pattern, call.url, call.query, call.description]
150    .find(v => typeof v === 'string') as string | undefined
151  const what = main === undefined ? '' : main === call.file_path ? main.split('/').pop()! : main.split('\n')[0]!
152  return `${String(call.tool)} ${what}`.trim().slice(0, 60)
153}
154
155// The desktop walker, as the claude.dev blog has him: he walks the band from
156// end to end, turns at each end, and every so often stops to look around,
157// wave, jump, or turn back. Walking and turning frames face right, so heading
158// left draws them mirrored.
159const decode = (frame: string) => frame.split('/').map(row => row.replace(/(\d*)(\D)/g, (_, n, cell) => cell.repeat(Number(n || 1))))
160const FRAMES = Object.fromEntries(Object.entries(CLIPS).map(([name, clip]) => [name, clip.frames.map(decode)])) as Record<ClipName, string[][]>
161const WALK_CYCLE = [2, 3, 4, 5, 6]
162const ACTS: ClipName[] = ['looking', 'waving', 'jumping', 'turning']
163const COLORS: Record<string, string> = { O: '#d97757', S: '#be684d', s: '#774635', d: '#553428' }
164
165export type Walker = {
166  x: number
167  dir: 1 | -1
168  clip: ClipName
169  // The frame within the clip: for walking, steps taken; -1 is the turn from
170  // facing front to the side.
171  at: number
172  // Steps until he stops to do something.
173  until: number
174}
175
176export const startWalker = (): Walker => ({ x: 0, dir: 1, clip: 'walking', at: -1, until: 40 })
177
178// The band's width in half pixels, from its width in columns.
179const viewWidth = (columns: number) => Math.floor((columns * COLUMN) / HALF)
180
181// His next frame. `t` picks the next stop and act, so the walk repeats
182// differently each time without any randomness to test around.
183export const stepWalker = (w: Walker, width: number, t: number): Walker => {
184  const right = Math.max(0, width - CLIP_WIDTH)
185  if (w.clip !== 'walking') {
186    if (w.at + 1 < CLIPS[w.clip].seq.length) return { ...w, at: w.at + 1 }
187    const dir = w.clip === 'turning' ? (-w.dir as 1 | -1) : w.dir
188    return { ...w, dir, clip: 'walking', at: -1, until: 30 + mod(t * 37, 90) }
189  }
190  const x = Math.min(right, Math.max(0, w.x + w.dir))
191  const atEnd = (w.dir > 0 && x >= right) || (w.dir < 0 && x <= 0)
192  if (atEnd) return { ...w, x, clip: 'turning', at: 0 }
193  if (w.until <= 0) return { ...w, x, clip: ACTS[mod(t, ACTS.length)]!, at: 0 }
194  return { ...w, x, at: w.at + 1, until: w.until - 1 }
195}
196
197// His frame as rows of cells, mirrored when he faces left.
198export const walkerFrame = (w: Walker) => {
199  const index = w.clip === 'walking' ? (w.at < 0 ? 1 : WALK_CYCLE[mod(w.at, WALK_CYCLE.length)]!) : CLIPS[w.clip].seq[w.at]!
200  const rows = FRAMES[w.clip][index]!
201  return w.dir < 0 ? rows.map(row => [...row].reverse().join('')) : rows
202}
203
204// His frame's rows from TOP down, BAND_ROWS of them. A frame that reaches
205// higher (the jump) is moved down to fit.
206const bandRows = (w: Walker) => {
207  const rows = walkerFrame(w)
208  const lift = Math.max(0, TOP - rows.findIndex(row => /[^.]/.test(row)))
209  return Array.from({ length: BAND_ROWS }, (_, y) => rows[y + TOP - lift] ?? '')
210}
211
212// A pose's shape, from his left edge: a path for each color through that
213// color's runs in each row, his eyes left as holes.
214const posePaths = (rows: string[]) => {
215  const runs: Record<string, string> = {}
216  rows.forEach((row, y) => {
217    for (let x = 0; x < row.length; x++) {
218      const cell = row[x]!
219      if (!COLORS[cell]) continue
220      let end = x
221      while (row[end + 1] === cell) end++
222      const w = end - x + 1
223      runs[cell] = `${runs[cell] ?? ''}M${x} ${y}h${w}v1h-${w}z`
224      x = end
225    }
226  })
227  return Object.entries(runs).map(([cell, d]) => `<path fill="${COLORS[cell]}" d="${d}"/>`).join('')
228}
229
230// A number from 0 to 1 that depends only on `n`, so the sky stays the same
231// from frame to frame.
232const hash = (n: number) => {
233  let x = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b)
234  x ^= x >>> 13
235  x = Math.imul(x, 0xc2b2ae35)
236  x ^= x >>> 16
237  return (x >>> 0) / 2 ** 32
238}
239const round = (n: number) => Math.round(n * 10) / 10
240
241// The stars behind him, `width` half pixels wide: single stars, and clusters
242// of dots on a grid that thin out toward their edges.
243const stars = (width: number) => {
244  const dots: { x: number; y: number; r: number; opacity: number }[] = []
245  for (let i = 0; i < width / 5; i++) {
246    const r = round(0.3 + 0.3 * hash(i * 3 + 2))
247    const opacity = round(0.3 + 0.5 * hash(i * 3 + 3))
248    dots.push({ x: round(hash(i * 3) * width), y: round(hash(i * 3 + 1) * BAND_ROWS), r, opacity })
249  }
250  for (let i = 0; i < Math.max(1, width / 80); i++) {
251    const seed = 1000 + i * 5
252    const cx = hash(seed) * width
253    const cy = hash(seed + 1) * BAND_ROWS
254    const size = 3 + 4 * hash(seed + 2)
255    for (let gx = Math.ceil((cx - size) / GRID); gx * GRID <= cx + size; gx++) {
256      for (let gy = Math.ceil((cy - size) / GRID); gy * GRID <= cy + size; gy++) {
257        const far = Math.hypot(gx * GRID - cx, gy * GRID - cy) / size
258        if (far >= 1 || gx < 0 || gy < 0 || gx * GRID > width || gy * GRID > BAND_ROWS || hash(seed + gx * 131 + gy * 7) < 0.3 + far / 2) continue
259        dots.push({ x: round(gx * GRID), y: round(gy * GRID), r: round(0.7 * (1 - far / 2)), opacity: 0.5 })
260      }
261    }
262  }
263  return dots
264}
265
266export const starfield = (width: number) =>
267  stars(width).map(d => `<circle cx="${d.x}" cy="${d.y}" r="${d.r}" fill="${STAR}" opacity="${d.opacity}"/>`).join('')
268
269// The sky: a dark fill and its stars, built once.
270let sky: string | undefined
271const skyMarkup = () => (sky ??= `<rect width="100%" height="100%" fill="${SKY}"/><g transform="scale(${HALF})">${starfield(SKY_WIDTH)}</g>`)
272export const skySvg = () => `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="${BAND_HEIGHT}">${skyMarkup()}</svg>`
273
274// The desktop draws the band as one SVG that animates itself: the sky, and
275// each walker's walk worked out ahead from stepWalker a frame a tick. The mod
276// sends it again only when the band changes. A new image every frame grew
277// the app's memory without end.
278
279// The most markup an Svg takes, and about what each frame of a walk costs.
280// The sky, the poses and the rest leave the walks the remainder: Clawd a
281// loop of up to four minutes, the minis the rest between them.
282const MAX_SOURCE = 131072
283const TICK_CHARS = 18
284const POSE_CHARS = 30000
285const LEADER_TICKS = 3000
286export const miniTicks = (minis: number) =>
287  Math.floor(((MAX_SOURCE - skyMarkup().length - POSE_CHARS) / TICK_CHARS - LEADER_TICKS) / Math.max(1, minis))
288
289// Back where he starts: at the left end, facing right, about to walk.
290const isHome = (w: Walker) => w.clip === 'walking' && w.at === -1 && w.x === 0 && w.dir === 1
291
292export type Walk = { intro: Walker[]; loop: Walker[] }
293
294// His walk from `w` at tick `t`, at most `ticks` frames: the frames until he
295// first gets home, then the most laps from home back home. With no lap that
296// fits, the frames he has, so he jumps back to the start of them each time.
297export const planWalk = (w: Walker, width: number, t: number, ticks: number): Walk => {
298  const frames = [w]
299  const homes: number[] = []
300  for (let i = 1; i <= ticks; i++) {
301    w = stepWalker(w, width, t + i)
302    if (isHome(w)) homes.push(i)
303    frames.push(w)
304  }
305  const home = isHome(frames[0]!) ? 0 : homes[0]
306  const cut = homes.at(-1)
307  if (home === undefined || cut === undefined || cut <= home) return { intro: [], loop: frames.slice(0, ticks) }
308  return { intro: frames.slice(0, home), loop: frames.slice(home, cut) }
309}
310
311// The poses the band's walkers take, each once, side by side in a strip that
312// a window his size shows one of at a time. Facing left, a pose is drawn
313// mirrored.
314const poseStrip = () => {
315  const poses = new Map<string, number>()
316  const slots = new Map<string, number>()
317  const defs: string[] = []
318  const strip: string[] = []
319  const slot = (w: Walker) => {
320    const rows = bandRows({ ...w, dir: 1 })
321    const key = rows.join('/')
322    if (!poses.has(key)) {
323      defs.push(`<g id="p${poses.size}">${posePaths(rows)}</g>`)
324      poses.set(key, poses.size)
325    }
326    const pose = poses.get(key)!
327    const name = `${pose} ${w.dir}`
328    if (!slots.has(name)) {
329      const left = slots.size * CLIP_WIDTH
330      strip.push(w.dir > 0
331        ? `<use href="#p${pose}" x="${left}"/>`
332        : `<use href="#p${pose}" transform="translate(${left + CLIP_WIDTH} 0) scale(-1 1)"/>`)
333      slots.set(name, slots.size)
334    }
335    return slots.get(name)!
336  }
337  return { slot, defs: () => `<defs>${defs.join('')}<g id="strip">${strip.join('')}</g></defs>` }
338}
339
340export type Walking = { walk: Walk; scale: number; elapsed: number }
341
342// One walker at `scale`, `elapsed` milliseconds into his walk. Three discrete
343// animations of `x` play it: how far along the band he is, the same share of
344// his own width back, so he spans it end to end without knowing its pixels,
345// and which pose of the strip shows through his window.
346const walkerMarkup = ({ walk, scale, elapsed }: Walking, width: number, slot: (w: Walker) => number) => {
347  const span = Math.max(1, Math.floor(width / scale) - CLIP_WIDTH)
348  const introMs = walk.intro.length * TICK_MS
349  const loopMs = walk.loop.length * TICK_MS
350  const parts = elapsed < introMs
351    ? [{ frames: walk.intro, begin: -elapsed, repeat: false }, { frames: walk.loop, begin: introMs - elapsed, repeat: true }]
352    : [{ frames: walk.loop, begin: -((elapsed - introMs) % loopMs), repeat: true }]
353  const animate = (value: (w: Walker) => string | number) => parts.map(p =>
354    `<animate attributeName="x" calcMode="discrete" values="${p.frames.map(value).join(';')}" dur="${p.frames.length * TICK_MS}ms" begin="${p.begin}ms"`
355    + `${p.repeat ? ' repeatCount="indefinite"' : ' fill="freeze"'}/>`).join('')
356  const along = (w: Walker) => w.x / span
357  return `<svg overflow="visible">${animate(w => `${round(along(w) * 100)}%`)}`
358    + `<g transform="translate(0 ${BAND_HEIGHT * (1 - scale)}) scale(${HALF * scale})" shape-rendering="crispEdges">`
359    + `<svg width="${CLIP_WIDTH}" height="${BAND_ROWS}">${animate(w => round(-along(w) * CLIP_WIDTH))}`
360    + `<svg overflow="visible">${animate(w => -slot(w) * CLIP_WIDTH)}<use href="#strip"/></svg></svg></g></svg>`
361}
362
363// The band as one SVG as wide as the app makes it: the sky, then each walker
364// in turn, so the last stands in front.
365export const bandSvg = (walkers: Walking[], width: number) => {
366  const { slot, defs } = poseStrip()
367  const bodies = walkers.map(w => walkerMarkup(w, width, slot)).join('')
368  return `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="${BAND_HEIGHT}">${defs()}${skyMarkup()}${bodies}</svg>`
369}
370
371// A new mini hops out of Clawd and heads off the other way.
372const spawnMini = (leader: Walker, i: number): Walker => ({
373  x: Math.round(leader.x / MINI_SCALE),
374  dir: (i % 2 ? leader.dir : -leader.dir) as 1 | -1,
375  clip: 'jumping',
376  at: 0,
377  until: 0,
378})
379
380const rgb = (hex: string) => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16))
381const SKY_RGB = rgb(SKY)
382const STAR_RGB = rgb(STAR)
383const CELL_RGB = Object.fromEntries(Object.entries(COLORS).map(([cell, hex]) => [cell, rgb(hex)]))
384
385// The sky as RGBA pixels, `width` by BAND_ROWS, each star one pixel mixed
386// into the dark at its opacity.
387const skyPixels = (width: number) => {
388  const px = new Uint8Array(width * BAND_ROWS * 4)
389  for (let i = 0; i < px.length; i += 4) px.set([...SKY_RGB, 255], i)
390  for (const d of stars(width)) {
391    const x = Math.floor(d.x)
392    const y = Math.floor(d.y)
393    if (x >= width || y >= BAND_ROWS) continue
394    px.set(SKY_RGB.map((c, k) => Math.round(c + (STAR_RGB[k]! - c) * d.opacity)), (y * width + x) * 4)
395  }
396  return px
397}
398const skies = new Map<number, Uint8Array>()
399
400// One walker painted into `px` at `scale`, standing on the bottom edge, each
401// pixel taking the frame cell under it.
402const paint = (px: Uint8Array, width: number, w: Walker, scale: number) => {
403  const rows = bandRows(w)
404  const tall = Math.round(BAND_ROWS * scale)
405  const left = Math.round(w.x * scale)
406  for (let ty = 0; ty < tall; ty++) {
407    const row = rows[Math.floor(ty / scale)]!
408    for (let tx = 0; tx < Math.round(CLIP_WIDTH * scale); tx++) {
409      const color = CELL_RGB[row[Math.floor(tx / scale)] ?? '.']
410      const x = left + tx
411      if (color && x < width) px.set(color, ((BAND_ROWS - tall + ty) * width + x) * 4)
412    }
413  }
414}
415
416// The band as RGBA pixels for kitty and Ghostty, `width` wide: the sky, the
417// minis, and Clawd in front, as the desktop draws them.
418const scene = (w: Walker | null, width: number, minis: Walker[]) => {
419  if (!skies.has(width)) skies.set(width, skyPixels(width))
420  const px = skies.get(width)!.slice()
421  for (const m of minis) paint(px, width, m, MINI_SCALE)
422  if (w) paint(px, width, w, 1)
423  return px
424}
425
426export const picture = (w: Walker | null, width: number, minis: Walker[] = []) =>
427  ({ rgba: scene(w, width, minis).toBase64(), width, height: BAND_ROWS })
428
429// What the Image shows: the scene as a PNG PICTURE_SCALE times larger, or as
430// large as the 4096 pixel limit allows, so the terminal barely stretches it.
431export const pictureSource = (w: Walker | null, width: number, minis: Walker[] = []) =>
432  ({ png: encodePng(scene(w, width, minis), width, BAND_ROWS, Math.max(1, Math.min(PICTURE_SCALE, Math.floor(4096 / width)))) })
433
434// What the band last drew, for the timer's repaints between draws.
435let timer: Timer | undefined
436let tick = 0
437let band: { id: string; surface: string; columns: number; minis: number; hasLeader: boolean } | undefined
438let walker = startWalker()
439let followers: Walker[] = []
440// Whether the terminal shows pictures (kitty, Ghostty), read at session start.
441let hasPictures = false
442// The desktop band: when the walk began, less the time the band sat idle, so
443// a new turn picks his walk up where the last one left it; when the band went
444// idle; when each mini hopped out; and the SVG as last drawn, so a redraw
445// sends the same markup.
446let since = 0
447let idleAt = 0
448let spawned: number[] = []
449let drawn: { key: string; source: string } | undefined
450let leaderWalk: { width: number; walk: Walk } | undefined
451
452// The desktop band's SVG: the minis first, so Clawd stands in front. Each mini
453// hops out of Clawd where his walk had him when it spawned.
454const desktopBand = (columns: number, hasLeader: boolean, now: number) => {
455  const width = viewWidth(columns)
456  const key = `${since} ${width} ${hasLeader} ${spawned.join(' ')}`
457  if (drawn?.key === key) return drawn.source
458  if (leaderWalk?.width !== width) leaderWalk = { width, walk: planWalk(startWalker(), width, 0, LEADER_TICKS) }
459  const { loop } = leaderWalk.walk
460  const walkers: Walking[] = spawned.map((at, i) => {
461    const t = Math.floor((at - since) / TICK_MS)
462    const mini = spawnMini(loop[t % loop.length]!, i)
463    const walk = planWalk(mini, Math.floor(width / MINI_SCALE), t + 7 * (i + 1), miniTicks(spawned.length))
464    return { walk, scale: MINI_SCALE, elapsed: now - at }
465  })
466  if (hasLeader) walkers.push({ walk: leaderWalk.walk, scale: 1, elapsed: now - since })
467  drawn = { key, source: bandSvg(walkers, width) }
468  return drawn.source
469}
470
471function start($: EngineInterface) {
472  timer ??= $.clock.every(TICK_MS, () => {
473    tick += 1
474    if (tick % PRUNE_TICKS === 0) prune($).catch(() => {})
475    // The desktop's walkers animate themselves. The terminal swaps its
476    // picture or repaints its cells in place. A frame that cannot be painted
477    // is skipped and the next one tries.
478    if (band?.surface !== 'terminal') return
479    if (hasPictures) {
480      const width = pictureWidth(band.columns)
481      if (band.hasLeader) walker = stepWalker(walker, width, tick)
482      const count = Math.min(band.minis, MAX_MINIS)
483      while (followers.length < count) followers.push(spawnMini(walker, followers.length))
484      followers = followers.slice(0, count).map((m, i) => stepWalker(m, Math.floor(width / MINI_SCALE), tick + 7 * (i + 1)))
485      const source = pictureSource(band.hasLeader ? walker : null, pictureWidth(band.columns), followers)
486      $.ui.blit({ requestId: band.id, key: KEY, source }).catch(() => {})
487    } else {
488      const cells = frame(Math.floor(tick / 2), band.columns, band.minis, band.hasLeader)
489      $.ui.blit({ requestId: band.id, key: KEY, cells }).catch(() => {})
490    }
491  })
492}
493
494function stop() {
495  timer?.cancel()
496  timer = undefined
497}
498
499// With nothing left working, the band goes and the animation stops.
500async function settle($: EngineInterface) {
501  if (!(await read($, working)) && (await read($, agents)).length === 0) {
502    stop()
503    idleAt = await $.clock.now()
504  }
505  $.ui.invalidate('ui.render')
506}
507
508// Drops the subagents the engine lists as ended. Their turn.complete can go
509// missing, or a late tool call can add one back after it, and its mini would
510// walk forever. A workflow's agents are not listed, so they stay.
511async function prune($: EngineInterface) {
512  const ended = new Set((await $.agent.list()).filter(a => ENDED.includes(a.status)).map(a => a.id))
513  if (!(await read($, agents)).some(id => ended.has(id))) return
514  await update($, agents, a => a.filter(id => !ended.has(id)))
515  await settle($)
516}
517
518export const register: Register = on => {
519  // A reload while anything works picks the animation back up.
520  on('session.start', async ($, e, next) => {
521    const r = await next(e)
522    const term = await $.env.get('TERM').catch(() => undefined)
523    hasPictures = /kitty|ghostty/.test(term ?? '')
524    if ((await read($, working)) || (await read($, agents)).length > 0) start($)
525    return r
526  })
527
528  on('turn.start', async ($, e, next) => {
529    if (!timer) {
530      tick = 0
531      since += (await $.clock.now()) - idleAt
532      spawned = []
533    }
534    await update($, working, () => e.turnId)
535    await update($, commands, () => [])
536    start($)
537    $.ui.invalidate('ui.render')
538    return next(e)
539  })
540
541  on('agent.spawn', async ($, e, next) => {
542    const ran = await next(e)
543    const { agentId } = ran
544    if (agentId) {
545      await update($, agents, a => (a.includes(agentId) ? a : [...a, agentId]))
546      start($)
547      $.ui.invalidate('ui.render')
548    }
549    return ran
550  })
551
552  // The main loop's TaskCreate and TaskUpdate calls keep the checklist.
553  // Its other calls feed the latest-command line as they start. A call from
554  // a subagent that already finished means it resumed. That check sits here,
555  // not on turn.step, which would pass every streamed chunk through the mod.
556  on('tool.call', async ($, e, next) => {
557    const call = e as unknown as Record<string, any>
558    const agentId = call.agentId as string | undefined
559    if (agentId && !(await read($, agents)).includes(agentId)) {
560      await update($, agents, a => (a.includes(agentId) ? a : [...a, agentId]))
561      start($)
562      $.ui.invalidate('ui.render')
563    }
564    if (!agentId && !/^(Task|TodoWrite|SubagentHandback)/.test(String(call.tool))) {
565      const line = describeCommand(call)
566      await update($, commands, list => [...list, line].slice(-MAX_COMMANDS))
567    }
568    const ran = await next(e)
569    const result = ran.result as Record<string, any> | undefined
570    if (call.agentId || ran.isError) return ran
571    if (call.tool === 'TaskCreate' && result?.task?.id) {
572      const task: WorkingTask = { subject: String(result.task.subject), status: 'pending' }
573      if (typeof call.activeForm === 'string') task.activeForm = call.activeForm
574      await update($, tasks, list => ({ ...list, [String(result.task.id)]: task }))
575    } else if (call.tool === 'TaskUpdate' && result?.success !== false) {
576      const id = String(call.taskId)
577      await update($, tasks, list => {
578        const task = list[id]
579        if (!task) return list
580        if (call.status === 'deleted') {
581          const { [id]: _, ...rest } = list
582          return rest
583        }
584        const changed: WorkingTask = { ...task }
585        if (typeof call.subject === 'string') changed.subject = call.subject
586        if (typeof call.activeForm === 'string') changed.activeForm = call.activeForm
587        if (call.status === 'pending' || call.status === 'in_progress' || call.status === 'completed') changed.status = call.status
588        return { ...list, [id]: changed }
589      })
590    }
591    return ran
592  })
593
594  on('turn.complete', async ($, e, next) => {
595    const { agentId } = e
596    if (agentId) {
597      await update($, agents, a => a.filter(id => id !== agentId))
598    } else if (e.turnId === (await read($, working))) {
599      await update($, working, () => null)
600    }
601    await settle($)
602    return next(e)
603  })
604
605  // The band spans the window, so a resize redraws it at the new width. The
606  // terminal draws cells; the desktop app, which has no Raster, the sky and
607  // an SVG for each walker, and nothing else.
608  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
609    const hasLeader = (await read($, working)) !== null
610    const minis = (await read($, agents)).length
611    const columns = Math.min(e.props.bodyColumns, MAX_COLUMNS)
612    const isIdle = !hasLeader && minis === 0
613    if (isIdle || e.props.hasSurvey || columns < MIN_COLUMNS) return next(e)
614    if (e.surface === 'desktop') {
615      band = { id: e.requestId, surface: e.surface, columns, minis, hasLeader }
616      const now = await $.clock.now()
617      while (spawned.length < Math.min(minis, MAX_MINIS)) spawned.push(now)
618      spawned = spawned.slice(0, minis)
619      const { Box, Svg } = $.ui.resolve(e)
620      // The app paints a placed Box over everything before it. So the sky holds
621      // the band's height, the sky color runs past the band's edges to cover
622      // its padding, and the band's own SVG, its sky and walkers, goes on top.
623      return (
624        <Box position="relative" flexDirection="column">
625          <Svg source={skySvg()} height={BAND_HEIGHT} alt="A starry sky" />
626          <Box position="absolute" top={-2} left={-2} right={-2} bottom={-2} backgroundColor={SKY} />
627          <Box position="absolute" top={0} left={0} right={0} bottom={0} flexDirection="column">
628            <Svg source={desktopBand(columns, hasLeader, now)} height={BAND_HEIGHT} alt="Clawd walking along the prompt box" />
629          </Box>
630        </Box>
631      )
632    }
633    if (e.surface !== 'terminal') return next(e)
634    band = { id: e.requestId, surface: e.surface, columns, minis, hasLeader }
635    const { Box, Image, Raster, Text } = $.ui.resolve(e)
636    if (hasPictures) {
637      const source = pictureSource(hasLeader ? walker : null, pictureWidth(columns), followers.slice(0, minis))
638      return <Image key={KEY} source={source} columns={columns} rows={PICTURE_ROWS} alt="Clawd walking along the prompt" />
639    }
640    const progress = checklist(await read($, tasks))
641    const latest = (await read($, commands)).at(-1)
642    const task = progress && (
643      <Text wrap="truncate">{progress.now ? `${progress.now}  ` : ''}<Text dimColor>{progress.count}</Text></Text>
644    )
645    return (
646      <Box flexDirection="column">
647        {(task || latest) && (
648          <Box flexDirection="row">
649            {latest && <Box flexShrink={1} marginRight={3}><Text dimColor wrap="truncate">{latest}</Text></Box>}
650            {task}
651          </Box>
652        )}
653        <Raster key={KEY} columns={columns} rows={ROWS} cells={frame(Math.floor(tick / 2), columns, minis, hasLeader)} />
654      </Box>
655    )
656  })
657}
658
hooks/clips.ts 79 lines
1// Clawd's five moves as the claude.dev blog animates him, sampled from its
2// Lottie clips at 12 frames a second onto a grid of half pixels, 24 wide by
3// 30 tall. Each frame's rows are run-length coded and joined by `/`: a count,
4// then a cell (O body, S and s shades, d dark shade, D eye, . empty). `seq`
5// is the clip's frames in play order, by index into `frames`.
6export const CLIP_WIDTH = 24
7export const CLIP_HEIGHT = 30
8
9export const CLIPS = {
10  walking: {
11    seq: [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 0, 0, 0],
12    frames: [
13      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
14      '24./24./24./24./24./24./24./24./24./24./24./24./24./11.9O4./4.3S13O4./4.3S11O2D4./4.3S2O2D7O2D4./4.3S2O2D9OS2O./.3S16OS2O./.3S16OS2O./.3S16OS2O./.3S16O4./4.3S13O4./4.3S13O4./4.3S13O4./4.3S9O2.3O3./4.2S2.2O4.2O3.2O3./4.2S2.2O4.2O3.2O3./4.2S2.2O4.2O3.2O3./4.2S2.2O4.2O8.',
15      '24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D7O2D4./.3S5O2D7O2D4./.3S16O4./.3S16OS2O./.3S16OS2O./4.3S13OS2O./4.3S13OS2O./4.3S13O4./4.3S13O4./4.3S13O4./4.2S3.2O3.2O2.2O4./3.3S3.2O3.2O2.3O3./3.2S3.3O2.3O3.2O3./2.2S4.2O3.2O5.2O2./2.2S9.2O5.2O2.',
16      '24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D7O2D4./.3S5O2D7O2D4./.3S16O4./.3S16OS2O./.3S16OS2O./4.3S13OS2O./4.3S13OS2O./4.3S13O4./4.3S13O4./4.3S13O4./5.3S2.3O2.2O.2O4./5.3S2.3O2.2O.2O4./4.3S4.2O.3O.2O4./4.2S5.2O.2O2.2O4./12.2O4.2O4./12.2O4.2O4.',
17      '24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D7O2D4./4.3S2O2D7O2D4./.3S16OS2O./.3S16OS2O./.3S16OS2O./.3S16OS2O./4.3S13O4./4.3S13O4./4.3S13O4./4.3S13O4./4.3S13O4./6.2S2.2O3.5O4./6.2S2.2O4.3O5./7.2S.2O4.3O5./7.2S.2O5.2O5.',
18      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D7O2D4./4.3S2O2D7O2D4./.3S16OS2O./.3S16OS2O./.3S16OS2O./.3S16OS2O./4.3S13O4./4.3S13O4./4.3S13O4./4.3S14O3./4.2S2.2O5.2O2.2O3./4.2S2.2O5.2O2.2O3./4.2S.2O7.2O.2O3./4.2S.2O7.2O6.',
19      '24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D7O2D4./.3S5O2D7O2D4./.3S16O4./.3S16OS2O./.3S16OS2O./4.3S13OS2O./4.3S13OS2O./4.3S13O4./4.3S13O4./4.3S13O4./4.2S2.2O4.2O2.2O4./3.3S2.2O4.2O2.3O3./3.2S.4O4.2O3.3O2./3.2S.2O6.2O4.2O2./3.2S9.2O8.',
20    ],
21  },
22  looking: {
23    seq: [0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 1, 1, 1, 0, 0, 0],
24    frames: [
25      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
26      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.3O2D8O2DO4./4.3O2D8O2DO4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
27      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.3O2D8O2DO4./4.3O2D8O2DO4./4.16O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
28      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
29      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.O2D8O2D3O4./4.O2D8O2D3O4./4.16O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
30      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.O2D8O2D3O4./4.O2D8O2D3O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
31      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.O2D8O2D3O4./5O2D8O2D7O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
32      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.16O4./5O2D8O2D7O/5O2D8O2D7O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
33      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.16O4./6O2D8O2D6O/6O2D8O2D6O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
34      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.16O4./7O2D8O2D5O/7O2D8O2D5O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
35      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.3O2D8O2DO4./7O2D8O2D5O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
36    ],
37  },
38  turning: {
39    seq: [0, 0, 0, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1],
40    frames: [
41      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
42      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.2O3D6O3D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
43      '24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D6O2DO4./4.3S2O2D6O2DO4./3S20O./3S20O./3S20O./3S20O./4.3S13O4./4.3S13O4./4.3S13O4./4.3S13O4./4.3S13O4./4.2S3.2O2.2O3.2O4./4.3S2.2O2.2O2.3O4./5.2S2.2O2.2O2.2O5./9.2O2.2O9.',
44      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.3S13O4./4.3S13O4./4.3S2O2D6O2DO4./4.3S2O2D6O2DO4./3S20O./3S20O./3S20O./3S20O./4.3S13O4./4.3S13O4./4.3S13O4./4.3S13O4./5.2S2.2O2.2O2.2O5./5.2S2.2O2.2O2.2O5./5.2S2.2O2.2O2.2O5./5.2S2.2O2.2O2.2O5.',
45      '24./24./24./24./24./24./24./24./24./24./24./24./24./3.13O3S5./3.13O3S5./3.O2D6O2D2O3S5./3.O2D6O2D2O3S5./20O3S./20O3S./20O3S./20O3S./3.13O3S5./3.13O3S5./3.13O3S5./3.13O3S5./3.13O3S5./3.2O3.2O2.2O3.2S5./3.3O2.2O2.2O2.3S5./4.2O2.2O2.2O2.2S6./8.2O2.2O10.',
46      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./3.13O3S5./3.13O3S5./3.O2D6O2D2O3S5./3.O2D6O2D2O3S5./20O3S./20O3S./20O3S./20O3S./3.13O3S5./3.13O3S5./3.13O3S5./3.13O3S5./4.2O2.2O2.2O2.2S6./4.2O2.2O2.2O2.2S6./4.2O2.2O2.2O2.2S6./4.2O2.2O2.2O2.2S6.',
47    ],
48  },
49  waving: {
50    seq: [0, 0, 0, 0, 1, 2, 3, 4, 5, 4, 5, 4, 5, 4, 5, 4, 6],
51    frames: [
52      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
53      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2D8O2D2O4./6O2D8O2D6O/24O/24O/24O/4.16O4./4.16O4./4.2S2.2S4.2S2.2S4./4.2O2.2O4.2O2.2O4.',
54      '24./24./24./24./24./24./24./24./24./18.4O2./18.5O./18.3S2O./12.8OS2O./6.14OS3O/4.12O2D2OS3O/4.2O2D8O2D2OS3O/4.2O2D12OS3O/4.16O4./4.16O4./20O4./20O4./20O4./20O4./4.16O4./4.12O2.2O4./4.2O2.4O2.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
55      '24./24./24./24./24./24./24./24./24./24./16.4O4./16.4O4./16.4S4./12.8O4./4.16O4./4.11O2D3O4./4.3O2D5OD2OD2O4./4.2OD2OD10O4./20O4./20O4./20O4./20O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
56      '24./24./24./24./24./24./24./24./24./24./15.4O5./15.4O5./15.4S5./12.8O4./4.16O4./4.11O2D3O4./4.3O2D5OD2OD2O4./4.2OD2OD10O4./4.16O4./20O4./20O4./20O4./20O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
57      '24./24./24./24./24./24./24./24./24./24./16.4O4./16.4O4./16.4S4./12.8O4./4.16O4./4.11O2D3O4./4.3O2D5OD2OD2O4./4.2OD2OD10O4./4.16O4./20O4./20O4./20O4./20O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
58      '24./24./24./24./24./24./24./24./24./24./18.4O2./18.5O./18.3S2O./12.8OS2O./4.16OS3O/4.11O2D3OS3O/4.3O2D5OD2OD2OS3O/4.2OD2OD10OS3O/4.16O4./4.16O4./20O4./20O4./20O4./20O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
59    ],
60  },
61  jumping: {
62    seq: [0, 0, 0, 0, 1, 2, 3, 4, 5, 4, 3, 3, 6, 7, 8, 9, 0, 0, 0, 0, 0],
63    frames: [
64      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./24O/24O/24O/24O/4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
65      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.S15O4./4.16O4./4.16O4./4.16O4./4.2O2D8O2D2O4./6O2D8O2D6O/24O/24O/24O/4.16O4./4.16O4./4.2S2.2S4.2S2.2S4./4.2O2.2O4.2O2.2O4.',
66      '24./24./24./24./24./24./2.4O12.4O2./2.4O12.4O2./2.20O2./2.20O2./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./24./24./24.',
67      '24./5.4O6.4O5./5.4O6.4O5./5.4O6.4O5./5.4O6.4O5./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./24./24./24./24./24./24./24./24./24.',
68      '6.4O4.4O6./6.4O4.4O6./6.4O4.4O6./6.4O4.4O6./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./24./24./24./24./24./24./24./24./24./24.',
69      '5.4O6.4O5./5.4O6.4O5./5.4O6.4O5./5.4O6.4O5./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./24./24./24./24./24./24./24./24./24./24.',
70      '24./24./24./24./24./3.4O10.4O3./3.4O10.4O3./3.4O10.4O3./3.18O3./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.16O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./24./24./24./24./24./24.',
71      '24./24./24./24./24./24./24./24./24./24./24./4.15O5./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./6O2D8O2D6O/6O2D8O2D6O/24O/24O/24O/.22O./2.20O2./4.16O4./4.16O4./4.16O4./3.3O.4O3.3O.3O3./3.2O2.2O6.2O2.2O3./3.3O.3O4.3O.3O3./4.2O2.2O4.2O2.2O4.',
72      '24./24./24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./24O/24O/24O/24O/4.16O4./3.18O3./3.2O2.10O2.2O3./3.3O.3O4.3O.3O3./4.2O2.2O4.2O2.2O4.',
73      '24./24./24./24./24./24./24./24./24./24./24./24./24./4.16O4./4.16O4./4.2O2D8O2D2O4./4.2O2D8O2D2O4./4.16O4./4.16O4./4.16O4./4.16O4./24O/24O/24O/24O/4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4./4.2O2.2O4.2O2.2O4.',
74    ],
75  },
76} as const
77
78export type ClipName = keyof typeof CLIPS
79
hooks/png.ts 111 lines
1// A small PNG encoder for pixel art, since the mod has no zlib. It scales the
2// picture up by a whole number with hard edges, so the terminal hardly
3// stretches it and nothing blurs, then compresses only runs of one byte. The
4// scaled picture is almost all runs, so it shrinks to a few percent.
5
6const CRC = Array.from({ length: 256 }, (_, n) => {
7  let c = n
8  for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
9  return c >>> 0
10})
11const crc32 = (bytes: Uint8Array) => {
12  let c = 0xffffffff
13  for (const b of bytes) c = CRC[(c ^ b) & 0xff]! ^ (c >>> 8)
14  return (c ^ 0xffffffff) >>> 0
15}
16
17const LENGTH_BASE = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258]
18const LENGTH_EXTRA = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0]
19
20// One deflate block with the fixed codes: each byte as a literal, and each
21// run of it after the first as copies of the byte before.
22const deflate = (data: Uint8Array) => {
23  const out: number[] = []
24  let acc = 0
25  let bits = 0
26  const put = (value: number, n: number) => {
27    acc |= value << bits
28    bits += n
29    while (bits >= 8) {
30      out.push(acc & 0xff)
31      acc >>>= 8
32      bits -= 8
33    }
34  }
35  // Codes go most significant bit first, the reverse of everything else.
36  const code = (value: number, n: number) => {
37    let reversed = 0
38    for (let k = 0; k < n; k++) reversed |= ((value >> k) & 1) << (n - 1 - k)
39    put(reversed, n)
40  }
41  const symbol = (s: number) => {
42    if (s < 144) code(0x30 + s, 8)
43    else if (s < 256) code(0x190 + s - 144, 9)
44    else if (s < 280) code(s - 256, 7)
45    else code(0xc0 + s - 280, 8)
46  }
47  put(1, 1)
48  put(1, 2)
49  for (let i = 0; i < data.length; ) {
50    const b = data[i]!
51    symbol(b)
52    let run = 0
53    while (data[i + 1 + run] === b) run++
54    i += 1
55    while (run >= 3) {
56      const length = Math.min(run, 258)
57      let k = LENGTH_BASE.length - 1
58      while (LENGTH_BASE[k]! > length) k--
59      symbol(257 + k)
60      put(length - LENGTH_BASE[k]!, LENGTH_EXTRA[k]!)
61      code(0, 5)
62      run -= length
63      i += length
64    }
65  }
66  symbol(256)
67  if (bits > 0) out.push(acc & 0xff)
68  return out
69}
70
71const adler32 = (data: Uint8Array) => {
72  let a = 1
73  let b = 0
74  for (const byte of data) {
75    a = (a + byte) % 65521
76    b = (b + a) % 65521
77  }
78  return ((b << 16) | a) >>> 0
79}
80
81const u32 = (n: number) => [n >>> 24, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]
82const chunk = (type: string, data: number[]) => {
83  const body = new Uint8Array([...type].map(c => c.charCodeAt(0)).concat(data))
84  return [...u32(data.length), ...body, ...u32(crc32(body))]
85}
86
87// RGBA pixels, `width` by `height`, as a base64 PNG `scale` times larger. Each
88// pixel's first column takes its difference from the one before (the Sub
89// filter) and the rest of its copies are zeros; each repeated row is all zeros
90// against the row above (the Up filter).
91export const encodePng = (rgba: Uint8Array, width: number, height: number, scale: number) => {
92  const wide = width * scale
93  const line = 1 + wide * 3
94  const raw = new Uint8Array(line * height * scale)
95  for (let y = 0; y < height; y++) {
96    const at = y * scale * line
97    raw[at] = 1
98    for (let x = 0; x < width; x++) {
99      for (let c = 0; c < 3; c++) {
100        const before = x > 0 ? rgba[(y * width + x - 1) * 4 + c]! : 0
101        raw[at + 1 + x * scale * 3 + c] = (rgba[(y * width + x) * 4 + c]! - before) & 0xff
102      }
103    }
104    for (let k = 1; k < scale; k++) raw[at + k * line] = 2
105  }
106  const zlib = [0x78, 0x01, ...deflate(raw), ...u32(adler32(raw))]
107  const header = [...u32(wide), ...u32(height * scale), 8, 2, 0, 0, 0]
108  const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, ...chunk('IHDR', header), ...chunk('IDAT', zlib), ...chunk('IEND', [])]
109  return new Uint8Array(png).toBase64()
110}
111
types/index.d.ts 27 lines
1/** The main turn Clawd is working on; null while the session is idle. */
2export type WorkingTurn = string | null
3
4/** One task on Claude's checklist, as its TaskCreate and TaskUpdate calls left it. */
5export type WorkingTask = {
6  subject: string
7  status: 'pending' | 'in_progress' | 'completed'
8  /** What the task reads as while in progress ("Running tests"). */
9  activeForm?: string
10}
11
12/** The checklist, by task id, in the order the tasks were made. */
13export type WorkingTasks = Record<string, WorkingTask>
14
15declare module 'claude-code' {
16  interface PluginState {
17    'working-clawd': {
18      turnId: WorkingTurn
19      /** The subagents running now, one mini Clawd each. */
20      agents: string[]
21      tasks: WorkingTasks
22      /** The main loop's latest commands this turn, oldest first. */
23      commands: string[]
24    }
25  }
26}
27