Ricky, a pixel-art cat companion for Claude Code: an animated night-sky pane with a task / subagent / activity / files board, a cat in the band above the…

<img src="docs/banner.png" alt="Ricky Pixel Mod" width="100%">
<a href="https://code.claude.com/docs/en/plugins/mods/create"><img src="https://img.shields.io/badge/Claude_Code-mod-8a5cf0?style=for-the-badge&logo=anthropic&logoColor=white" alt="Claude Code mod"></a> <a href=".claude-plugin/plugin.json"><img src="https://img.shields.io/badge/version-0.4.1-f5c542?style=for-the-badge" alt="version"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/code-MIT-b79bff?style=for-the-badge" alt="license"></a> <a href="https://github.com/muxia23/ricky-pixel-mod/stargazers"><img src="https://img.shields.io/github/stars/muxia23/ricky-pixel-mod?style=for-the-badge&color=2a1650&logo=github" alt="stars"></a>
<a href="#-快速上手"><b>快速上手</b></a> · <a href="#-功能"><b>功能</b></a> · <a href="#-表情"><b>表情</b></a> · <a href="#%EF%B8%8F-选项"><b>选项</b></a> · <a href="#-常见问题"><b>常见问题</b></a> · <a href="README.en.md"><b>English</b></a>
<img src="docs/hero-zh.gif" alt="Ricky in Claude Code" width="100%">
https://github.com/user-attachments/assets/81b3f878-c860-4d3a-ad5b-624ac57eeeb6
Ricky 在对话旁边闪烁的星空里飞,跟着 Claude 的状态变表情:思考时闭眼、额头星星发光,调用工具时扇翅膀撒星尘,完成时眯眼笑,出错时炸毛。星空下面是一块看板:任务进度、运行中的子 agent、最近的工具调用,以及你改过的文件。
1. 克隆
git clone https://github.com/muxia23/ricky-pixel-mod ~/ricky-pixel-mod
2. 加载:只在本次会话加载:
claude --plugin-dir ~/ricky-pixel-mod
或者每次启动都加载,在 ~/.claude/settings.json 里加:
{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/ricky-pixel-mod" } }
3. 切到全屏渲染:在 Claude Code 里输入:
/tui fullscreen
[!IMPORTANT] 侧边面板只有在全屏渲染下、终端宽度 ≥ 110 列时才会停靠在对话右侧;默认渲染模式下它会显示在输入框上方。这个设置会被记住,只需要切一次;想切回来用
/tui default。
4. 打个招呼:终端够宽时面板会自动打开,也可以随时输入 /ricky。
[!NOTE] 需要 Claude Code 2.1.287+(支持 mods),终端需要支持真彩色(Ghostty、iTerm2、WezTerm、Kitty、VS Code 等)。
<table> <tr> <td width="50%" valign="top">
横向铺满面板的像素星空,星星闪烁,金色月牙,32×32 的 Ricky 会上下浮动、眨眼、飞行。
</td> <td width="50%" valign="top">
任务进度条 · 运行中的子 agent 和它正在做什么 · 最近 6 次工具调用(✓ / ✗)· 改动文件。没有内容的块自动隐藏。
</td> </tr> <tr> <td width="50%" valign="top">
小号 Ricky,Claude 工作时拖着星尘来回飞。它会根据剩余空间自动选大小:24 像素、16 像素,或者只露一个 8 像素的头,窗口再小也不会被截断。
</td> <td width="50%" valign="top">
加载提示变成「Ricky 摘星中…」,每个回合结束显示「✦ Ricky 陪你施法了 12s」。支持中英文。
</td> </tr> </table>
<img src="docs/frames-32.png" alt="Ricky 的表情帧" width="100%">
| Claude 在… | Ricky… |
|---|---|
| 空闲 | 悬停,慢慢扇翅膀,偶尔眨眼 |
| 思考 | 闭眼,额头星星发光 |
| 调用工具 | 快速扇翅膀,撒星尘 |
| 完成 | 眯眼笑 |
| 出错 | 瞪眼炸毛 |
在 /config 里找到 ricky-pixel-mod 下的选项,或者在 ~/.claude/settings.json 里设置:
{ "pluginConfigs": { "ricky-pixel-mod": { "options": { "language": "zh", "bandSize": "small" } } } }
| 选项 | 可选值 |
|---|---|
language | en(默认)English · zh 简体中文 |
bandSize | auto(默认)自动选能放下的最大尺寸 · large / medium / small 最大 24 / 16 / 8 像素 · off 关闭横栏 |
colors | auto(默认)跟随 Claude Code 主题,深色主题用更亮的配色;主题是 Auto (match terminal) 时读取 macOS 的深浅色设置 · light / dark 固定用浅色 / 深色背景的配色 |
Ricky 就是一段纯文本:tools/sprites.py 里每一帧只画左半边,自动镜像,一个字母就是一个像素。改几个字母,然后:
python3 tools/sprites.py # 需要 Pillow → 生成 hooks/sprites.ts 和 docs/ 预览图
python3 tools/banner.py # 重画封面
用 --plugin-dir 加载时,保存文件后 Claude Code 会自动热重载。
输入 <code>/tui fullscreen</code>,并把终端拉宽到至少 110 列。宽度不到 144 列时面板不会自动打开,用 <code>/ricky</code> 打开。
终端需要支持真彩色(24 位色),大多数现代终端都支持。
默认已经会根据空间自动缩小。想一直保持小号,在 <code>/config</code> 里把 <code>bandSize</code> 设成 <code>small</code>(或 <code>off</code> 关闭);只想临时收起,按 <code>ctrl+x ctrl+a</code>。
Ricky 默认跟随 Claude Code 的主题。如果终端是深色背景、主题却是 <code>light</code>,在 <code>/config</code> 里把 Theme 改成 <i>Auto (match terminal)</i> 或 <i>Dark mode</i>;也可以把 <code>colors</code> 直接设成 <code>dark</code>。
<code>/clear</code> 开启新对话时会一起清空。
claude plugin validate . # 查看引擎看到了什么、会拒绝什么
claude plugin test . # 运行 hooks/*.test.tsx 里的测试
tools/sprites.py、hooks/sprites.ts 中的像素数据和 docs/ 中的图片):灵感来自 VALORANT 的一款皮肤角色,属于同人作品,不在 MIT 授权范围内,仅可按 Riot Games 粉丝内容政策非商业使用。详见 LICENSE。<sub>Ricky Pixel Mod was created under Riot Games' "Legal Jibber Jabber" policy using assets owned by Riot Games. Riot Games does not endorse or sponsor this project.</sub>
如果 Ricky 让你的终端变得可爱了一点,点个 ⭐ Ricky 会很开心。
hooks/register.tsx 425 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { RickyActivity, RickyAgent, RickyFile, RickyMood, RickyTodo } from '../types'
5import {
6 ACTIVITY_MAX,
7 applyTodoWrite,
8 clock,
9 detailOf,
10 EDIT_TOOLS,
11 formatElapsed,
12 noteEdit,
13 progressBar,
14 TASK_TOOLS,
15 toolLabel,
16} from './board'
17import { stringsFor } from './i18n'
18import { BAND_ROWS, bandScene, bandSizeFor, paneScene } from './scene'
19import type { BandSize } from './scene'
20
21const PANE = 'ricky'
22const PANE_ROWS = 18
23const TICK_MS = 200
24// text colours for a light and a dark background; `lavender` is the progress bar's empty part
25const LIGHT = { gold: '#c99a12', purple: '#8a5cf0', lavender: '#e4dcff', rose: '#d0457a' }
26const DARK = { gold: '#f5c542', purple: '#a98bff', lavender: '#3d3263', rose: '#ff6b8b' }
27
28const moodAtom = atom({ plugin: 'ricky-pixel-mod', key: 'mood' } as const, 'idle')
29const todosAtom = atom({ plugin: 'ricky-pixel-mod', key: 'todos' } as const, [])
30const activityAtom = atom({ plugin: 'ricky-pixel-mod', key: 'activity' } as const, [])
31const agentsAtom = atom({ plugin: 'ricky-pixel-mod', key: 'agents' } as const, [])
32const filesAtom = atom({ plugin: 'ricky-pixel-mod', key: 'files' } as const, [])
33
34const pick = <T,>(list: T[]) => list[Math.floor(Math.random() * list.length)] as T
35
36// What the timer needs synchronously. Anything a drawing reads lives in $.state.
37// A reload re-runs this module, starting over.
38const live = {
39 tick: 0,
40 mood: 'idle' as RickyMood,
41 inTurn: false,
42 toolsRunning: 0,
43 revert: undefined as { cancel: () => void } | undefined,
44 spinnerWord: 'Ricky',
45 cwd: '',
46 hasClock: false, // an in-progress task or a running agent shows a ticking time
47 pane: { cols: 0, isLive: false },
48 band: undefined as { requestId: string; cols: number; size: BandSize; isLive: boolean } | undefined,
49 isDark: false,
50}
51
52/** Whether Claude Code's theme sits on a dark background; `auto` (match terminal) asks macOS, else assumes dark. */
53async function isDarkTheme($: EngineInterface, theme: unknown): Promise<boolean> {
54 if (typeof theme === 'string' && theme.startsWith('dark')) return true
55 if (typeof theme === 'string' && theme.startsWith('light')) return false
56 try {
57 // macOS: "Dark" in dark mode; the key is missing (non-zero exit) in light mode
58 const { exitCode, stdout } = await $.process.run(['defaults', 'read', '-g', 'AppleInterfaceStyle'], { timeoutMs: 2000 })
59 return exitCode === 0 && stdout.includes('Dark')
60 } catch {
61 // not macOS
62 return true
63 }
64}
65
66async function setMood($: EngineInterface, next: RickyMood, holdMs?: number) {
67 live.revert?.cancel()
68 live.revert = undefined
69 live.mood = next
70 await update($, moodAtom, () => next)
71 if (holdMs) {
72 live.revert = $.clock.after(holdMs, () => {
73 void setMood($, live.inTurn ? (live.toolsRunning > 0 ? 'fly' : 'think') : 'idle')
74 })
75 }
76}
77
78async function refreshClockFlag($: EngineInterface) {
79 const todos = (await read($, todosAtom)) as RickyTodo[]
80 const agents = (await read($, agentsAtom)) as RickyAgent[]
81 live.hasClock = agents.length > 0 || todos.some(t => t.status === 'in_progress')
82}
83
84/** Record what a finished task tool did to the task list. */
85async function applyTaskTool($: EngineInterface, tool: string, input: Record<string, unknown>, result: unknown) {
86 const now = await $.clock.now()
87 if (tool === 'TodoWrite' && Array.isArray(input.todos)) {
88 const todos = input.todos as Parameters<typeof applyTodoWrite>[1]
89 await update($, todosAtom, prev => applyTodoWrite((prev ?? []) as RickyTodo[], todos, now))
90 } else if (tool === 'TaskCreate') {
91 const id = (result as { task?: { id?: string } } | undefined)?.task?.id
92 const content = String(input.subject ?? '')
93 await update($, todosAtom, prev => [...((prev ?? []) as RickyTodo[]), { ...(id ? { id } : {}), content, status: 'pending' as const }])
94 } else if (tool === 'TaskUpdate') {
95 const id = String(input.taskId ?? '')
96 const status = input.status as string | undefined
97 await update($, todosAtom, prev =>
98 ((prev ?? []) as RickyTodo[])
99 .filter(t => !(t.id === id && status === 'deleted'))
100 .map(t => {
101 if (t.id !== id) return t
102 const next: RickyTodo = { ...t, ...(typeof input.subject === 'string' ? { content: input.subject } : {}) }
103 if (status === 'pending' || status === 'in_progress' || status === 'completed') next.status = status
104 if (next.status === 'in_progress') {
105 next.startedAt = t.status === 'in_progress' && t.startedAt ? t.startedAt : now
106 if (typeof input.activeForm === 'string') next.label = input.activeForm
107 } else {
108 delete next.startedAt
109 delete next.label
110 }
111 return next
112 }),
113 )
114 }
115 await refreshClockFlag($)
116}
117
118export const register: Register = (on, options) => {
119 const t = stringsFor(options.language)
120 // the band's largest Ricky; `auto` and `large` start at 24 and step down to fit
121 const bandPref = options.bandSize
122 const preferredBand: BandSize | undefined =
123 bandPref === 'off' ? undefined : bandPref === 'small' ? 8 : bandPref === 'medium' ? 16 : 24
124 live.spinnerWord = t.spinnerWords[0] ?? 'Ricky'
125 // `auto` follows Claude Code's theme; `light` / `dark` pin the colours
126 const colorMode = options.colors === 'light' || options.colors === 'dark' ? options.colors : 'auto'
127 live.isDark = colorMode === 'dark'
128 const c = () => (live.isDark ? DARK : LIGHT)
129
130 on('session.start', async ($, e, next) => {
131 live.mood = (await read($, moodAtom)) as RickyMood
132 live.cwd = await $.session.cwd()
133 await refreshClockFlag($)
134 if (colorMode === 'auto') {
135 try {
136 const theme = (await $.config.list()).find(row => row.key === 'theme')?.value
137 live.isDark = await isDarkTheme($, theme)
138 } catch {
139 // no theme row: keep the light colours
140 }
141 }
142 await $.command.register({ name: 'ricky', description: t.commandDescription })
143 void $.ui.open({ id: PANE, title: '✦ Ricky' })
144
145 $.clock.every(TICK_MS, () => {
146 live.tick += 1
147 if (live.pane.isLive && live.pane.cols > 0) {
148 void $.ui
149 .blit({ requestId: PANE, key: 'sky', cells: paneScene(live.pane.cols, PANE_ROWS, live.mood, live.tick) })
150 .then(r => {
151 if (r.deny) live.pane.isLive = false
152 })
153 // keep the elapsed times ticking, once a second
154 if (live.hasClock && live.tick % 5 === 0) $.ui.invalidate('ui.render')
155 }
156 const b = live.band
157 if (b?.isLive) {
158 void $.ui
159 .blit({ requestId: b.requestId, key: 'ribbon', cells: bandScene(b.cols, live.mood, live.tick, b.size) })
160 .then(r => {
161 if (r.deny) b.isLive = false
162 })
163 }
164 })
165
166 return next(e)
167 })
168
169 on('session.end', async ($, e, next) => {
170 if (e.reason === 'clear') {
171 await update($, todosAtom, () => [])
172 await update($, activityAtom, () => [])
173 await update($, agentsAtom, () => [])
174 await update($, filesAtom, () => [])
175 live.hasClock = false
176 }
177 return next(e)
178 })
179
180 on('config.set', { key: 'theme' }, async ($, e, next) => {
181 const set = await next(e)
182 if (colorMode === 'auto' && 'value' in set) {
183 live.isDark = await isDarkTheme($, set.value)
184 $.ui.invalidate('ui.render')
185 }
186 return set
187 })
188
189 on('command.run', { command: 'ricky' }, async $ => {
190 await $.ui.open({ id: PANE, title: '✦ Ricky' })
191 return { text: t.commandReply }
192 })
193
194 on('turn.start', async ($, e, next) => {
195 live.inTurn = true
196 live.spinnerWord = pick(t.spinnerWords)
197 await setMood($, 'think')
198 return next(e)
199 })
200
201 on('tool.call', async ($, e, next) => {
202 const now = await $.clock.now()
203 const isMain = !e.agentId
204 const input = e as unknown as Record<string, unknown> // a tool's own fields sit on the event
205 const id = e.tool_use_id ?? `${e.tool}-${now}`
206 const detail = detailOf(e.tool, input, live.cwd)
207 const isLogged = isMain && !TASK_TOOLS.has(e.tool)
208
209 live.toolsRunning += 1
210 if (live.mood !== 'shock') await setMood($, 'fly')
211
212 if (isLogged) {
213 const entry: RickyActivity = { id, at: now, tool: toolLabel(e.tool), detail, status: 'running' }
214 await update($, activityAtom, list => [...((list ?? []) as RickyActivity[]), entry].slice(-ACTIVITY_MAX))
215 }
216 if (isMain && e.tool === 'Agent') {
217 const card: RickyAgent = {
218 id,
219 type: String(input.subagent_type ?? 'agent'),
220 description: String(input.description ?? ''),
221 startedAt: now,
222 calls: 0,
223 }
224 await update($, agentsAtom, list => [...((list ?? []) as RickyAgent[]), card])
225 live.hasClock = true
226 }
227 if (!isMain) {
228 // a subagent's call: pin its loop id to the oldest card without one
229 await update($, agentsAtom, list => {
230 const cards = (list ?? []) as RickyAgent[]
231 const owner = cards.find(c => c.agentId === e.agentId) ?? cards.find(c => !c.agentId)
232 if (!owner) return cards
233 return cards.map(c =>
234 c === owner ? { ...c, agentId: e.agentId, calls: c.calls + 1, current: { tool: toolLabel(e.tool), detail } } : c,
235 )
236 })
237 }
238
239 const ran = await next(e)
240
241 live.toolsRunning = Math.max(0, live.toolsRunning - 1)
242 if (isLogged) {
243 const status = ran.isError ? 'err' : 'ok'
244 await update($, activityAtom, list =>
245 ((list ?? []) as RickyActivity[]).map(a => (a.id === id ? { ...a, status } : a)),
246 )
247 }
248 if (isMain && e.tool === 'Agent') {
249 // a background subagent is still running: keep its card until its own turn completes
250 const launched = ('result' in ran ? ran.result : undefined) as { status?: string; agentId?: string } | undefined
251 const bgId = !ran.isError && launched?.status === 'async_launched' ? launched.agentId : undefined
252 await update($, agentsAtom, list => {
253 const cards = (list ?? []) as RickyAgent[]
254 return bgId ? cards.map(c => (c.id === id ? { ...c, agentId: bgId } : c)) : cards.filter(c => c.id !== id)
255 })
256 await refreshClockFlag($)
257 }
258 if (!ran.isError && TASK_TOOLS.has(e.tool)) await applyTaskTool($, e.tool, input, 'result' in ran ? ran.result : undefined)
259 if (!ran.isError && EDIT_TOOLS.has(e.tool) && detail) {
260 const isNew = e.tool === 'Write' && (ran as { result?: { type?: string } }).result?.type === 'create'
261 await update($, filesAtom, list => noteEdit((list ?? []) as RickyFile[], detail, isNew))
262 }
263
264 if (ran.isError) await setMood($, 'shock', 2500)
265 else if (live.toolsRunning === 0 && live.mood !== 'shock') await setMood($, 'think')
266 return ran
267 })
268
269 on('turn.complete', async ($, e, next) => {
270 if (e.agentId) {
271 // a subagent finished: drop its card (a background one outlives its Agent call)
272 await update($, agentsAtom, list => ((list ?? []) as RickyAgent[]).filter(c => c.agentId !== e.agentId))
273 await refreshClockFlag($)
274 return next(e)
275 }
276 live.inTurn = false
277 live.toolsRunning = 0
278 if (e.reason === 'answer') await setMood($, 'happy', 4000)
279 else if (e.reason === 'error') await setMood($, 'shock', 3000)
280 else await setMood($, 'idle')
281 return next(e)
282 })
283
284 on('ui.close', async ($, e, next) => {
285 if (e.id === PANE) live.pane.isLive = false
286 return next(e)
287 })
288
289 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
290 if (e.surface !== 'terminal') return next(e)
291 const { Box, Raster, Text } = $.ui.resolve(e)
292 const cols = Math.max(34, e.props.bodyColumns)
293 live.pane = { cols, isLive: true }
294 const m = (await read($, moodAtom)) as RickyMood
295 const todos = (await read($, todosAtom)) as RickyTodo[]
296 const activity = (await read($, activityAtom)) as RickyActivity[]
297 const agents = (await read($, agentsAtom)) as RickyAgent[]
298 const files = (await read($, filesAtom)) as RickyFile[]
299 const now = await $.clock.now()
300 const { gold: GOLD, purple: PURPLE, lavender: LAVENDER, rose: ROSE } = c()
301
302 const done = todos.filter(t => t.status === 'completed').length
303 const bar = progressBar(done, todos.length, Math.max(10, Math.min(26, cols - 24)))
304 const pct = todos.length ? Math.round((done / todos.length) * 100) : 0
305
306 return (
307 <Box flexDirection="column">
308 <Raster key="sky" columns={cols} rows={PANE_ROWS} cells={paneScene(cols, PANE_ROWS, m, live.tick)} />
309
310 {todos.length > 0 && (
311 <Box flexDirection="column" marginTop={1} paddingX={1}>
312 <Text>
313 <Text color={GOLD} bold>{t.tasks}</Text>
314 {` ${done}/${todos.length} `}
315 <Text color={PURPLE}>{bar.on}</Text>
316 <Text color={LAVENDER}>{bar.off}</Text>
317 {` ${pct}%`}
318 </Text>
319 {todos.map(t =>
320 t.status === 'completed' ? (
321 <Text wrap="truncate-end">
322 {' '}
323 <Text color={PURPLE}>✓</Text> <Text dimColor>{t.content}</Text>
324 </Text>
325 ) : t.status === 'in_progress' ? (
326 <Text wrap="truncate-end">
327 {' '}
328 <Text color={GOLD}>◆ {t.label ?? t.content}</Text>
329 {t.startedAt ? <Text dimColor>{` ${formatElapsed(now - t.startedAt)}`}</Text> : null}
330 </Text>
331 ) : (
332 <Text dimColor wrap="truncate-end">
333 {' ○ '}
334 {t.content}
335 </Text>
336 ),
337 )}
338 </Box>
339 )}
340
341 {agents.length > 0 && (
342 <Box flexDirection="column" marginTop={1} paddingX={1}>
343 <Text>
344 <Text color={GOLD} bold>{t.subagents}</Text>
345 <Text dimColor>{` ${t.running(agents.length)}`}</Text>
346 </Text>
347 {agents.map(a => (
348 <Box flexDirection="column" marginLeft={1} paddingLeft={1} borderStyle="single" borderColor={PURPLE}>
349 <Text wrap="truncate-end">
350 <Text color={PURPLE} bold>{a.type}</Text> {a.description}
351 <Text dimColor>{` ${formatElapsed(now - a.startedAt)} · ${t.calls(a.calls)}`}</Text>
352 </Text>
353 {a.current && (
354 <Text wrap="truncate-end">
355 <Text color={GOLD}>›</Text> {a.current.tool} <Text dimColor>{a.current.detail}</Text>
356 </Text>
357 )}
358 </Box>
359 ))}
360 </Box>
361 )}
362
363 {activity.length > 0 && (
364 <Box flexDirection="column" marginTop={1} paddingX={1}>
365 <Text color={GOLD} bold>{t.recent}</Text>
366 {[...activity].reverse().map(a => (
367 <Text wrap="truncate-end">
368 <Text dimColor>{clock(a.at)}</Text>{' '}
369 {a.status === 'running' ? (
370 <Text color={GOLD}>›</Text>
371 ) : a.status === 'ok' ? (
372 <Text color={PURPLE}>✓</Text>
373 ) : (
374 <Text color={ROSE}>✗</Text>
375 )}{' '}
376 {a.tool.padEnd(6)} <Text dimColor>{a.detail}</Text>
377 </Text>
378 ))}
379 </Box>
380 )}
381
382 {files.length > 0 && (
383 <Box flexDirection="column" marginTop={1} paddingX={1}>
384 <Text>
385 <Text color={GOLD} bold>{t.filesChanged}</Text>
386 <Text dimColor>{` ${files.length}`}</Text>
387 </Text>
388 {files.map(f => (
389 <Text wrap="truncate-start">
390 {' '}
391 {f.path}{' '}
392 {f.isNew && f.edits === 1 ? <Text color={PURPLE}>{t.newFile}</Text> : <Text dimColor>{t.edits(f.edits)}</Text>}
393 </Text>
394 ))}
395 </Box>
396 )}
397 </Box>
398 )
399 })
400
401 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
402 if (e.props.hasSurvey || e.surface !== 'terminal') return next(e)
403 const { Raster } = $.ui.resolve(e)
404 const size = preferredBand && bandSizeFor(preferredBand, e.props.maxRows)
405 if (!size) {
406 live.band = undefined
407 return next(e)
408 }
409 const cols = Math.max(size + 2, e.props.bodyColumns - 2)
410 live.band = { requestId: e.requestId, cols, size, isLive: true }
411 const m = (await read($, moodAtom)) as RickyMood
412
413 return <Raster key="ribbon" columns={cols} rows={BAND_ROWS[size]} cells={bandScene(cols, m, live.tick, size)} />
414 })
415
416 on('ui.render', { component: 'Spinner' }, ($, e, next) =>
417 next({ ...e, props: { ...e.props, word: live.spinnerWord, suffix: ' ✦' } }),
418 )
419
420 on('ui.render', { component: 'TurnDuration' }, async ($, e) => {
421 const { Text } = $.ui.resolve(e)
422 return <Text color={c().gold}>{t.turnDone(formatElapsed(e.props.durationMs))}</Text>
423 })
424}
425hooks/board.ts 92 lines1// Pure helpers for the dashboard under the sky: no `$`, so they are testable.
2import type { RickyFile, RickyTodo } from '../types'
3
4export const ACTIVITY_MAX = 6
5// tools whose calls the task list already shows
6export const TASK_TOOLS = new Set(['TodoWrite', 'TaskCreate', 'TaskUpdate', 'TaskList', 'TaskGet'])
7export const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'])
8
9const str = (v: unknown) => (typeof v === 'string' ? v : '')
10
11/** A path relative to `cwd` when inside it, else with $HOME shortened. */
12export function relPath(path: string, cwd: string, home = ''): string {
13 if (cwd && path.startsWith(cwd + '/')) return path.slice(cwd.length + 1)
14 if (home && path.startsWith(home + '/')) return '~' + path.slice(home.length)
15 return path
16}
17
18/** One short line saying what a tool call works on. */
19export function detailOf(tool: string, input: unknown, cwd: string): string {
20 const i = (input ?? {}) as Record<string, unknown>
21 switch (tool) {
22 case 'Bash':
23 return str(i.command).split('\n')[0] ?? ''
24 case 'Read':
25 case 'Edit':
26 case 'Write':
27 return relPath(str(i.file_path), cwd)
28 case 'NotebookEdit':
29 return relPath(str(i.notebook_path), cwd)
30 case 'Grep':
31 return `"${str(i.pattern)}"${i.path ? ' ' + relPath(str(i.path), cwd) : ''}`
32 case 'Glob':
33 return str(i.pattern)
34 case 'WebFetch':
35 return str(i.url).replace(/^https?:\/\//, '')
36 case 'WebSearch':
37 return str(i.query)
38 case 'Agent':
39 return str(i.description)
40 case 'Skill':
41 return str(i.skill)
42 default:
43 return ''
44 }
45}
46
47/** Shorter names for MCP tools: mcp__server__tool -> server·tool */
48export function toolLabel(tool: string): string {
49 const m = /^mcp__(.+?)__(.+)$/.exec(tool)
50 return m ? `${m[1]}·${m[2]}` : tool
51}
52
53/** TodoWrite replaces the list; keep each in-progress item's start time. */
54export function applyTodoWrite(
55 prev: RickyTodo[],
56 todos: { content: string; status: RickyTodo['status']; activeForm?: string }[],
57 now: number,
58): RickyTodo[] {
59 return todos.map(t => {
60 const todo: RickyTodo = { content: t.content, status: t.status }
61 if (t.status === 'in_progress') {
62 const before = prev.find(p => p.content === t.content)
63 todo.startedAt = before?.status === 'in_progress' && before.startedAt ? before.startedAt : now
64 if (t.activeForm) todo.label = t.activeForm
65 }
66 return todo
67 })
68}
69
70export function noteEdit(files: RickyFile[], path: string, isNew: boolean): RickyFile[] {
71 const hit = files.find(f => f.path === path)
72 if (hit) return files.map(f => (f.path === path ? { ...f, edits: f.edits + 1 } : f))
73 return [...files, { path, edits: 1, isNew }]
74}
75
76export function formatElapsed(ms: number): string {
77 const s = Math.max(0, Math.round(ms / 1000))
78 if (s < 60) return `${s}s`
79 const m = Math.floor(s / 60)
80 return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`
81}
82
83export function clock(at: number): string {
84 const d = new Date(at)
85 return [d.getHours(), d.getMinutes(), d.getSeconds()].map(n => String(n).padStart(2, '0')).join(':')
86}
87
88export function progressBar(done: number, total: number, width: number): { on: string; off: string } {
89 const n = total === 0 ? 0 : Math.round((done / total) * width)
90 return { on: '█'.repeat(n), off: '█'.repeat(width - n) }
91}
92hooks/i18n.ts 53 lines1// Every word Ricky shows, in each language the `language` option offers.
2
3export type Language = 'en' | 'zh'
4
5export type Strings = {
6 spinnerWords: string[]
7 turnDone: (elapsed: string) => string
8 commandDescription: string
9 commandReply: string
10 tasks: string
11 subagents: string
12 running: (n: number) => string
13 calls: (n: number) => string
14 recent: string
15 filesChanged: string
16 newFile: string
17 edits: (n: number) => string
18}
19
20const EN: Strings = {
21 spinnerWords: ['Ricky is casting', 'Ricky is stargazing', 'Ricky is chasing the moon', 'Ricky is weaving dreams', 'Ricky is reading the stars'],
22 turnDone: elapsed => `✦ Ricky cast with you for ${elapsed}`,
23 commandDescription: "Open Ricky's pixel pane",
24 commandReply: 'Ricky is here ✦',
25 tasks: '✦ Tasks',
26 subagents: '✦ Subagents',
27 running: n => `${n} running`,
28 calls: n => `${n} call${n === 1 ? '' : 's'}`,
29 recent: '✦ Recent',
30 filesChanged: '✦ Files changed',
31 newFile: 'new',
32 edits: n => `${n} edit${n === 1 ? '' : 's'}`,
33}
34
35const ZH: Strings = {
36 spinnerWords: ['Ricky 施法中', 'Ricky 摘星中', 'Ricky 追月中', 'Ricky 织梦中', 'Ricky 占星中'],
37 turnDone: elapsed => `✦ Ricky 陪你施法了 ${elapsed}`,
38 commandDescription: '打开 Ricky 的像素面板',
39 commandReply: 'Ricky 来啦 ✦',
40 tasks: '✦ 任务',
41 subagents: '✦ 子 agent',
42 running: n => `${n} 个运行中`,
43 calls: n => `${n} 次调用`,
44 recent: '✦ 最近动作',
45 filesChanged: '✦ 改动文件',
46 newFile: '新建',
47 edits: n => `改 ${n} 次`,
48}
49
50export function stringsFor(language: unknown): Strings {
51 return language === 'zh' ? ZH : EN
52}
53hooks/scene.ts 195 lines1// Pure pixel drawing: no `$` here, so it is testable and cheap to call per tick.
2import { FRAMES, FRAMES8, FRAMES16, FRAMES24, PALETTE, SPRITE_H, SPRITE_W } from './sprites'
3import type { FrameName } from './sprites'
4import type { RickyMood } from '../types'
5
6const DEFAULT = 0x01000000 // the terminal's own colour
7const HALF_UPPER = 0x2580 // ▀
8const HALF_LOWER = 0x2584 // ▄
9
10// palette chars -> colours, once
11const DECODED = {} as Record<FrameName, (number | null)[][]>
12for (const name of Object.keys(FRAMES) as FrameName[]) {
13 DECODED[name] = FRAMES[name].map(row => [...row].map(ch => PALETTE[ch] ?? null))
14}
15// the band's smaller Rickys, same frame names: 24x24, 16x16, and an 8x6 head
16export type BandSize = 24 | 16 | 8
17const decode = (table: Record<FrameName, readonly string[]>) => {
18 const out = {} as Record<FrameName, (number | null)[][]>
19 for (const name of Object.keys(table) as FrameName[]) {
20 out[name] = table[name].map(row => [...row].map(ch => PALETTE[ch] ?? null))
21 }
22 return out
23}
24const BAND_FRAMES: Record<BandSize, Record<FrameName, (number | null)[][]>> = {
25 24: decode(FRAMES24),
26 16: decode(FRAMES16),
27 8: decode(FRAMES8),
28}
29/** Terminal rows each band size takes (two pixels per row). */
30export const BAND_ROWS: Record<BandSize, number> = { 24: 12, 16: 8, 8: 3 }
31
32/** The biggest size no bigger than `preferred` that fits `maxRows`, or none. */
33export function bandSizeFor(preferred: BandSize, maxRows: number): BandSize | undefined {
34 return ([24, 16, 8] as const).find(s => s <= preferred && BAND_ROWS[s] <= maxRows)
35}
36
37const GOLD = 0xf5c542
38const STAR_COLORS = [0xffe89a, 0xf4f0ff, 0xb79bff]
39
40/** Which sprite frame a mood shows on a tick. */
41export function frameFor(mood: RickyMood, tick: number): FrameName {
42 switch (mood) {
43 case 'think':
44 return Math.floor(tick / 3) % 2 === 0 ? 'think' : 'think2'
45 case 'fly':
46 return tick % 2 === 0 ? 'idle' : 'idle2'
47 case 'happy':
48 return Math.floor(tick / 2) % 2 === 0 ? 'happy' : 'happy2'
49 case 'shock':
50 return 'shock'
51 default:
52 if (tick % 30 === 0) return 'blink'
53 return Math.floor(tick / 4) % 2 === 0 ? 'idle' : 'idle2'
54 }
55}
56
57/** A pixel canvas, `null` meaning see-through. */
58type Canvas = { w: number; h: number; px: (number | null)[] }
59
60function canvas(w: number, h: number, fill: number | null): Canvas {
61 return { w, h, px: new Array(w * h).fill(fill) }
62}
63
64function put(c: Canvas, x: number, y: number, color: number | null | undefined) {
65 if (color == null || x < 0 || y < 0 || x >= c.w || y >= c.h) return
66 c.px[y * c.w + x] = color
67}
68
69function stamp(c: Canvas, img: (number | null)[][], x0: number, y0: number) {
70 img.forEach((row, y) => row.forEach((color, x) => put(c, x0 + x, y0 + y, color)))
71}
72
73const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
74
75/** Standard padded base64; the environment's own may be missing. */
76export function base64(bytes: Uint8Array): string {
77 let out = ''
78 for (let i = 0; i < bytes.length; i += 3) {
79 const a = bytes[i] ?? 0
80 const b = bytes[i + 1] ?? 0
81 const c = bytes[i + 2] ?? 0
82 const n = (a << 16) | (b << 8) | c
83 out += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]!
84 out += i + 1 < bytes.length ? B64[(n >> 6) & 63]! : '='
85 out += i + 2 < bytes.length ? B64[n & 63]! : '='
86 }
87 return out
88}
89
90/** Pack a canvas into Raster cells: two pixels per cell, upper over lower. */
91export function toCells(c: Canvas): string {
92 const rows = Math.ceil(c.h / 2)
93 const words = new Uint32Array(c.w * rows * 3)
94 let i = 0
95 for (let r = 0; r < rows; r++) {
96 for (let x = 0; x < c.w; x++) {
97 const top = c.px[2 * r * c.w + x] ?? null
98 const bottom = 2 * r + 1 < c.h ? (c.px[(2 * r + 1) * c.w + x] ?? null) : null
99 if (top === null && bottom === null) {
100 words.set([0x20, DEFAULT, DEFAULT], i)
101 } else if (top === null) {
102 words.set([HALF_LOWER, bottom as number, DEFAULT], i)
103 } else {
104 words.set([HALF_UPPER, top, bottom ?? DEFAULT], i)
105 }
106 i += 3
107 }
108 }
109 return base64(new Uint8Array(words.buffer))
110}
111
112// deterministic stars per size
113function stars(w: number, h: number) {
114 let seed = w * 7919 + h * 104729
115 const rand = () => ((seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff)
116 const n = Math.max(6, Math.floor((w * h) / 45))
117 return Array.from({ length: n }, () => ({
118 x: Math.floor(rand() * w),
119 y: Math.floor(rand() * h),
120 color: STAR_COLORS[Math.floor(rand() * STAR_COLORS.length)] ?? GOLD,
121 phase: Math.floor(rand() * 12),
122 }))
123}
124
125/** The pane's night sky with Ricky in the middle, `cols` wide, `rows` cells tall. */
126export function paneScene(cols: number, rows: number, mood: RickyMood, tick: number): string {
127 const w = cols
128 const h = rows * 2
129 const c = canvas(w, h, 0)
130 // gradient sky
131 for (let y = 0; y < h; y++) {
132 const t = y / Math.max(1, h - 1)
133 const r = Math.round(0x0d + (0x2a - 0x0d) * t)
134 const g = Math.round(0x06 + (0x12 - 0x06) * t)
135 const b = Math.round(0x20 + (0x55 - 0x20) * t)
136 for (let x = 0; x < w; x++) c.px[y * w + x] = (r << 16) | (g << 8) | b
137 }
138 // twinkling stars
139 for (const s of stars(w, h)) {
140 if ((tick + s.phase) % 12 < 9) put(c, s.x, s.y, s.color)
141 if ((tick + s.phase) % 12 === 0) {
142 put(c, s.x - 1, s.y, s.color)
143 put(c, s.x + 1, s.y, s.color)
144 }
145 }
146 // crescent moon, top right
147 const mx = w - 7
148 const MOON: [number, number][] = [[1, 0], [2, 0], [0, 1], [0, 2], [0, 3], [1, 4], [2, 4], [1, 1], [1, 3]]
149 for (const [dx, dy] of MOON) {
150 put(c, mx + dx, 2 + dy, dx === 0 ? GOLD : 0xffe89a)
151 }
152 // Ricky
153 const bob = mood === 'fly' ? Math.round(Math.sin(tick / 1.5) * 2) : Math.round(Math.sin(tick / 4) * 1)
154 const jitter = mood === 'shock' ? (tick % 2 === 0 ? -1 : 1) : 0
155 const x0 = Math.floor((w - SPRITE_W) / 2) + jitter
156 const y0 = Math.floor((h - SPRITE_H) / 2) + bob + 1
157 if (mood === 'fly') {
158 // sparkle trail under the wings
159 for (let k = 0; k < 4; k++) {
160 const sy = y0 + SPRITE_H - 2 + ((tick + k) % 4)
161 put(c, x0 + 3 + k * 2, sy, k % 2 ? GOLD : 0xb8f4ff)
162 put(c, x0 + SPRITE_W - 4 - k * 2, sy, k % 2 ? 0xb8f4ff : GOLD)
163 }
164 }
165 stamp(c, DECODED[frameFor(mood, tick)], x0, y0)
166 return toCells(c)
167}
168
169/** The band: a see-through ribbon `cols` wide with a `size`-px Ricky; it flies across while working. */
170export function bandScene(cols: number, mood: RickyMood, tick: number, size: BandSize = 24): string {
171 const sprite = BAND_FRAMES[size][frameFor(mood, tick)]
172 const spriteW = sprite[0]?.length ?? size
173 const w = cols
174 const h = BAND_ROWS[size] * 2
175 const c = canvas(w, h, null)
176 const span = Math.max(0, w - spriteW - 2)
177 let x = 1
178 if ((mood === 'fly' || mood === 'think') && span > 0) {
179 const p = tick % (span * 2)
180 const forward = p < span
181 x = 1 + (forward ? p : span * 2 - p) // fly back and forth
182 const tail = forward ? x - 2 : x + spriteW + 1
183 const behind = forward ? -1 : 1
184 for (let k = 0; k < 8; k++) {
185 // a trail of sparkles behind
186 if ((tick + k) % 3 === 0) continue
187 put(c, tail + behind * k * 2, Math.floor(h * 0.4) + ((k * 5 + tick) % Math.max(1, Math.floor(h * 0.4))), k % 2 ? GOLD : 0xb79bff)
188 }
189 } else if (mood === 'shock') {
190 x = 1 + (tick % 2)
191 }
192 stamp(c, sprite, x, 0)
193 return toCells(c)
194}
195hooks/sprites.ts 724 lines1// Generated by tools/sprites.py. Do not edit.
2export const PALETTE: Record<string, number | null> = {
3 ".": null,
4 "K": 0x2a1650,
5 "D": 0x5b36b8,
6 "P": 0x8a5cf0,
7 "L": 0xb79bff,
8 "E": 0xe07bff,
9 "W": 0xf4f0ff,
10 "w": 0xcfc6ec,
11 "G": 0xf5c542,
12 "Y": 0xffe89a,
13 "B": 0x1e1a4a,
14 "b": 0x4a5cc8,
15 "C": 0xb8f4ff,
16 "c": 0x6fd3f0,
17 "M": 0x3a2050,
18}
19
20export const SPRITE_W = 32
21export const SPRITE_H = 32
22
23export const FRAMES = {
24 idle: [
25 ".GG.............................",
26 "GY..............................",
27 "G.....K..................K......",
28 "GY...KEK................KEK.....",
29 ".GG..KEPK..............KPEK.....",
30 "..D.KEEPK..............KPEEK....",
31 "..D.KEEPPK............KPPEEK....",
32 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
33 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
34 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
35 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
36 ".KPLPPPBBBPPPPGYYGPPPPBBBPPPLPK.",
37 ".KPLPPBBBBBPPPPGGPPPPBBBBBPPLPK.",
38 ".KPLPBWWBBBBPPPPPPPPBBBBWWBPLPK.",
39 ".KPPPBWWBBBBPPPPPPPPBBBBWWBPPPK.",
40 ".KPPPBBBBWBBPPPPPPPPBBWBBBBPPPK.",
41 ".KPPPBbbbbbBPPPWWPPPBbbbbbBPPPK.",
42 ".KPPPPBbbbBPPWWWWWWPPBbbbBPPPPK.",
43 "cKPEEPPPPPPPWWWWWWWWPPPPPPPEEPKc",
44 "CcKPPPPPPPWWWMWMMWMWWWPPPPPPPKcC",
45 "CCKPPPPPPWWWWWMWWMWWWWWPPPPPPKCC",
46 "CCCKPPPPWWWWWWWWWWWWWWWWPPPPKCCC",
47 "cCCCKKPPWWWWWWWWWWWWWWWWPPKKCCCc",
48 ".cCCCcKKPPWWWWWWWWWWWWPPKKcCCCc.",
49 "..cCCc.KPPPwWWWWWWWWwPPPK.cCCc..",
50 "...cc..KPPPPwWWWWWWwPPPPK..cc...",
51 ".......KPPPPPPPPPPPPPPPPK.......",
52 ".......KDPPPPPPPPPPPPPPDK.......",
53 ".......KDDPPKDPPPPDKPPDDK.......",
54 "........KWWKKWWKKWWKKWWK........",
55 ".........KK..KK..KK..KK.........",
56 "................................",
57 ],
58 idle2: [
59 ".GG.............................",
60 "GY..............................",
61 "G.....K..................K......",
62 "GY...KEK................KEK.....",
63 ".GG..KEPK..............KPEK.....",
64 "..D.KEEPK..............KPEEK....",
65 "..D.KEEPPK............KPPEEK....",
66 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
67 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
68 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
69 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
70 ".KPLPPPBBBPPPPGYYGPPPPBBBPPPLPK.",
71 ".KPLPPBBBBBPPPPGGPPPPBBBBBPPLPK.",
72 ".KPLPBWWBBBBPPPPPPPPBBBBWWBPLPK.",
73 ".KPPPBWWBBBBPPPPPPPPBBBBWWBPPPK.",
74 ".KPPPBBBBWBBPPPPPPPPBBWBBBBPPPK.",
75 ".KPPPBbbbbbBPPPWWPPPBbbbbbBPPPK.",
76 ".KPPPPBbbbBPPWWWWWWPPBbbbBPPPPK.",
77 ".KPEEPPPPPPPWWWWWWWWPPPPPPPEEPK.",
78 "..KPPPPPPPWWWMWMMWMWWWPPPPPPPK..",
79 "..KPPPPPPWWWWWMWWMWWWWWPPPPPPK..",
80 "...KPPPPWWWWWWWWWWWWWWWWPPPPK...",
81 "....KKPPWWWWWWWWWWWWWWWWPPKK....",
82 "...cCcKKPPWWWWWWWWWWWWPPKKcCc...",
83 "..cCCCcKPPPwWWWWWWWWwPPPKcCCCc..",
84 ".cCCCCcKPPPPwWWWWWWwPPPPKcCCCCc.",
85 "cCCCCc.KPPPPPPPPPPPPPPPPK.cCCCCc",
86 "CCCc...KDPPPPPPPPPPPPPPDK...cCCC",
87 "Cc.....KDDPPKDPPPPDKPPDDK.....cC",
88 "c.......KWWKKWWKKWWKKWWK.......c",
89 ".........KK..KK..KK..KK.........",
90 "................................",
91 ],
92 blink: [
93 ".GG.............................",
94 "GY..............................",
95 "G.....K..................K......",
96 "GY...KEK................KEK.....",
97 ".GG..KEPK..............KPEK.....",
98 "..D.KEEPK..............KPEEK....",
99 "..D.KEEPPK............KPPEEK....",
100 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
101 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
102 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
103 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
104 ".KPLPPPPPPPPPPGYYGPPPPPPPPPPLPK.",
105 ".KPLPPPPPPPPPPPGGPPPPPPPPPPPLPK.",
106 ".KPLPPPPPPPPPPPPPPPPPPPPPPPPLPK.",
107 ".KPPPPPPPPPPPPPPPPPPPPPPPPPPPPK.",
108 ".KPPPBPPPPPBPPPPPPPPBPPPPPBPPPK.",
109 ".KPPPPBBBBBPPPPWWPPPPBBBBBPPPPK.",
110 ".KPPPPPPPPPPPWWWWWWPPPPPPPPPPPK.",
111 "cKPEEPPPPPPPWWWWWWWWPPPPPPPEEPKc",
112 "CcKPPPPPPPWWWMWMMWMWWWPPPPPPPKcC",
113 "CCKPPPPPPWWWWWMWWMWWWWWPPPPPPKCC",
114 "CCCKPPPPWWWWWWWWWWWWWWWWPPPPKCCC",
115 "cCCCKKPPWWWWWWWWWWWWWWWWPPKKCCCc",
116 ".cCCCcKKPPWWWWWWWWWWWWPPKKcCCCc.",
117 "..cCCc.KPPPwWWWWWWWWwPPPK.cCCc..",
118 "...cc..KPPPPwWWWWWWwPPPPK..cc...",
119 ".......KPPPPPPPPPPPPPPPPK.......",
120 ".......KDPPPPPPPPPPPPPPDK.......",
121 ".......KDDPPKDPPPPDKPPDDK.......",
122 "........KWWKKWWKKWWKKWWK........",
123 ".........KK..KK..KK..KK.........",
124 "................................",
125 ],
126 think: [
127 ".GG.............................",
128 "GY..............................",
129 "G.....K..................K......",
130 "GY...KEK.......Y........KEK.....",
131 ".GG..KEPK......YY......KPEK.....",
132 "..D.KEEPK.......Y......KPEEK....",
133 "..D.KEEPPK............KPPEEK....",
134 "...KEEEPPPKKKKKYYKKKKKPPPEEEK...",
135 "...KEEPPPPPYPPPYYPPPYPPPPPEEK...",
136 "..KPPPPPPPPPPPYWWYPPPPPPPPPPPK..",
137 "..KPPPPPPPPPYYYWWYYYPPPPPPPPPK..",
138 ".KPLPPPPPPPPPPYWWYPPPPPPPPPPLPK.",
139 ".KPLPPPPPPPPPPPYYPPPPPPPPPPPLPK.",
140 ".KPLPPPPPPPPPPPPPPPPPPPPPPPPLPK.",
141 ".KPPPPPPPPPPPPPPPPPPPPPPPPPPPPK.",
142 ".KPPPBPPPPPBPPPPPPPPBPPPPPBPPPK.",
143 ".KPPPPBBBBBPPPPWWPPPPBBBBBPPPPK.",
144 ".KPPPPPPPPPPPWWWWWWPPPPPPPPPPPK.",
145 "cKPEEPPPPPPPWWWWWWWWPPPPPPPEEPKc",
146 "CcKPPPPPPPWWWMWMMWMWWWPPPPPPPKcC",
147 "CCKPPPPPPWWWWWMWWMWWWWWPPPPPPKCC",
148 "CCCKPPPPWWWWWWWWWWWWWWWWPPPPKCCC",
149 "cCCCKKPPWWWWWWWWWWWWWWWWPPKKCCCc",
150 ".cCCCcKKPPWWWWWWWWWWWWPPKKcCCCc.",
151 "..cCCc.KPPPwWWWWWWWWwPPPK.cCCc..",
152 "...cc..KPPPPwWWWWWWwPPPPK..cc...",
153 ".......KPPPPPPPPPPPPPPPPK.......",
154 ".......KDPPPPPPPPPPPPPPDK.......",
155 ".......KDDPPKDPPPPDKPPDDK.......",
156 "........KWWKKWWKKWWKKWWK........",
157 ".........KK..KK..KK..KK.........",
158 "................................",
159 ],
160 think2: [
161 ".GG.............................",
162 "GY..............................",
163 "G.....K..................K......",
164 "GY...KEK................KEK.....",
165 ".GG..KEPK..............KPEK.....",
166 "..D.KEEPK..............KPEEK....",
167 "..D.KEEPPK............KPPEEK....",
168 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
169 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
170 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
171 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
172 ".KPLPPPPPPPPPPGYYGPPPPPPPPPPLPK.",
173 ".KPLPPPPPPPPPPPGGPPPPPPPPPPPLPK.",
174 ".KPLPPPPPPPPPPPPPPPPPPPPPPPPLPK.",
175 ".KPPPPPPPPPPPPPPPPPPPPPPPPPPPPK.",
176 ".KPPPBPPPPPBPPPPPPPPBPPPPPBPPPK.",
177 ".KPPPPBBBBBPPPPWWPPPPBBBBBPPPPK.",
178 ".KPPPPPPPPPPPWWWWWWPPPPPPPPPPPK.",
179 ".KPEEPPPPPPPWWWWWWWWPPPPPPPEEPK.",
180 "..KPPPPPPPWWWMWMMWMWWWPPPPPPPK..",
181 "..KPPPPPPWWWWWMWWMWWWWWPPPPPPK..",
182 "...KPPPPWWWWWWWWWWWWWWWWPPPPK...",
183 "....KKPPWWWWWWWWWWWWWWWWPPKK....",
184 "...cCcKKPPWWWWWWWWWWWWPPKKcCc...",
185 "..cCCCcKPPPwWWWWWWWWwPPPKcCCCc..",
186 ".cCCCCcKPPPPwWWWWWWwPPPPKcCCCCc.",
187 "cCCCCc.KPPPPPPPPPPPPPPPPK.cCCCCc",
188 "CCCc...KDPPPPPPPPPPPPPPDK...cCCC",
189 "Cc.....KDDPPKDPPPPDKPPDDK.....cC",
190 "c.......KWWKKWWKKWWKKWWK.......c",
191 ".........KK..KK..KK..KK.........",
192 "................................",
193 ],
194 happy: [
195 ".GG.............................",
196 "GY..............................",
197 "G.....K..................K......",
198 "GY...KEK................KEK.....",
199 ".GG..KEPK..............KPEK.....",
200 "..D.KEEPK..............KPEEK....",
201 "..D.KEEPPK............KPPEEK....",
202 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
203 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
204 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
205 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
206 ".KPLPPPPPPPPPPGYYGPPPPPPPPPPLPK.",
207 ".KPLPPPPPPPPPPPGGPPPPPPPPPPPLPK.",
208 ".KPLPPPPBPPPPPPPPPPPPPPBPPPPLPK.",
209 ".KPPPPPBPBPPPPPPPPPPPPBPBPPPPPK.",
210 ".KPPPPBPPPBPPPPPPPPPPBPPPBPPPPK.",
211 ".KPPPBPPPPPBPPPWWPPPBPPPPPBPPPK.",
212 ".KPPPPPPPPPPPWWWWWWPPPPPPPPPPPK.",
213 "cKPEEPPPPPPPWWWWWWWWPPPPPPPEEPKc",
214 "CcKPPPPPPPWWWWMMMMWWWWPPPPPPPKcC",
215 "CCKPPPPPPWWWWWMEEMWWWWWPPPPPPKCC",
216 "CCCKPPPPWWWWWWMMMMWWWWWWPPPPKCCC",
217 "cCCCKKPPWWWWWWWWWWWWWWWWPPKKCCCc",
218 ".cCCCcKKPPWWWWWWWWWWWWPPKKcCCCc.",
219 "..cCCc.KPPPwWWWWWWWWwPPPK.cCCc..",
220 "...cc..KPPPPwWWWWWWwPPPPK..cc...",
221 ".......KPPPPPPPPPPPPPPPPK.......",
222 ".......KDPPPPPPPPPPPPPPDK.......",
223 ".......KDDPPKDPPPPDKPPDDK.......",
224 "........KWWKKWWKKWWKKWWK........",
225 ".........KK..KK..KK..KK.........",
226 "................................",
227 ],
228 happy2: [
229 ".GG.............................",
230 "GY..............................",
231 "G.....K..................K......",
232 "GY...KEK................KEK.....",
233 ".GG..KEPK..............KPEK.....",
234 "..D.KEEPK..............KPEEK....",
235 "..D.KEEPPK............KPPEEK....",
236 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
237 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
238 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
239 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
240 ".KPLPPPPPPPPPPGYYGPPPPPPPPPPLPK.",
241 ".KPLPPPPPPPPPPPGGPPPPPPPPPPPLPK.",
242 ".KPLPPPPBPPPPPPPPPPPPPPBPPPPLPK.",
243 ".KPPPPPBPBPPPPPPPPPPPPBPBPPPPPK.",
244 ".KPPPPBPPPBPPPPPPPPPPBPPPBPPPPK.",
245 ".KPPPBPPPPPBPPPWWPPPBPPPPPBPPPK.",
246 ".KPPPPPPPPPPPWWWWWWPPPPPPPPPPPK.",
247 ".KPEEPPPPPPPWWWWWWWWPPPPPPPEEPK.",
248 "..KPPPPPPPWWWWMMMMWWWWPPPPPPPK..",
249 "..KPPPPPPWWWWWMEEMWWWWWPPPPPPK..",
250 "...KPPPPWWWWWWMMMMWWWWWWPPPPK...",
251 "....KKPPWWWWWWWWWWWWWWWWPPKK....",
252 "...cCcKKPPWWWWWWWWWWWWPPKKcCc...",
253 "..cCCCcKPPPwWWWWWWWWwPPPKcCCCc..",
254 ".cCCCCcKPPPPwWWWWWWwPPPPKcCCCCc.",
255 "cCCCCc.KPPPPPPPPPPPPPPPPK.cCCCCc",
256 "CCCc...KDPPPPPPPPPPPPPPDK...cCCC",
257 "Cc.....KDDPPKDPPPPDKPPDDK.....cC",
258 "c.......KWWKKWWKKWWKKWWK.......c",
259 ".........KK..KK..KK..KK.........",
260 "................................",
261 ],
262 shock: [
263 ".GG.............................",
264 "GY..............................",
265 "G.....K..................K......",
266 "GY...KEK................KEK.....",
267 ".GG..KEPK..............KPEK.....",
268 "..D.KEEPK..............KPEEK....",
269 "..D.KEEPPK............KPPEEK....",
270 "...KEEEPPPKKKKKGGKKKKKPPPEEEK...",
271 "...KEEPPPPPPPPPGGPPPPPPPPPEEK...",
272 "..KPPPPPPPPPPPGYYGPPPPPPPPPPPK..",
273 "..KPPPPPPPPPGGGYYGGGPPPPPPPPPK..",
274 ".KPLPPPBBBPPPPGYYGPPPPBBBPPPLPK.",
275 ".KPLPPBWWWBPPPPGGPPPPBWWWBPPLPK.",
276 ".KPLPBWWWWWBPPPPPPPPBWWWWWBPLPK.",
277 ".KPPPBWWBWWBPPPPPPPPBWWBWWBPPPK.",
278 ".KPPPBWWWWWBPPPPPPPPBWWWWWBPPPK.",
279 ".KPPPPBWWWBPPPPWWPPPPBWWWBPPPPK.",
280 ".KPPPPPBBBPPPWWWWWWPPPBBBPPPPPK.",
281 "cKPEEPPPPPPPWWWWWWWWPPPPPPPEEPKc",
282 "CcKPPPPPPPWWWWMMMMWWWWPPPPPPPKcC",
283 "CCKPPPPPPWWWWWMEEMWWWWWPPPPPPKCC",
284 "CCCKPPPPWWWWWWMMMMWWWWWWPPPPKCCC",
285 "cCCCKKPPWWWWWWWWWWWWWWWWPPKKCCCc",
286 ".cCCCcKKPPWWWWWWWWWWWWPPKKcCCCc.",
287 "..cCCc.KPPPwWWWWWWWWwPPPK.cCCc..",
288 "...cc..KPPPPwWWWWWWwPPPPK..cc...",
289 ".......KPPPPPPPPPPPPPPPPK.......",
290 ".......KDPPPPPPPPPPPPPPDK.......",
291 ".......KDDPPKDPPPPDKPPDDK.......",
292 "........KWWKKWWKKWWKKWWK........",
293 ".........KK..KK..KK..KK.........",
294 "................................",
295 ],
296} as const
297
298export type FrameName = keyof typeof FRAMES
299
300export const FRAMES24 = {
301 idle: [
302 ".GG.....................",
303 "GY..K..............K....",
304 "G..KEK............KEK...",
305 ".GGKEPK..........KPEK...",
306 "..KEEPK..........KPEEK..",
307 "..KEEPPKKKKKKKKKKPPEEK..",
308 ".KEEPPPPPPPGGPPPPPPPEEK.",
309 ".KPPPPPPPPPYYPPPPPPPPPK.",
310 "KPLPPPPPPGGYYGGPPPPPPLPK",
311 "KPLPPBBBPPPGGPPPBBBPPLPK",
312 "KPLPBWWBBPPPPPPBBWWBPLPK",
313 "KPPPBWBBBPPPPPPBBBWBPPPK",
314 "KPPPBbbbBPPWWPPBbbbBPPPK",
315 "KPEEPBBBPPWWWWPPBBBPEEPK",
316 "cKPPPPPPWWMWWMWWPPPPPPKc",
317 "CKPPPPPWWWWMMWWWWPPPPPKC",
318 "CCKPPPWWWWWWWWWWWWPPPKCC",
319 "cCCKKPPWWWWWWWWWWPPKKCCc",
320 ".cCCcKPPwWWWWWWwPPKcCCc.",
321 "..cc.KPPPwWWWWwPPPK.cc..",
322 ".....KPPPPPPPPPPPPK.....",
323 ".....KDDPKPPPPKPDDK.....",
324 "......KWWK....KWWK......",
325 "........................",
326 ],
327 idle2: [
328 ".GG.....................",
329 "GY..K..............K....",
330 "G..KEK............KEK...",
331 ".GGKEPK..........KPEK...",
332 "..KEEPK..........KPEEK..",
333 "..KEEPPKKKKKKKKKKPPEEK..",
334 ".KEEPPPPPPPGGPPPPPPPEEK.",
335 ".KPPPPPPPPPYYPPPPPPPPPK.",
336 "KPLPPPPPPGGYYGGPPPPPPLPK",
337 "KPLPPBBBPPPGGPPPBBBPPLPK",
338 "KPLPBWWBBPPPPPPBBWWBPLPK",
339 "KPPPBWBBBPPPPPPBBBWBPPPK",
340 "KPPPBbbbBPPWWPPBbbbBPPPK",
341 "KPEEPBBBPPWWWWPPBBBPEEPK",
342 ".KPPPPPPWWMWWMWWPPPPPPK.",
343 ".KPPPPPWWWWMMWWWWPPPPPK.",
344 "..KPPPWWWWWWWWWWWWPPPK..",
345 "...KKPPWWWWWWWWWWPPKK...",
346 "..cCcKPPwWWWWWWwPPKcCc..",
347 ".cCCcKPPPwWWWWwPPPKcCCc.",
348 "cCCc.KPPPPPPPPPPPPK.cCCc",
349 "Cc...KDDPKPPPPKPDDK...cC",
350 "......KWWK....KWWK......",
351 "........................",
352 ],
353 blink: [
354 ".GG.....................",
355 "GY..K..............K....",
356 "G..KEK............KEK...",
357 ".GGKEPK..........KPEK...",
358 "..KEEPK..........KPEEK..",
359 "..KEEPPKKKKKKKKKKPPEEK..",
360 ".KEEPPPPPPPGGPPPPPPPEEK.",
361 ".KPPPPPPPPPYYPPPPPPPPPK.",
362 "KPLPPPPPPGGYYGGPPPPPPLPK",
363 "KPLPPPPPPPPGGPPPPPPPPLPK",
364 "KPLPBPPPBPPPPPPBPPPBPLPK",
365 "KPPPPBBBPPPPPPPPBBBPPPPK",
366 "KPPPPPPPPPPWWPPPPPPPPPPK",
367 "KPEEPPPPPPWWWWPPPPPPEEPK",
368 "cKPPPPPPWWMWWMWWPPPPPPKc",
369 "CKPPPPPWWWWMMWWWWPPPPPKC",
370 "CCKPPPWWWWWWWWWWWWPPPKCC",
371 "cCCKKPPWWWWWWWWWWPPKKCCc",
372 ".cCCcKPPwWWWWWWwPPKcCCc.",
373 "..cc.KPPPwWWWWwPPPK.cc..",
374 ".....KPPPPPPPPPPPPK.....",
375 ".....KDDPKPPPPKPDDK.....",
376 "......KWWK....KWWK......",
377 "........................",
378 ],
379 think: [
380 ".GG.....................",
381 "GY..K..............K....",
382 "G..KEK............KEK...",
383 ".GGKEPK....Y.....KPEK...",
384 "..KEEPK.....Y....KPEEK..",
385 "..KEEPPKKKKKKKKKKPPEEK..",
386 ".KEEPPPPPPPWWPPPPPPPEEK.",
387 ".KPPPPPPYPPWWPPYPPPPPPK.",
388 "KPLPPPPPPGGWWGGPPPPPPLPK",
389 "KPLPPPPPPPPGGPPPPPPPPLPK",
390 "KPLPBPPPBPPPPPPBPPPBPLPK",
391 "KPPPPBBBPPPPPPPPBBBPPPPK",
392 "KPPPPPPPPPPWWPPPPPPPPPPK",
393 "KPEEPPPPPPWWWWPPPPPPEEPK",
394 "cKPPPPPPWWMWWMWWPPPPPPKc",
395 "CKPPPPPWWWWMMWWWWPPPPPKC",
396 "CCKPPPWWWWWWWWWWWWPPPKCC",
397 "cCCKKPPWWWWWWWWWWPPKKCCc",
398 ".cCCcKPPwWWWWWWwPPKcCCc.",
399 "..cc.KPPPwWWWWwPPPK.cc..",
400 ".....KPPPPPPPPPPPPK.....",
401 ".....KDDPKPPPPKPDDK.....",
402 "......KWWK....KWWK......",
403 "........................",
404 ],
405 think2: [
406 ".GG.....................",
407 "GY..K..............K....",
408 "G..KEK............KEK...",
409 ".GGKEPK..........KPEK...",
410 "..KEEPK..........KPEEK..",
411 "..KEEPPKKKKKKKKKKPPEEK..",
412 ".KEEPPPPPPPGGPPPPPPPEEK.",
413 ".KPPPPPPPPPYYPPPPPPPPPK.",
414 "KPLPPPPPPGGYYGGPPPPPPLPK",
415 "KPLPPPPPPPPGGPPPPPPPPLPK",
416 "KPLPBPPPBPPPPPPBPPPBPLPK",
417 "KPPPPBBBPPPPPPPPBBBPPPPK",
418 "KPPPPPPPPPPWWPPPPPPPPPPK",
419 "KPEEPPPPPPWWWWPPPPPPEEPK",
420 ".KPPPPPPWWMWWMWWPPPPPPK.",
421 ".KPPPPPWWWWMMWWWWPPPPPK.",
422 "..KPPPWWWWWWWWWWWWPPPK..",
423 "...KKPPWWWWWWWWWWPPKK...",
424 "..cCcKPPwWWWWWWwPPKcCc..",
425 ".cCCcKPPPwWWWWwPPPKcCCc.",
426 "cCCc.KPPPPPPPPPPPPK.cCCc",
427 "Cc...KDDPKPPPPKPDDK...cC",
428 "......KWWK....KWWK......",
429 "........................",
430 ],
431 happy: [
432 ".GG.....................",
433 "GY..K..............K....",
434 "G..KEK............KEK...",
435 ".GGKEPK..........KPEK...",
436 "..KEEPK..........KPEEK..",
437 "..KEEPPKKKKKKKKKKPPEEK..",
438 ".KEEPPPPPPPGGPPPPPPPEEK.",
439 ".KPPPPPPPPPYYPPPPPPPPPK.",
440 "KPLPPPPPPGGYYGGPPPPPPLPK",
441 "KPLPPPPPPPPGGPPPPPPPPLPK",
442 "KPLPPPBPPPPPPPPPPBPPPLPK",
443 "KPPPPBPBPPPPPPPPBPBPPPPK",
444 "KPPPBPPPBPPWWPPBPPPBPPPK",
445 "KPEEPPPPPPWWWWPPPPPPEEPK",
446 "cKPPPPPPWWMMMMWWPPPPPPKc",
447 "CKPPPPPWWWWEEWWWWPPPPPKC",
448 "CCKPPPWWWWWWWWWWWWPPPKCC",
449 "cCCKKPPWWWWWWWWWWPPKKCCc",
450 ".cCCcKPPwWWWWWWwPPKcCCc.",
451 "..cc.KPPPwWWWWwPPPK.cc..",
452 ".....KPPPPPPPPPPPPK.....",
453 ".....KDDPKPPPPKPDDK.....",
454 "......KWWK....KWWK......",
455 "........................",
456 ],
457 happy2: [
458 ".GG.....................",
459 "GY..K..............K....",
460 "G..KEK............KEK...",
461 ".GGKEPK..........KPEK...",
462 "..KEEPK..........KPEEK..",
463 "..KEEPPKKKKKKKKKKPPEEK..",
464 ".KEEPPPPPPPGGPPPPPPPEEK.",
465 ".KPPPPPPPPPYYPPPPPPPPPK.",
466 "KPLPPPPPPGGYYGGPPPPPPLPK",
467 "KPLPPPPPPPPGGPPPPPPPPLPK",
468 "KPLPPPBPPPPPPPPPPBPPPLPK",
469 "KPPPPBPBPPPPPPPPBPBPPPPK",
470 "KPPPBPPPBPPWWPPBPPPBPPPK",
471 "KPEEPPPPPPWWWWPPPPPPEEPK",
472 ".KPPPPPPWWMMMMWWPPPPPPK.",
473 ".KPPPPPWWWWEEWWWWPPPPPK.",
474 "..KPPPWWWWWWWWWWWWPPPK..",
475 "...KKPPWWWWWWWWWWPPKK...",
476 "..cCcKPPwWWWWWWwPPKcCc..",
477 ".cCCcKPPPwWWWWwPPPKcCCc.",
478 "cCCc.KPPPPPPPPPPPPK.cCCc",
479 "Cc...KDDPKPPPPKPDDK...cC",
480 "......KWWK....KWWK......",
481 "........................",
482 ],
483 shock: [
484 ".GG.....................",
485 "GY..K..............K....",
486 "G..KEK............KEK...",
487 ".GGKEPK..........KPEK...",
488 "..KEEPK..........KPEEK..",
489 "..KEEPPKKKKKKKKKKPPEEK..",
490 ".KEEPPPPPPPGGPPPPPPPEEK.",
491 ".KPPPPPPPPPYYPPPPPPPPPK.",
492 "KPLPPPPPPGGYYGGPPPPPPLPK",
493 "KPLPPBBBPPPGGPPPBBBPPLPK",
494 "KPLPBWWWBPPPPPPBWWWBPLPK",
495 "KPPPBWBWBPPPPPPBWBWBPPPK",
496 "KPPPBWWWBPPWWPPBWWWBPPPK",
497 "KPEEPBBBPPWWWWPPBBBPEEPK",
498 "cKPPPPPPWWMMMMWWPPPPPPKc",
499 "CKPPPPPWWWWEEWWWWPPPPPKC",
500 "CCKPPPWWWWWWWWWWWWPPPKCC",
501 "cCCKKPPWWWWWWWWWWPPKKCCc",
502 ".cCCcKPPwWWWWWWwPPKcCCc.",
503 "..cc.KPPPwWWWWwPPPK.cc..",
504 ".....KPPPPPPPPPPPPK.....",
505 ".....KDDPKPPPPKPDDK.....",
506 "......KWWK....KWWK......",
507 "........................",
508 ],
509} as const
510
511export const FRAMES16 = {
512 idle: [
513 ".G..............",
514 "G.K..........K..",
515 "GKEK........KEK.",
516 "GKEPKKKGGKKKPEK.",
517 "KPPPPPGYYGPPPPPK",
518 "KPPPPPPGGPPPPPPK",
519 "KPWBPPPPPPPPBWPK",
520 "KPBBPPPPPPPPBBPK",
521 "KEbbPPPWWPPPbbEK",
522 "cKPPPWMWWMWPPPKc",
523 "CcKPWWWMMWWWPKcC",
524 "CCcKWWWWWWWWKcCC",
525 ".cCKPWWWWWWPKCc.",
526 "...KPPPPPPPPK...",
527 "...KDPKPPKPDK...",
528 "...KWK....KWK...",
529 ],
530 idle2: [
531 ".G..............",
532 "G.K..........K..",
533 "GKEK........KEK.",
534 "GKEPKKKGGKKKPEK.",
535 "KPPPPPGYYGPPPPPK",
536 "KPPPPPPGGPPPPPPK",
537 "KPWBPPPPPPPPBWPK",
538 "KPBBPPPPPPPPBBPK",
539 "KEbbPPPWWPPPbbEK",
540 ".KPPPWMWWMWPPPK.",
541 "..KPWWWMMWWWPK..",
542 "...KWWWWWWWWK...",
543 ".cCKPWWWWWWPKCc.",
544 "cCCKPPPPPPPPKCCc",
545 "Cc.KDPKPPKPDK.cC",
546 "c..KWK....KWK..c",
547 ],
548 blink: [
549 ".G..............",
550 "G.K..........K..",
551 "GKEK........KEK.",
552 "GKEPKKKGGKKKPEK.",
553 "KPPPPPGYYGPPPPPK",
554 "KPPPPPPGGPPPPPPK",
555 "KPPPPPPPPPPPPPPK",
556 "KPBBBPPPPPPBBBPK",
557 "KEPPPPPWWPPPPPEK",
558 "cKPPPWMWWMWPPPKc",
559 "CcKPWWWMMWWWPKcC",
560 "CCcKWWWWWWWWKcCC",
561 ".cCKPWWWWWWPKCc.",
562 "...KPPPPPPPPK...",
563 "...KDPKPPKPDK...",
564 "...KWK....KWK...",
565 ],
566 think: [
567 ".G..............",
568 "G.K..........K..",
569 "GKEK........KEK.",
570 "GKEPKKKWWKKKPEK.",
571 "KPPPPPGWWGPPPPPK",
572 "KPPPPPPGGPPPPPPK",
573 "KPPPPPPPPPPPPPPK",
574 "KPBBBPPPPPPBBBPK",
575 "KEPPPPPWWPPPPPEK",
576 "cKPPPWMWWMWPPPKc",
577 "CcKPWWWMMWWWPKcC",
578 "CCcKWWWWWWWWKcCC",
579 ".cCKPWWWWWWPKCc.",
580 "...KPPPPPPPPK...",
581 "...KDPKPPKPDK...",
582 "...KWK....KWK...",
583 ],
584 think2: [
585 ".G..............",
586 "G.K..........K..",
587 "GKEK........KEK.",
588 "GKEPKKKGGKKKPEK.",
589 "KPPPPPGYYGPPPPPK",
590 "KPPPPPPGGPPPPPPK",
591 "KPPPPPPPPPPPPPPK",
592 "KPBBBPPPPPPBBBPK",
593 "KEPPPPPWWPPPPPEK",
594 ".KPPPWMWWMWPPPK.",
595 "..KPWWWMMWWWPK..",
596 "...KWWWWWWWWK...",
597 ".cCKPWWWWWWPKCc.",
598 "cCCKPPPPPPPPKCCc",
599 "Cc.KDPKPPKPDK.cC",
600 "c..KWK....KWK..c",
601 ],
602 happy: [
603 ".G..............",
604 "G.K..........K..",
605 "GKEK........KEK.",
606 "GKEPKKKGGKKKPEK.",
607 "KPPPPPGYYGPPPPPK",
608 "KPPPPPPGGPPPPPPK",
609 "KPPBPPPPPPPPBPPK",
610 "KPBPBPPPPPPBPBPK",
611 "KEPPPPPWWPPPPPEK",
612 "cKPPPWMWWMWPPPKc",
613 "CcKPWWWMMWWWPKcC",
614 "CCcKWWWWWWWWKcCC",
615 ".cCKPWWWWWWPKCc.",
616 "...KPPPPPPPPK...",
617 "...KDPKPPKPDK...",
618 "...KWK....KWK...",
619 ],
620 happy2: [
621 ".G..............",
622 "G.K..........K..",
623 "GKEK........KEK.",
624 "GKEPKKKGGKKKPEK.",
625 "KPPPPPGYYGPPPPPK",
626 "KPPPPPPGGPPPPPPK",
627 "KPPBPPPPPPPPBPPK",
628 "KPBPBPPPPPPBPBPK",
629 "KEPPPPPWWPPPPPEK",
630 ".KPPPWMWWMWPPPK.",
631 "..KPWWWMMWWWPK..",
632 "...KWWWWWWWWK...",
633 ".cCKPWWWWWWPKCc.",
634 "cCCKPPPPPPPPKCCc",
635 "Cc.KDPKPPKPDK.cC",
636 "c..KWK....KWK..c",
637 ],
638 shock: [
639 ".G..............",
640 "G.K..........K..",
641 "GKEK........KEK.",
642 "GKEPKKKGGKKKPEK.",
643 "KPPPPPGYYGPPPPPK",
644 "KPPPPPPGGPPPPPPK",
645 "KWWWPPPPPPPPWWWK",
646 "KWBWPPPPPPPPWBWK",
647 "KEWWPPPWWPPPWWEK",
648 "cKPPPWMWWMWPPPKc",
649 "CcKPWWWMMWWWPKcC",
650 "CCcKWWWWWWWWKcCC",
651 ".cCKPWWWWWWPKCc.",
652 "...KPPPPPPPPK...",
653 "...KDPKPPKPDK...",
654 "...KWK....KWK...",
655 ],
656} as const
657
658export const FRAMES8 = {
659 idle: [
660 ".K....K.",
661 "KEK..KEK",
662 "KPPGGPPK",
663 "KBPPPPBK",
664 "KPPWWPPK",
665 ".KKKKKK.",
666 ],
667 idle2: [
668 ".K....K.",
669 "KEK..KEK",
670 "KPPGGPPK",
671 "KBPPPPBK",
672 "KPPWWPPK",
673 ".KKKKKK.",
674 ],
675 blink: [
676 ".K....K.",
677 "KEK..KEK",
678 "KPPGGPPK",
679 "KPPPPPPK",
680 "KBBWWBBK",
681 ".KKKKKK.",
682 ],
683 think: [
684 ".K....K.",
685 "KEK..KEK",
686 "KPPGGPPK",
687 "KPPPPPPK",
688 "KBBWWBBK",
689 ".KKKKKK.",
690 ],
691 think2: [
692 ".K....K.",
693 "KEK..KEK",
694 "KPPGGPPK",
695 "KPPPPPPK",
696 "KBBWWBBK",
697 ".KKKKKK.",
698 ],
699 happy: [
700 ".K....K.",
701 "KEK..KEK",
702 "KPPGGPPK",
703 "KBPPPPBK",
704 "KPWMMWPK",
705 ".KKKKKK.",
706 ],
707 happy2: [
708 ".K....K.",
709 "KEK..KEK",
710 "KPPGGPPK",
711 "KBPPPPBK",
712 "KPPWWPPK",
713 ".KKKKKK.",
714 ],
715 shock: [
716 ".K....K.",
717 "KEK..KEK",
718 "KPPGGPPK",
719 "KWPPPPWK",
720 "KPPMMPPK",
721 ".KKKKKK.",
722 ],
723} as const
724types/index.d.ts 46 lines1export type RickyMood = 'idle' | 'think' | 'fly' | 'happy' | 'shock'
2
3export type RickyTodo = {
4 /** TaskCreate's id; absent for TodoWrite items */
5 id?: string
6 content: string
7 /** what to show while in progress (TodoWrite's activeForm) */
8 label?: string
9 status: 'pending' | 'in_progress' | 'completed'
10 /** when it went in_progress, ms */
11 startedAt?: number
12}
13
14export type RickyActivity = {
15 id: string
16 at: number
17 tool: string
18 detail: string
19 status: 'running' | 'ok' | 'err'
20}
21
22export type RickyAgent = {
23 id: string
24 /** the subagent loop's id, learned from its first tool call */
25 agentId?: string
26 type: string
27 description: string
28 startedAt: number
29 calls: number
30 current?: { tool: string; detail: string }
31}
32
33export type RickyFile = { path: string; edits: number; isNew: boolean }
34
35declare module 'claude-code' {
36 interface PluginState {
37 'ricky-pixel-mod': {
38 mood: RickyMood
39 todos: RickyTodo[]
40 activity: RickyActivity[]
41 agents: RickyAgent[]
42 files: RickyFile[]
43 }
44 }
45}
46