A pixel Abra that lives at the right edge of the band above the prompt

A Claude Code mod that draws a pixel Abra at the right edge of the band above the prompt. It bobs in place while you're idle and paces back and forth while Claude works.
Raster, so the mod draws nothing there.| Command | Effect |
|---|---|
/abra | Toggle between default and shiny, saved across sessions |
/abra shiny, /abra default | Pick a variant |
| Ctrl+X Ctrl+A | Collapse or expand the band (Claude Code's own binding) |
| Path | Contents |
|---|---|
hooks/register.js | Band renderer, animation timer, /abra command |
hooks/frames.js | Generated pixel frames. Don't edit by hand. |
sprites/*.gif | Source GIFs, 32x32 at 2 frames each |
scripts/build-frames.mjs | Regenerates frames.js from the GIFs with ffmpeg |
tests/abra.test.ts | claude plugin test suite |
node scripts/build-frames.mjs # after changing sprites/
claude plugin validate .
claude plugin test
claude --plugin-dir . # live-reloading session
The sprite is 19x19 pixels. Each terminal cell holds two vertical pixels with ▀/▄ half blocks, so Abra takes 19 columns by 10 rows inside a 40 column strip.
Sprites come from jakobhoeg/vscode-pokemon. Pokémon sprites are © The Pokémon Company / Nintendo / Game Freak, used here for a personal, non-commercial fan project.
hooks/register.js 117 lines1import { SPRITE } from './frames.js'
2
3// Abra lives at the right edge of the band above the prompt. It bobs in place
4// while you're idle and paces back and forth while Claude works.
5
6const TICK_MS = 150
7const FRAME_TICKS = 2
8const STRIP_COLUMNS = 40
9const DEFAULT_COLOR = 0x01000000
10const UPPER_HALF = 0x2580
11const LOWER_HALF = 0x2584
12const SPACE = 0x20
13
14let variant = 'default'
15let working = false
16let bandId = null
17let columns = STRIP_COLUMNS
18let tick = 0
19let x = STRIP_COLUMNS - SPRITE.width
20let facing = 'left'
21
22const rows = Math.ceil(SPRITE.height / 2)
23
24// Palette letter to 0xRRGGBB, per variant
25const COLORS = Object.fromEntries(
26 Object.entries(SPRITE.variants).map(([name, sheet]) => [
27 name,
28 Object.fromEntries(sheet.palette.map((hex, i) => [String.fromCharCode(97 + i), parseInt(hex.slice(1), 16)])),
29 ]),
30)
31
32// Look up a pixel's color in a frame, or null where it's transparent
33function pixelsOf(frame, colors, flip) {
34 return (px, py) => {
35 const row = frame[py]
36 if (!row) return null
37 return colors[row[flip ? SPRITE.width - 1 - px : px]] ?? null
38 }
39}
40
41// Pack the current frame into Raster cells, two pixels per cell with half blocks
42function cellsNow() {
43 const sheet = SPRITE.variants[variant]
44 const anim = working ? sheet.walk : sheet.idle
45 const frame = anim[Math.floor(tick / FRAME_TICKS) % anim.length]
46 const pixel = pixelsOf(frame, COLORS[variant], working && facing === 'left')
47 const words = new Uint32Array(columns * rows * 3)
48 for (let cy = 0; cy < rows; cy++) {
49 for (let cx = 0; cx < columns; cx++) {
50 const px = cx - x
51 const top = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2) : null
52 const bottom = px >= 0 && px < SPRITE.width ? pixel(px, cy * 2 + 1) : null
53 const i = (cy * columns + cx) * 3
54 if (top !== null) words.set([UPPER_HALF, top, bottom ?? DEFAULT_COLOR], i)
55 else if (bottom !== null) words.set([LOWER_HALF, bottom, DEFAULT_COLOR], i)
56 else words.set([SPACE, DEFAULT_COLOR, DEFAULT_COLOR], i)
57 }
58 }
59 return new Uint8Array(words.buffer).toBase64()
60}
61
62// Move one column per tick while working, turning at the strip's edges
63function step() {
64 tick += 1
65 if (!working) return
66 const maxX = columns - SPRITE.width
67 if (facing === 'left' && x <= 0) facing = 'right'
68 else if (facing === 'right' && x >= maxX) facing = 'left'
69 x = Math.max(0, Math.min(maxX, x + (facing === 'left' ? -1 : 1)))
70}
71
72export function register(on) {
73 on('session.start', async ($, e, next) => {
74 await $.command.register({
75 name: 'abra',
76 description: 'Switch Abra between default and shiny',
77 argumentHint: '[default|shiny]',
78 immediate: true,
79 })
80 const saved = await $.store.get('variant')
81 if (saved in SPRITE.variants) variant = saved
82 $.clock.every(TICK_MS, () => {
83 step()
84 if (bandId !== null) $.ui.blit({ requestId: bandId, key: 'abra', columns, rows, cells: cellsNow() })
85 })
86 return next(e)
87 })
88
89 on('command.run', { command: 'abra' }, async ($, e) => {
90 const asked = e.args.trim()
91 variant = asked in SPRITE.variants ? asked : variant === 'shiny' ? 'default' : 'shiny'
92 await $.store.set('variant', variant)
93 $.ui.invalidate('ui.render')
94 return { text: 'Abra is now ' + variant + '.' }
95 })
96
97 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
98 if (e.surface !== 'terminal' || e.props.hasSurvey) {
99 bandId = null
100 return next(e)
101 }
102 const { Box, Raster } = $.ui.resolve(e)
103 working = e.props.isWorking
104 bandId = e.requestId
105 columns = Math.max(SPRITE.width, Math.min(STRIP_COLUMNS, e.props.bodyColumns))
106 x = Math.max(0, Math.min(columns - SPRITE.width, x))
107
108 const abra = Raster({ key: 'abra', columns, rows, cells: cellsNow() })
109 const theirs = await next(e)
110 return Box({
111 flexDirection: 'row',
112 justifyContent: theirs ? 'space-between' : 'flex-end',
113 children: theirs ? [Box({ flexGrow: 1, children: [theirs] }), abra] : [abra],
114 })
115 })
116}
117hooks/frames.js 206 lines1// Generated by scripts/build-frames.mjs from sprites/*.gif. Do not edit by hand.
2// Each frame is rows of palette letters; '.' is transparent.
3
4export const SPRITE = {
5 "width": 19,
6 "height": 19,
7 "variants": {
8 "default": {
9 "palette": [
10 "#000000",
11 "#483818",
12 "#b89828",
13 "#f8d860",
14 "#907860",
15 "#786038"
16 ],
17 "idle": [
18 [
19 "...................",
20 "...................",
21 "........aaaaa......",
22 "....bbbacdddcabbb..",
23 "....bddcdddddcddb..",
24 "....bcdddddddddcb..",
25 ".....bdddddddddb...",
26 "....bcdddddddddcb..",
27 "....acbadddddabca..",
28 "....acddadddaddca..",
29 ".....aadddddddaa...",
30 "....beeaadddaaeeb..",
31 "....affbcaaacbffa..",
32 ".....aaccbfbccaa...",
33 "...bbdcaccbccacdbb.",
34 "...bdccaddaddaccdb.",
35 "....bbccaaaaaccbb..",
36 "......aa.....aa....",
37 "..................."
38 ],
39 [
40 "...................",
41 "........aaaaa......",
42 "....bbbacdddcabbb..",
43 "....bddcdddddcddb..",
44 "....bcdddddddddcb..",
45 ".....bdddddddddb...",
46 "....bcdddddddddcb..",
47 "....acbadddddabca..",
48 "....acddadddaddca..",
49 ".....aadddddddaa...",
50 "....beeaadddaaeeb..",
51 "....affbcaaacbffa..",
52 ".....aaccbfbccaa...",
53 "...bbdcaccbccacdbb.",
54 "...bdccaddaddaccdb.",
55 "....bbccaaaaaccbb..",
56 "......aa.....aa....",
57 "...................",
58 "..................."
59 ]
60 ],
61 "walk": [
62 [
63 "...................",
64 ".........bb........",
65 "........bccbba.....",
66 "........aabcddba...",
67 ".......addcbdddda..",
68 ".......accddddddda.",
69 "........accddddddb.",
70 ".......bcbdddddddda",
71 ".......acddddbadddb",
72 ".......adddddddaddb",
73 ".bb.....accdddddbb.",
74 "bcb....afaaaaaaa...",
75 "bccbbb.afffab......",
76 "bccfccbcaaacbbb....",
77 ".bffcccaffbacaaa...",
78 "..bbcccabbccacda...",
79 "....babbaccccdb....",
80 ".........aabccb....",
81 "............bb....."
82 ],
83 [
84 ".........bb........",
85 "........bccbba.....",
86 "........aabcddba...",
87 ".......addcbdddda..",
88 ".......accddddddda.",
89 "........accddddddb.",
90 ".......bcbdddddddda",
91 "..bb...acddddbadddb",
92 ".bcb...adddddddaddb",
93 ".bcb....accdddddbb.",
94 ".bcfb..afaaaaaaa...",
95 ".affcbbafffab......",
96 "..bcccccaaacbbb....",
97 "..accccaffbacaaa...",
98 "...abccabbccacda...",
99 ".....abbaccccdb....",
100 ".........aabccb....",
101 "............bb.....",
102 "..................."
103 ]
104 ]
105 },
106 "shiny": {
107 "palette": [
108 "#000000",
109 "#604820",
110 "#d8b870",
111 "#f8e0a0",
112 "#a89080",
113 "#a08048"
114 ],
115 "idle": [
116 [
117 "...................",
118 "...................",
119 "........aaaaa......",
120 "....bbbacdddcabbb..",
121 "....bddcdddddcddb..",
122 "....bcdddddddddcb..",
123 ".....bdddddddddb...",
124 "....bcdddddddddcb..",
125 "....acbadddddabca..",
126 "....acddadddaddca..",
127 ".....aadddddddaa...",
128 "....beeaadddaaeeb..",
129 "....affbcaaacbffa..",
130 ".....aaccbfbccaa...",
131 "...bbdcaccbccacdbb.",
132 "...bdccaddaddaccdb.",
133 "....bbccaaaaaccbb..",
134 "......aa.....aa....",
135 "..................."
136 ],
137 [
138 "...................",
139 "........aaaaa......",
140 "....bbbacdddcabbb..",
141 "....bddcdddddcddb..",
142 "....bcdddddddddcb..",
143 ".....bdddddddddb...",
144 "....bcdddddddddcb..",
145 "....acbadddddabca..",
146 "....acddadddaddca..",
147 ".....aadddddddaa...",
148 "....beeaadddaaeeb..",
149 "....affbcaaacbffa..",
150 ".....aaccbfbccaa...",
151 "...bbdcaccbccacdbb.",
152 "...bdccaddaddaccdb.",
153 "....bbccaaaaaccbb..",
154 "......aa.....aa....",
155 "...................",
156 "..................."
157 ]
158 ],
159 "walk": [
160 [
161 "...................",
162 ".........bb........",
163 "........bccbba.....",
164 "........aabcddba...",
165 ".......addcbdddda..",
166 ".......accddddddda.",
167 "........accddddddb.",
168 ".......bcbdddddddda",
169 ".......acddddbadddb",
170 ".......adddddddaddb",
171 ".bb.....accdddddbb.",
172 "bcb....afaaaaaaa...",
173 "bccbbb.afffab......",
174 "bccfccbcaaacbbb....",
175 ".bffcccaffbacaaa...",
176 "..bbcccabbccacda...",
177 "....babbaccccdb....",
178 ".........aabccb....",
179 "............bb....."
180 ],
181 [
182 ".........bb........",
183 "........bccbba.....",
184 "........aabcddba...",
185 ".......addcbdddda..",
186 ".......accddddddda.",
187 "........accddddddb.",
188 ".......bcbdddddddda",
189 "..bb...acddddbadddb",
190 ".bcb...adddddddaddb",
191 ".bcb....accdddddbb.",
192 ".bcfb..afaaaaaaa...",
193 ".affcbbafffab......",
194 "..bcccccaaacbbb....",
195 "..accccaffbacaaa...",
196 "...abccabbccacda...",
197 ".....abbaccccdb....",
198 ".........aabccb....",
199 "............bb.....",
200 "..................."
201 ]
202 ]
203 }
204 }
205}
206