Two pixel cats (bright golden and brown tabby) above the prompt: sitting while idle, walking in place while Claude works.

A Claude Code mod: two pixel cats on a thin ground line, just above the prompt, on the right.
The left cat is a bright golden British Shorthair, the right one a browner tabby.
Type this at the prompt in Claude Code:
/plugin install prompt-cats --marketplace Harvey1311/claude-code-mods-cat
Answer y to add the marketplace, then pick the user scope so the cats appear in every session.
/plugin marketplace update claude-code-mods-cat
hooks/register.tsx 183 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Two pixel cats at the bottom of the band, right-aligned, standing on a thin ground line drawn under their paws.
4// Each cat is 9 x 4 pixels, both facing left. One terminal cell holds two pixels stacked (a half block), so 4
5// pixels make 2 rows; the ground is a third row of upper-eighth blocks (▔), which hug the paws. Pixel codes: '.' see-through, 'O' fur, 'D' darker fur (shading, far legs), 'T' tail tip, 'E' eye, '0'-'6' the belly, lightest
6// to darkest.
7type Sprite = readonly string[]
8
9const SIT: Sprite = ['..O.O....', '..EOE....', '..OOOO...', '.TOOOOO..']
10const BLINK: Sprite = ['..O.O....', '..OOO....', '..OOOO...', '.TOOOOO..']
11// The tail tip lifts off the ground for a beat.
12const FLICK: Sprite = ['..O.O....', '..EOE....', '..OOOO...', 'T.OOOOO..']
13
14const walkFrame = (legs: string, isTailUp: boolean): Sprite => [
15 isTailUp ? 'O.O.....T' : 'O.O......',
16 isTailUp ? 'OOODODOD.' : 'OOODODODT',
17 '.0123456.',
18 legs,
19]
20// A four-step walk cycle: stride, legs passing, the other stride, legs passing; the tail sways every two steps.
21export const WALK: readonly Sprite[] = [
22 walkFrame('O.D..D.O.', false),
23 walkFrame('.OD..OD..', false),
24 walkFrame('D.O..O.D.', true),
25 walkFrame('.DO..DO..', true),
26]
27
28type Palette = { O: number; D: number; T: number; E: number; B: number }
29// Matched to a photo of the two real cats, sampled and lifted for a dark terminal, the left one kept brighter.
30// Left: a British Shorthair, black golden shaded (NY11): warm apricot gold with darker shading on the back. Right: a
31// duller, browner tabby with a dark tail tip (dark brown, as black would vanish into a dark background). As asked,
32// the left one's tail tip is its body gold, and the eyes (green) show only while sitting. Bellies (walking only; a
33// sitting cat is one block of color): a gradient from front to hind, starting at B (the left one white, the right one
34// a light shade of its body color) and darkening evenly to the body color O.
35export const ORANGE: Palette = { O: 0xd9a55c, D: 0x8c6230, T: 0xd9a55c, E: 0x2e9e44, B: 0xffffff }
36export const GREY: Palette = { O: 0xb08a55, D: 0x6e5a3e, T: 0x51473b, E: 0x2e9e44, B: 0xd8c4aa }
37
38const BELLY_STEPS = 7
39const mix = (from: number, to: number, t: number) =>
40 [16, 8, 0].reduce((rgb, shift) => {
41 const a = (from >> shift) & 0xff
42 const b = (to >> shift) & 0xff
43 return rgb | (Math.round(a + (b - a) * t) << shift)
44 }, 0)
45/** The belly's color at step 0 (front, lightest) to BELLY_STEPS - 1 (hind, the body color). */
46export const bellyShade = (palette: Palette, step: number) => mix(palette.B, palette.O, step / (BELLY_STEPS - 1))
47
48const CAT_WIDTH = 9
49const GAP = 2
50// The ground runs this far past the cats on each side.
51const MARGIN = 4
52export const COLUMNS = MARGIN + CAT_WIDTH + GAP + CAT_WIDTH + MARGIN
53const CAT_ROWS = 2
54/** The Raster's rows: the cats, then the ground. */
55export const ROWS = CAT_ROWS + 1
56const GROUND_COLOR = 0x555555
57export const KEY = 'cats'
58const TICK_MS = 160
59const DEFAULT_COLOR = 0x01000000
60
61// macOS Terminal.app only; VS Code draws the cells above unchanged. Terminal.app draws block glyphs from the font, a
62// little low and short, so the top few pixels of every line show only the cell's background. Where a cell has both
63// halves colored, the top half goes in the background and the bottom half is a ▄, which closes that strip. A belly dot
64// with no leg under it (3 and 4 always, 1, 6 and 7 while their leg is lifted) gets UNDER_BELLY in the empty half, a
65// dark color chosen to pass for the window: measured against a see-through "Clear Dark" profile, it can show as a faint block over a
66// brighter backdrop or another profile.
67export const UNDER_BELLY = 0x222233
68const BELLY_DOTS = [1, 2, 3, 4, 5, 6, 7]
69
70// Uint8Array.prototype.toBase64 is in the plugin environment (the Raster docs use it) but not yet in the es2023
71// lib types.
72type Base64Bytes = Uint8Array & { toBase64: () => string }
73
74const walkAt = (step: number): Sprite => WALK[step % WALK.length] ?? SIT
75
76export type Pose = { orange: Sprite; grey: Sprite }
77
78/**
79 * The cats at a tick. Working: both walk, half a cycle apart so they are not in lockstep. Idle: both sit, each
80 * blinking and flicking its tail on its own schedule (at 160 ms a tick, orange blinks every ~5 s, grey ~7 s).
81 */
82export const poseAt = (tick: number, isWorking: boolean): Pose => {
83 if (isWorking) return { orange: walkAt(tick), grey: walkAt(tick + 2) }
84 const orange = tick % 31 === 0 ? BLINK : tick % 53 === 20 || tick % 53 === 21 ? FLICK : SIT
85 const grey = tick % 43 === 7 ? BLINK : tick % 67 === 40 || tick % 67 === 41 ? FLICK : SIT
86 return { orange, grey }
87}
88
89// The Raster columns of the belly dots of both cats.
90const BELLY_COLUMNS = new Set(BELLY_DOTS.flatMap(x => [MARGIN + x, MARGIN + CAT_WIDTH + GAP + x]))
91
92/** The pose as Raster cells: COLUMNS x ROWS cells, each `[codePoint, foreground, background]`, base64. */
93export const encode = (pose: Pose, isAppleTerminal = false): string => {
94 const pixels: (number | null)[][] = Array.from({ length: CAT_ROWS * 2 }, (_, y) => {
95 const color = (sprite: Sprite, palette: Palette, x: number) => {
96 const code = sprite[y]?.[x] ?? '.'
97 if (code === '.') return null
98 // Terminal.app: the belly is solid, in the ears' color (O), instead of the gradient.
99 if (code >= '0' && code <= '9') return isAppleTerminal ? palette.O : bellyShade(palette, Number(code))
100 return palette[code as keyof Palette]
101 }
102 return [
103 ...Array.from({ length: MARGIN }, () => null),
104 ...Array.from({ length: CAT_WIDTH }, (_, x) => color(pose.orange, ORANGE, x)),
105 ...Array.from({ length: GAP }, () => null),
106 ...Array.from({ length: CAT_WIDTH }, (_, x) => color(pose.grey, GREY, x)),
107 ...Array.from({ length: MARGIN }, () => null),
108 ]
109 })
110 const words = new Uint32Array(COLUMNS * ROWS * 3)
111 for (let row = 0; row < CAT_ROWS; row++) {
112 for (let col = 0; col < COLUMNS; col++) {
113 const top = pixels[row * 2]?.[col] ?? null
114 const pixel = pixels[row * 2 + 1]?.[col] ?? null
115 const isUnderBelly = isAppleTerminal && row === 1 && BELLY_COLUMNS.has(col) && top !== null && pixel === null
116 const bottom = isUnderBelly ? UNDER_BELLY : pixel
117 const cell =
118 isAppleTerminal && top !== null && bottom !== null
119 ? [0x2584, bottom, top] // ▄
120 : top !== null
121 ? [0x2580, top, bottom ?? DEFAULT_COLOR] // ▀
122 : bottom !== null
123 ? [0x2584, bottom, DEFAULT_COLOR] // ▄
124 : [0x20, DEFAULT_COLOR, DEFAULT_COLOR]
125 words.set(cell, (row * COLUMNS + col) * 3)
126 }
127 }
128 for (let col = 0; col < COLUMNS; col++) {
129 words.set([0x2594, GROUND_COLOR, DEFAULT_COLOR], (CAT_ROWS * COLUMNS + col) * 3) // ▔
130 }
131 return (new Uint8Array(words.buffer) as Base64Bytes).toBase64()
132}
133
134/** Repaints the mounted cats with new cells. */
135const paint = ($: EngineInterface, requestId: string, cells: string) =>
136 $.ui.blit({ requestId, key: KEY, cells, columns: COLUMNS, rows: ROWS })
137
138export const register: Register = on => {
139 // Animation bookkeeping. A hot reload starts these over, which costs at most one frame.
140 let tick = 0
141 let isWorking = false
142 let band: string | null = null
143 let shown = ''
144 // Unknown until the first draw reads TERM_PROGRAM.
145 let isAppleTerminal: boolean | null = null
146
147 on('session.start', async ($, e, next) => {
148 const started = await next(e)
149 $.clock.every(TICK_MS, () => {
150 tick += 1
151 if (band === null) return
152 const cells = encode(poseAt(tick, isWorking), isAppleTerminal === true)
153 if (cells === shown) return
154 shown = cells
155 void paint($, band, cells)
156 })
157 return started
158 })
159
160 // The band is shared with other mods (Output Artifacts): draw whatever the rest of the chain draws first, then
161 // the cats on a row of their own beneath it, directly above the prompt.
162 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
163 const below = await next(e)
164 if (e.surface !== 'terminal' || e.props.hasSurvey || e.props.bodyColumns < COLUMNS) {
165 band = null
166 return below
167 }
168 isAppleTerminal ??= (await $.env.get('TERM_PROGRAM').catch(() => undefined)) === 'Apple_Terminal'
169 isWorking = e.props.isWorking
170 band = e.requestId
171 shown = encode(poseAt(tick, isWorking), isAppleTerminal)
172 const { Box, Raster } = $.ui.resolve(e)
173 return (
174 <Box flexDirection="column">
175 {below}
176 <Box flexDirection="row" justifyContent="flex-end">
177 <Raster key={KEY} columns={COLUMNS} rows={ROWS} cells={shown} />
178 </Box>
179 </Box>
180 )
181 })
182}
183