Astral Express: each subagent boards as a Honkai: Star Rail pixel hero; the Express rides above the prompt with the working heroes aboard, and a pane plays…

A collection of public Claude Code mods. Each one is a standalone plugin under plugins/, so you install only the ones you want.
<img src="https://img.shields.io/badge/Claude%20Code-plugin%20marketplace-D97757" alt="Claude Code plugin marketplace"> <img src="https://img.shields.io/badge/agents--pane-v0.3.0-555555" alt="agents-pane v0.3.0"> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"></a>
| Mod | Version | What it does |
|---|---|---|
| agents-pane | 0.3.0 | Every subagent boards the Astral Express as a Honkai: Star Rail pixel hero. A strip above the prompt shows the train with the working heroes in its windows, and the /agents-pane pane tracks each subagent's status, tools, tokens and time while its hero plays through their kit. |
[!IMPORTANT] Mods are built on function hooks (
hooks/register.tsx), an early-access Claude Code API that may change between releases.
Each subagent takes the next hero in the roster, or the hero its description names (for example "Firefly …"). While it works, its card loops through the hero's kit and names each move as it plays.
| Hero | Path | Basic ATK | Skill | Ultimate |
|---|---|---|---|---|
| Dan Heng • Imbibitor Lunae | Destruction | Beneficent Lotus: water slashes | Transcendence: dragon scales and a spear thrust | Azure's Aqua Ablutes All: full moon and the azure dragon |
| Dan Heng • Permansor Terrae | Preservation | Aegis Vitae: a gauntlet strike | Terra Omnibus: a golden shield, Bondmate and Souldragon | A Dragon's Zenith Knows No Rue: the golden dragon dives into a shockwave |
| Castorice | Remembrance | Lament, Nethersea's Ripple | Silence, Wraithfly's Caress: wraithflies | Doomshriek, Dawn's Chime: Netherwing over the Lost Netherland |
| Firefly | Destruction | Order: Flare Propulsion | Order: Aerial Bombardment | Complete Combustion: SAM's green flame wings |
Firefly fights as SAM, her armor, and returns to herself once her work is done.
/agents-pane) opens on a board styled after Star Rail's battle screen: the action order with the working heroes' faces, trailblaze time, victories and defeats, a skill point for each hero at work, and token totals.In Claude Code, run:
/plugin marketplace add desmondlee95700/claude-mod
/plugin install agents-pane@claude-mod
Then restart the session.
[!TIP] If a mod installed from the marketplace doesn't show up, load it by hand as described below.
Clone the repository:
git clone https://github.com/desmondlee95700/claude-mod.git ~/claude-mod
Load a mod for a single session:
claude --plugin-dir ~/claude-mod/plugins/agents-pane
Or load it for every session, including the desktop app, through env in ~/.claude/settings.json. Separate several folders with :, and append to the variable if it's already set:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/claude-mod/plugins/agents-pane"
}
}
Run git pull in the clone to update mods loaded this way.
.claude-plugin/marketplace.json catalog read by /plugin marketplace add
plugins/<name>/
.claude-plugin/plugin.json manifest
hooks/hooks.json points at the hooks module
hooks/register.tsx the hooks module
types/index.d.ts its $.state contract
tests/*.test.ts tests
Validate a plugin and the catalog before publishing:
claude plugin validate plugins/<name>
claude plugin validate .
The code is released under the MIT License.
[!NOTE] The pixel heroes are unofficial fan art of Honkai: Star Rail characters. This project is not affiliated with or endorsed by HoYoverse. Honkai: Star Rail and its characters belong to COGNOSPHERE / HoYoverse.
hooks/register.tsx 772 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { AgentRow, AgentStatus } from '../types'
5import { H, W, bigSprite, bustOf, heroNamed, heroOf, hex, pixelsOf } from './cast'
6import type { Hero } from './cast'
7
8const PANE = 'agents'
9const agents = atom({ plugin: 'agents-pane', key: 'agents' } as const, [])
10const isStripHidden = atom({ plugin: 'agents-pane', key: 'isStripHidden' } as const, false)
11// True while the pane is drawn; the strip steps aside then.
12const isPaneUp = atom({ plugin: 'agents-pane', key: 'isPaneUp' } as const, false)
13// Bumped each second while anyone is in battle, so the battle clocks tick.
14const tick = atom({ plugin: 'agents-pane', key: 'tick' } as const, 0)
15// The next roster spot: each subagent boards the Astral Express as the next hero in line.
16const signed = atom({ plugin: 'agents-pane', key: 'signed' } as const, 0)
17
18const STATUS_COLOR: Record<AgentStatus, string> = {
19 running: 'success',
20 done: 'suggestion',
21 stopped: 'warning',
22 failed: 'error',
23}
24const STATUS_MARK: Record<AgentStatus, string> = {
25 running: '●',
26 done: '✦',
27 stopped: '■',
28 failed: '✗',
29}
30const STATUS_LABEL: Record<AgentStatus, string> = {
31 running: 'IN BATTLE',
32 done: 'VICTORY',
33 stopped: 'RETREATED',
34 failed: 'DEFEATED',
35}
36
37type Key = [t: number, x: number, y: number]
38
39function motion(keys: Key[], total: number, begin = 0): string {
40 const times = keys.map(([t]) => (t / total).toFixed(4)).join(';')
41 const values = keys.map(([, x, y]) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(';')
42 return `<animateTransform attributeName="transform" type="translate" values="${values}" keyTimes="${times}" dur="${total.toFixed(2)}s" begin="${begin.toFixed(2)}s" repeatCount="indefinite"/>`
43}
44
45// Shown only between `from` and `to` (seconds) of each loop.
46function blink(from: number, to: number, total: number): string {
47 const a = Math.max(0, from / total).toFixed(4)
48 const b = Math.min(1, to / total).toFixed(4)
49 return `<animate attributeName="opacity" values="0;1;0" keyTimes="0;${a};${b}" calcMode="discrete" dur="${total.toFixed(2)}s" repeatCount="indefinite"/>`
50}
51
52function fade(values: string, total: number, begin = 0): string {
53 return `<animate attributeName="opacity" values="${values}" dur="${total.toFixed(2)}s" begin="${begin.toFixed(2)}s" repeatCount="indefinite"/>`
54}
55
56const px = (x: number, y: number, size: number, color: number, extra = '') =>
57 `<rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${hex(color)}"${extra}/>`
58
59const STAR = (x: number, y: number, color: number) =>
60 px(x, y - 1, 1, color) + px(x - 1, y, 1, color) + px(x, y, 1, 0xffffff) + px(x + 1, y, 1, color) + px(x, y + 1, 1, color)
61
62// A detailed hero (or SAM) in a nested box, clipped to `view` ("x y w h" of the 32×40 sprite).
63function bust(name: string, x: number, y: number, w: number, h: number, view = '4 2 24 15'): string {
64 const [, top, , rows] = view.split(' ').map(Number)
65 return `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="${view.replace(/^(\S+) \S+/, '$1 0')}" overflow="hidden">${bustOf(name, top, rows)}</svg>`
66}
67
68// The strip: the Astral Express riding the star rail, one carriage window per working subagent (up to four).
69const SCENE_H = 44
70const CAR_W = 30
71const LOCO_W = 40
72
73function train(party: Hero[], isMoving: boolean): { source: string; width: number } {
74 const n = Math.max(1, party.length)
75 const x0 = 6
76 const loco = x0 + n * (CAR_W + 2)
77 const width = loco + LOCO_W + 10
78 const anim = (inner: string, keys: Key[], total: number) => (isMoving ? `<g>${motion(keys, total)}${inner}</g>` : inner)
79
80 const STARS = [[9, 4], [23, 9], [41, 3], [57, 7], [74, 2], [88, 10], [103, 5], [121, 8], [137, 3], [152, 9], [169, 4], [186, 7], [201, 2], [218, 6]]
81 const field = STARS.filter(([x]) => x! < width)
82 .map(([x, y], i) => `<rect x="${x}" y="${y}" width="${i % 3 ? 1 : 1.5}" height="${i % 3 ? 1 : 1.5}" fill="#ffffff" opacity="${i % 2 ? 0.45 : 0.8}"/>`)
83 .join('')
84 const sky = isMoving ? anim(field + `<g transform="translate(${width} 0)">${field}</g>`, [[0, 0, 0], [8, -width, 0]], 8) : field
85
86 const sleepers = Array.from({ length: Math.ceil(width / 5) + 2 }, (_, i) => `<rect x="${i * 5}" y="41" width="2" height="1" fill="#6b5630"/>`).join('')
87 const rail = `<rect x="0" y="40" width="${width}" height="1" fill="#c9a24a"/>` + anim(sleepers, [[0, 0, 0], [0.25, -5, 0]], 0.25)
88
89 const wheel = (cx: number, cy: number, r: number) =>
90 `<g transform="translate(${cx} ${cy})"><circle r="${r}" fill="#25263a" stroke="#c9a24a" stroke-width="0.6"/>` +
91 `<g>${isMoving ? `<animateTransform attributeName="transform" type="rotate" values="0;360" dur="0.6s" repeatCount="indefinite"/>` : ''}` +
92 `<rect x="-0.3" y="${-r + 0.6}" width="0.6" height="${2 * r - 1.2}" fill="#c9a24a"/><rect x="${-r + 0.6}" y="-0.3" width="${2 * r - 1.2}" height="0.6" fill="#c9a24a"/></g>` +
93 `<circle r="0.9" fill="#f2c14e"/></g>`
94
95 const cars = Array.from({ length: n }, (_, i) => {
96 const x = x0 + i * (CAR_W + 2)
97 const hero = party[i]
98 const window = hero
99 ? `<rect x="${x + 4}" y="17" width="22" height="13" fill="#232c5c"/>` + bust(hero.name, x + 5, 17.5, 20, 12.5)
100 : `<rect x="${x + 4}" y="17" width="22" height="13" fill="#141a3a"/>` + STAR(x + 15, 23, 0x5a6aa8)
101 return (
102 `<rect x="${x + 1}" y="11" width="${CAR_W - 2}" height="3" fill="#2a2f55"/>` +
103 `<rect x="${x}" y="14" width="${CAR_W}" height="1" fill="#c9a24a"/>` +
104 `<rect x="${x}" y="15" width="${CAR_W}" height="21" fill="#efe6d2"/>` +
105 `<rect x="${x}" y="30" width="${CAR_W}" height="1" fill="#c9a24a"/>` +
106 `<rect x="${x}" y="31" width="${CAR_W}" height="5" fill="#7a1f2b"/>` +
107 window +
108 `<rect x="${x + 4}" y="17" width="22" height="13" fill="none" stroke="#c9a24a" stroke-width="0.8"/>` +
109 `<rect x="${x + CAR_W}" y="33" width="2" height="1.5" fill="#3a3a48"/>` +
110 wheel(x + 7, 37, 3.2) +
111 wheel(x + CAR_W - 7, 37, 3.2)
112 )
113 }).join('')
114
115 const L = loco
116 const puffs = isMoving
117 ? [0, 1, 2, 3]
118 .map(
119 i =>
120 `<g opacity="0"><g>${motion([[0, L + 28, 9], [1.6, L + 14, -2]], 1.6, -i * 0.4)}<circle r="2.4" fill="#e9e6ff"/><circle cx="1.6" cy="-0.6" r="0.5" fill="#f2c14e"/></g>${fade('0;0.85;0', 1.6, -i * 0.4)}</g>`,
121 )
122 .join('')
123 : ''
124 const locomotive =
125 `<rect x="${L}" y="11" width="16" height="3" fill="#2a2f55"/>` +
126 `<rect x="${L}" y="14" width="15" height="22" fill="#efe6d2"/>` +
127 `<rect x="${L + 3}" y="16.5" width="9" height="7" fill="#f6d27c"/><rect x="${L + 3}" y="16.5" width="9" height="7" fill="none" stroke="#c9a24a" stroke-width="0.8"/>` +
128 `<rect x="${L}" y="30" width="15" height="1" fill="#c9a24a"/><rect x="${L}" y="31" width="15" height="5" fill="#7a1f2b"/>` +
129 `<rect x="${L + 15}" y="19" width="20" height="17" fill="#7a1f2b"/><rect x="${L + 15}" y="19" width="20" height="2" fill="#9b2f3d"/>` +
130 `<rect x="${L + 19}" y="19" width="1.5" height="17" fill="#c9a24a"/><rect x="${L + 28}" y="19" width="1.5" height="17" fill="#c9a24a"/>` +
131 `<rect x="${L + 21}" y="15.5" width="5" height="3.5" fill="#f2c14e"/>` +
132 `<rect x="${L + 27}" y="12" width="4" height="7" fill="#2a2f55"/><rect x="${L + 26}" y="11" width="6" height="1.5" fill="#f2c14e"/>` +
133 `<rect x="${L + 35}" y="18" width="3" height="18" fill="#c9a24a"/>` +
134 `<circle cx="${L + 37}" cy="23" r="5" fill="#fff3b0" opacity="0.3">${isMoving ? `<animate attributeName="opacity" values="0.2;0.45;0.2" dur="1.2s" repeatCount="indefinite"/>` : ''}</circle>` +
135 `<circle cx="${L + 37}" cy="23" r="1.8" fill="#fff6c8"/>` +
136 `<polygon points="${L + 30},36 ${L + 38},36 ${L + 41},40 ${L + 30},40" fill="#a8802a"/>` +
137 STAR(L + 24, 27, 0xf2c14e) +
138 wheel(L + 7, 37, 3.2) +
139 wheel(L + 20, 36.5, 3.8) +
140 wheel(L + 30, 36.5, 3.8)
141
142 const body = anim(cars + locomotive, [[0, 0, 0], [0.25, 0, -0.4], [0.5, 0, 0]], 0.5)
143 return {
144 source: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${SCENE_H}" shape-rendering="crispEdges">${sky}${rail}${puffs}${body}</svg>`,
145 width,
146 }
147}
148
149// A hero's card while they work: their kit on a loop, Basic ATK, then Skill, then Ultimate, each named as it plays.
150const LOOP = 6
151const PHASES: [number, number][] = [
152 [0, 1.6],
153 [1.6, 3.4],
154 [3.4, LOOP],
155]
156
157function phase(i: number, inner: string): string {
158 const [from, to] = PHASES[i]!
159 return `<g opacity="0">${inner}${blink(from, to, LOOP)}</g>`
160}
161
162const spin = (dur: number) => `dur="${dur}s" repeatCount="indefinite"`
163
164function hexagon(r: number): string {
165 return Array.from({ length: 6 }, (_, k) => {
166 const a = (Math.PI / 3) * k + Math.PI / 6
167 return `${(r * Math.cos(a)).toFixed(1)},${(r * Math.sin(a)).toFixed(1)}`
168 }).join(' ')
169}
170
171function chain(path: Key[], total: number, count: number, colors: number[], gap: number, size: (j: number) => number = j => (j === 0 ? 3 : j < 4 ? 2 : 1.5)): string {
172 return Array.from({ length: count }, (_, j) => `<g>${motion(path, total, -j * gap)}${px(-size(j) / 2, -size(j) / 2, size(j), colors[j % colors.length]!)}</g>`)
173 .reverse()
174 .join('')
175}
176
177function butterfly(color: number, dark: number): string {
178 return (
179 `<g><animateTransform attributeName="transform" type="scale" values="1 1;0.2 1;1 1" ${spin(0.3)}/>` +
180 `${px(-2, -1.5, 2, color)}${px(0, -1.5, 2, color)}${px(-1.5, 0.5, 1.5, dark)}${px(0, 0.5, 1.5, dark)}</g>`
181 )
182}
183
184function effects(hero: Hero): { before: string; after: string; body: Key[]; ultBody?: string } {
185 if (hero.name === 'Imbibitor Lunae') {
186 const slash = (d: string, from: number, to: number) =>
187 `<g opacity="0"><path d="${d}" fill="none" stroke="#7ff3e2" stroke-width="1.6" stroke-linecap="round" shape-rendering="geometricPrecision"/>${blink(from, to, LOOP)}</g>`
188 const scale = (x: number, from: number) =>
189 `<g opacity="0"><polygon points="${x},-6 ${x + 2},-3.5 ${x},-1 ${x - 2},-3.5" fill="#5fe3d2" stroke="#e9fffb" stroke-width="0.4"/>${blink(from, 3.4, LOOP)}</g>`
190 const loop: Key[] = Array.from({ length: 25 }, (_, k) => {
191 const a = (k / 24) * Math.PI * 2
192 return [(k / 24) * 1.4, 16 + 24 * Math.cos(a), 20 + 9 * Math.sin(a) - 5 * Math.sin(2 * a)]
193 })
194 return {
195 before: phase(2, `<circle cx="34" cy="-2" r="12" fill="#f8eeb0" opacity="0.2"/><circle cx="34" cy="-2" r="8" fill="#f8eeb0" opacity="0.9"/>`),
196 after:
197 slash('M30 4 Q44 14 32 30', 0.15, 0.6) +
198 slash('M33 8 Q46 20 34 34', 0.6, 1.05) +
199 slash('M28 12 Q42 22 30 38', 1.05, 1.55) +
200 scale(8, 1.7) +
201 scale(16, 1.9) +
202 scale(24, 2.1) +
203 `<g opacity="0"><g>${motion([[0, -8, 0], [2.5, -8, 0], [2.9, 12, 0], [LOOP, 12, 0]], LOOP)}<rect x="16" y="20" width="22" height="1.5" fill="#f6d27c"/><polygon points="38,18.5 43,20.75 38,23" fill="#e9fffb"/></g>${blink(2.5, 3.3, LOOP)}</g>` +
204 phase(2, chain(loop, 1.4, 12, [0xe9fffb, 0x5fe3d2, 0x2aa69c], 0.06)),
205 body: [[0, 0, 0], [0.3, 2, 0], [1.5, 2, 0], [1.6, 0, 0], [2.5, 0, 0], [2.7, 3, -1], [3.3, 0, 0], [3.6, 0, -3], [5.6, 0, -3], [LOOP, 0, 0]],
206 }
207 }
208 if (hero.name === 'Permansor Terrae') {
209 const burst = (r: number) =>
210 `<polygon points="${Array.from({ length: 16 }, (_, k) => {
211 const a = (Math.PI / 8) * k
212 const rr = k % 2 ? r / 2.5 : r
213 return `${(40 + rr * Math.cos(a)).toFixed(1)},${(20 + rr * Math.sin(a)).toFixed(1)}`
214 }).join(' ')}" fill="#f2c14e"/>`
215 const dive: Key[] = [[0, 46, -14], [0.5, 30, 8], [0.8, 16, 36], [2.6, 16, 36]]
216 const shards = [-6, 4, 26, 38]
217 .map((x, i) => `<g opacity="0"><g>${motion([[0, x, 40], [3.4 + 0.8, x, 40], [3.4 + 1.4, x + (i % 2 ? 4 : -4), 24], [LOOP, x + (i % 2 ? 4 : -4), 24]], LOOP)}<rect width="2.5" height="2" fill="#8a6a3a"/></g>${blink(4.2, 5.2, LOOP)}</g>`)
218 .join('')
219 return {
220 before: phase(
221 1,
222 `<g transform="translate(16 20)"><polygon points="${hexagon(24)}" fill="#f2c14e" fill-opacity="0.12" stroke="#f2c14e" stroke-width="1" shape-rendering="geometricPrecision"/></g>`,
223 ),
224 after:
225 `<g opacity="0">${burst(5)}${blink(0.5, 0.8, LOOP)}</g><g opacity="0">${burst(8)}${blink(0.8, 1.2, LOOP)}</g>` +
226 phase(1, `<polygon points="16,-9 18.5,-6 16,-3 13.5,-6" fill="#f2c14e" stroke="#fff1c0" stroke-width="0.4"/>`) +
227 phase(1, `<g>${chain([[0, 36, 30], [0.4, 40, 22], [0.8, 36, 14], [1.2, 40, 6], [1.6, 36, -2]], 1.6, 7, [0xfff1c0, 0x3fc7b5, 0xf2c14e], 0.08)}</g>`) +
228 phase(2, `<g>${chain(dive.map(([t, x, y]) => [t, x, y] as Key), 2.6, 12, [0xfff1c0, 0xf2c14e, 0xd9a63a], 0.05, j => (j === 0 ? 4 : j < 5 ? 3 : 2))}</g>`) +
229 `<g opacity="0"><ellipse cx="16" cy="40" rx="4" ry="1.5" fill="none" stroke="#f2c14e" stroke-width="1" shape-rendering="geometricPrecision"><animate attributeName="rx" values="4;4;30;30" keyTimes="0;${(4.2 / LOOP).toFixed(3)};${(5.2 / LOOP).toFixed(3)};1" dur="${LOOP}s" repeatCount="indefinite"/></ellipse>${blink(4.2, 5.4, LOOP)}</g>` +
230 shards +
231 phase(2, `<g transform="translate(16 20)"><polygon points="${hexagon(22)}" fill="none" stroke="#fff1c0" stroke-width="0.6" stroke-dasharray="2 2" shape-rendering="geometricPrecision"/></g>`),
232 body: [[0, 0, 0], [0.3, 0, 0], [0.5, 4, 0], [1.2, 4, 0], [1.5, 0, 0], [4.0, 0, 0], [4.2, 0, 1], [4.4, 0, 0], [LOOP, 0, 0]],
233 }
234 }
235 if (hero.name === 'Castorice') {
236 const ripple = (begin: number) =>
237 `<g opacity="0"><ellipse cx="36" cy="22" rx="2" ry="4" fill="none" stroke="#b784ea" stroke-width="0.8" shape-rendering="geometricPrecision"><animate attributeName="rx" values="2;8" dur="0.6s" begin="${begin}s" repeatCount="indefinite"/><animate attributeName="ry" values="4;12" dur="0.6s" begin="${begin}s" repeatCount="indefinite"/></ellipse>${blink(0.1, 1.5, LOOP)}</g>`
238 const flies = [10, 18, 26, 14, 30]
239 .map((y, i) => {
240 const keys: Key[] = [[0, 20, 18], [1.6 + i * 0.12, 20, 18], [2.6 + i * 0.12, 44, y], [LOOP, 44, y]]
241 return `<g opacity="0"><g>${motion(keys, LOOP)}${butterfly(0xc996ff, 0x8650d4)}</g>${blink(1.6 + i * 0.12, 3.3, LOOP)}</g>`
242 })
243 .join('')
244 const flowers = [-6, 2, 30, 38, 12]
245 .map(x => `${px(x, 38, 1, 0xffffff)}${px(x - 1, 39, 1, 0xc996ff)}${px(x + 1, 39, 1, 0xc996ff)}`)
246 .join('')
247 const wing = (dir: number) =>
248 `<polygon points="16,6 ${16 + dir * 26},-10 ${16 + dir * 24},4 ${16 + dir * 28},12 ${16 + dir * 10},16" fill="#2c1840" fill-opacity="0.85" stroke="#e8dcf5" stroke-width="0.6" shape-rendering="geometricPrecision"/>` +
249 `<path d="M16 6 L${16 + dir * 26} -10 M16 8 L${16 + dir * 24} 4 M16 10 L${16 + dir * 28} 12" stroke="#e8dcf5" stroke-width="0.5" shape-rendering="geometricPrecision"/>`
250 const netherwing =
251 `<g opacity="0"><g>${motion([[0, 0, 14], [3.4, 0, 14], [4.2, 0, 0], [LOOP, 0, 0]], LOOP)}` +
252 wing(-1) +
253 wing(1) +
254 `<polygon points="11,-13 21,-13 23,-7 19,-3 13,-3 9,-7" fill="#ece4f6"/>${px(12.5, -10, 2, 0x7b4fb0)}${px(17.5, -10, 2, 0x7b4fb0)}` +
255 `<rect x="13" y="-4" width="6" height="1" fill="#2c1840"/></g>${blink(3.4, LOOP, LOOP)}</g>`
256 return {
257 before: phase(2, `<ellipse cx="16" cy="40" rx="30" ry="4" fill="#6b3a9c" opacity="0.55"/>${flowers}`) + netherwing,
258 after: ripple(0) + ripple(0.3) + flies,
259 body: [[0, 0, 0], [0.2, 1, 0], [1.4, 1, 0], [1.6, 0, 0], [3.4, 0, 0], [4.0, 0, -2], [5.8, 0, -2], [LOOP, 0, 0]],
260 }
261 }
262 if (hero.name === 'Firefly') {
263 const jet = [0, 1, 2, 3, 4, 5]
264 .map(i => {
265 const c = [0x7ff0c8, 0xf2a03a, 0xd4f26a][i % 3]!
266 return `<g opacity="0"><g>${motion([[0, 28, 18 + (i % 3)], [0.5, 46, 16 + (i % 4)]], 0.5, -i * 0.08)}<rect width="3" height="2" fill="${hex(c)}"/></g>${blink(0.2, 1.5, LOOP)}</g>`
267 })
268 .join('')
269 const impact = [-4, 2, 8, 24, 30, 36]
270 .map((x, i) => `<g opacity="0"><g>${motion([[0, x, 40], [0.6, x + (i % 2 ? 1 : -1), 26]], 0.6, -i * 0.1)}<rect width="2" height="3" fill="${hex([0x7ff0c8, 0xd4f26a, 0x3fd0a0][i % 3]!)}"/></g>${blink(2.7, 3.4, LOOP)}</g>`)
271 .join('')
272 const flameWing = (dir: number) =>
273 `<polygon points="16,14 ${16 + dir * 30},-8 ${16 + dir * 22},6 ${16 + dir * 32},10 ${16 + dir * 18},18 ${16 + dir * 26},28 16,24" fill="#6ff7d0" fill-opacity="0.55" stroke="#c8fff0" stroke-width="0.5" shape-rendering="geometricPrecision"><animate attributeName="fill-opacity" values="0.35;0.7;0.35" ${spin(0.5)}/></polygon>`
274 const rising = [-6, 0, 6, 26, 32, 38]
275 .map((x, i) => `<g opacity="0"><g>${motion([[0, x, 40], [1, x, 8]], 1, -i * 0.17)}<rect width="2" height="3" fill="${hex([0x7ff0c8, 0xd4f26a][i % 2]!)}"/></g>${fade('0;1;0', 1, -i * 0.17)}</g>`)
276 .join('')
277 return {
278 before: phase(2, flameWing(-1) + flameWing(1)),
279 after: jet + impact + phase(2, rising),
280 body: [[0, 0, 0], [1.6, 0, 0], [2.2, 2, -14], [2.5, 2, -14], [2.7, 0, 0], [3.4, 0, 0], [3.8, 0, -2], [5.8, 0, -2], [LOOP, 0, 0]],
281 ultBody: 'SAM',
282 }
283 }
284 return { before: '', after: '', body: [[0, 0, 0], [LOOP, 0, 0]] }
285}
286
287function cardSvg(hero: Hero, isLive: boolean): string {
288 const wrap = (inner: string) =>
289 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-12 -14 56 58" shape-rendering="crispEdges"><rect x="-12" y="40" width="56" height="1" fill="${hex(hero.accent)}" opacity="0.4"/>${inner}</svg>`
290 if (!isLive || hero.kit === undefined) {
291 return wrap(bigSprite(hero.name))
292 }
293 const fx = effects(hero)
294 // Firefly fights as SAM, her armor; the other heroes fight as themselves.
295 const body = `<g>${motion(fx.body, LOOP)}${bigSprite(fx.ultBody ?? hero.name)}</g>`
296 const title = (i: number, tag: string) => {
297 const name = hero.kit![i]!
298 const fit = name.length > 18 ? ` textLength="54" lengthAdjust="spacingAndGlyphs"` : ''
299 return phase(
300 i,
301 `<text x="16" y="-9" text-anchor="middle" font-size="3.4" font-family="ui-monospace, Menlo, monospace" fill="#8d94b8" letter-spacing="0.6">${tag}</text>` +
302 `<text x="16" y="-4" text-anchor="middle" font-size="4.6" font-weight="700" font-family="ui-sans-serif, system-ui" fill="${hex(hero.accent)}"${fit}>${name.replace(/&/g, '&')}</text>`,
303 )
304 }
305 return wrap(fx.before + body + fx.after + title(0, 'BASIC ATK') + title(1, 'SKILL') + title(2, 'ULTIMATE'))
306}
307
308// The pane's header, styled after Star Rail's battle screen: action order, trailblaze time, victories and defeats,
309// skill points for the heroes at work, and the tokens spent.
310type Board = { wins: number; losses: number; clock: string; roster: number; tokens: string; party: Hero[] }
311
312const DIGITS = [
313 ['111', '101', '101', '101', '111'],
314 ['010', '110', '010', '010', '111'],
315 ['111', '001', '111', '100', '111'],
316 ['111', '001', '111', '001', '111'],
317 ['101', '101', '111', '001', '001'],
318 ['111', '100', '111', '001', '111'],
319 ['111', '100', '111', '101', '111'],
320 ['111', '001', '010', '010', '010'],
321 ['111', '101', '111', '101', '111'],
322 ['111', '101', '111', '001', '111'],
323]
324
325function led(text: string, cx: number, top: number, pitch: number, on: string, off: string): string {
326 const widthOf = (ch: string) => (ch === ':' ? 2 : 4) * pitch
327 let x = cx - ([...text].reduce((sum, ch) => sum + widthOf(ch), 0) - pitch) / 2
328 const r = pitch * 0.38
329 const dot = (dx: number, dy: number, isOn: boolean) =>
330 `<circle cx="${(x + dx * pitch + pitch / 2).toFixed(1)}" cy="${(top + dy * pitch + pitch / 2).toFixed(1)}" r="${r.toFixed(2)}" fill="${isOn ? on : off}"/>`
331 const out: string[] = []
332 for (const ch of text) {
333 if (ch === ':') {
334 out.push(dot(0, 1, true), dot(0, 3, true))
335 } else {
336 const glyph = DIGITS[Number(ch)] ?? DIGITS[0]!
337 glyph.forEach((line, dy) => [...line].forEach((bit, dx) => out.push(dot(dx, dy, bit === '1'))))
338 }
339 x += widthOf(ch)
340 }
341 return out.join('')
342}
343
344const EMBLEM = (cx: number, cy: number, r: number, color: string) =>
345 `<polygon points="${Array.from({ length: 16 }, (_, k) => {
346 const a = (Math.PI / 8) * k - Math.PI / 2
347 const rr = k % 2 ? r * 0.38 : k % 4 ? r * 0.7 : r
348 return `${(cx + rr * Math.cos(a)).toFixed(1)},${(cy + rr * Math.sin(a)).toFixed(1)}`
349 }).join(' ')}" fill="${color}"/>`
350
351function scoreboard(b: Board): string {
352 const GOLD = '#f2c14e'
353 const CYAN = '#5fe3d2'
354 const FONT = 'font-family="ui-monospace, Menlo, monospace"'
355 const label = (x: number, y: number, text: string, fill = '#8d94b8', size = 5.5, anchor = 'middle') =>
356 `<text x="${x}" y="${y}" fill="${fill}" font-size="${size}" font-weight="600" ${FONT} text-anchor="${anchor}" letter-spacing="1">${text}</text>`
357 const panel = (x: number, w: number) =>
358 `<rect x="${x}" y="24" width="${w}" height="52" rx="2" fill="#0f1430" fill-opacity="0.85" stroke="#c9a24a" stroke-opacity="0.45"/>` +
359 `<polygon points="${x + w / 2},22 ${x + w / 2 + 2},24 ${x + w / 2},26 ${x + w / 2 - 2},24" fill="${GOLD}"/>`
360 const stars = [[24, 30], [70, 84], [130, 6], [196, 88], [250, 30], [300, 52], [12, 60], [306, 82], [160, 96], [100, 4]]
361 .map(([x, y], i) => `<circle cx="${x}" cy="${y}" r="${i % 3 ? 0.6 : 0.9}" fill="#ffffff" opacity="0.6"/>`)
362 .join('')
363 const corner = (x: number, y: number) => `<polygon points="${x},${y - 3} ${x + 3},${y} ${x},${y + 3} ${x - 3},${y}" fill="${GOLD}"/>`
364
365 const order = b.party.length
366 ? b.party
367 .slice(0, 4)
368 .map((hero, i) => {
369 const x = 13 + i * 26
370 return (
371 `<rect x="${x}" y="36" width="22" height="18" fill="#1b2350" stroke="${hex(hero.accent)}" stroke-width="0.8"/>` +
372 bust(hero.name, x + 0.5, 36.5, 21, 17, '4 2 24 19') +
373 `<rect x="${x}" y="56" width="22" height="2" fill="${hex(hero.accent)}"/>`
374 )
375 })
376 .join('')
377 : label(62, 52, 'STANDBY', '#5a6290', 7)
378 const points = Array.from({ length: 5 }, (_, i) => {
379 const x = 66 + i * 9
380 const isOn = i < Math.min(5, b.party.length)
381 return `<polygon points="${x},84 ${x + 3},87.5 ${x},91 ${x - 3},87.5" fill="${isOn ? CYAN : '#16304a'}" stroke="${GOLD}" stroke-width="0.5"/>`
382 }).join('')
383
384 return (
385 `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="200" viewBox="0 0 320 100">` +
386 `<defs><linearGradient id="night" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1a1f4a"/><stop offset="1" stop-color="#070915"/></linearGradient></defs>` +
387 `<rect x="1" y="1" width="318" height="98" rx="4" fill="url(#night)"/>` +
388 stars +
389 `<path d="M-10 92 Q160 62 330 92" fill="none" stroke="#c9a24a" stroke-opacity="0.25" stroke-width="0.8"/>` +
390 `<rect x="3" y="3" width="314" height="94" rx="3" fill="none" stroke="#c9a24a" stroke-opacity="0.8" stroke-width="0.8"/>` +
391 `<rect x="5" y="5" width="310" height="90" rx="2" fill="none" stroke="#c9a24a" stroke-opacity="0.3" stroke-width="0.5"/>` +
392 corner(3, 3) +
393 corner(317, 3) +
394 corner(3, 97) +
395 corner(317, 97) +
396 EMBLEM(18, 14, 6, GOLD) +
397 `<text x="28" y="17" fill="${GOLD}" font-size="8" font-weight="800" font-family="ui-serif, Georgia, serif" letter-spacing="2">ASTRAL EXPRESS</text>` +
398 label(310, 16.5, 'TRAILBLAZE LOG', '#8d94b8', 5, 'end') +
399 panel(8, 110) +
400 panel(124, 90) +
401 panel(220, 92) +
402 label(63, 32, 'ACTION ORDER') +
403 order +
404 label(169, 32, 'TRAILBLAZE TIME') +
405 led(b.clock, 169, 42, 4.4, CYAN, '#0c2624') +
406 label(169, 72, `PARTY ${b.roster}`, GOLD) +
407 label(243, 32, 'VICTORY', GOLD) +
408 led(String(b.wins).padStart(2, '0'), 243, 42, 4, GOLD, '#2a2110') +
409 label(289, 32, 'DEFEAT', '#e0507a') +
410 led(String(b.losses).padStart(2, '0'), 289, 42, 4, '#e0507a', '#2d0f17') +
411 label(266, 72, 'EXPRESS · BUGS', '#5a6290', 4.5) +
412 label(12, 90, 'SKILL POINTS', '#8d94b8', 5, 'start') +
413 points +
414 `<circle cx="252" cy="87.5" r="4" fill="none" stroke="${CYAN}" stroke-width="1"/><circle cx="252" cy="87.5" r="1.6" fill="${CYAN}"/>` +
415 label(310, 90, `TOKENS ${b.tokens.toUpperCase()}`, GOLD, 5.5, 'end') +
416 `</svg>`
417 )
418}
419
420// Two pixels per terminal cell: ▀ in the top color over the bottom one.
421function cells(hero: Hero): string {
422 const pixels = pixelsOf(hero)
423 const DEFAULT = 0x01000000
424 const words: number[] = []
425 for (let y = 0; y < H; y += 2) {
426 for (let x = 0; x < W; x++) {
427 const top = pixels[y * W + x] ?? null
428 const bottom = pixels[(y + 1) * W + x] ?? null
429 if (top === null && bottom === null) words.push(0x20, DEFAULT, DEFAULT)
430 else if (top === null) words.push(0x2584, bottom!, DEFAULT)
431 else words.push(0x2580, top, bottom ?? DEFAULT)
432 }
433 }
434 const bytes = new Uint8Array(Uint32Array.from(words).buffer) as Uint8Array & { toBase64(): string }
435 return bytes.toBase64()
436}
437
438function tokens(n: number): string {
439 return n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`
440}
441
442function clock(ms: number): string {
443 const s = Math.max(0, Math.round(ms / 1000))
444 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
445}
446
447function shortModel(model: string): string {
448 return model.replace(/^claude-/, '').replace(/-\d{8}$/, '')
449}
450
451function spotOf(row: AgentRow, list: AgentRow[]): number {
452 return row.player ?? list.indexOf(row)
453}
454
455function patch($: EngineInterface, id: string, change: (row: AgentRow) => AgentRow) {
456 return update($, agents, list => list.map(row => (row.id === id ? change(row) : row)))
457}
458
459async function openPane($: EngineInterface) {
460 await update($, isStripHidden, () => false)
461 const opened = await $.ui.open({ id: PANE, title: 'Astral Express' })
462 await update($, isPaneUp, () => opened.isPlaced)
463 return opened
464}
465
466let ticker: Timer | null = null
467
468export const register: Register = on => {
469 on('session.start', async ($, e, next) => {
470 await $.command.register({ name: 'agents-pane', description: 'Show the subagents of this session in a pane' })
471 // A pane kept open across a reload is still the engine's; pick it back up.
472 const isUp = (await $.ui.panes()).some(pane => pane.id === PANE && pane.isPlaced)
473 await update($, isPaneUp, () => isUp)
474
475 return next(e)
476 })
477
478 on('command.run', { command: 'agents-pane' }, async $ => {
479 await openPane($)
480
481 return { text: 'Agents pane opened.' }
482 })
483
484 on('ui.close', async ($, e, next) => {
485 const closed = await next(e)
486 if (e.id === PANE) {
487 await update($, isPaneUp, () => false)
488 }
489
490 return closed
491 })
492
493 on('agent.spawn', async ($, e, next) => {
494 const spawned = await next(e)
495 if (spawned.deny !== undefined || spawned.agentId === undefined) {
496 return spawned
497 }
498 const row: AgentRow = {
499 id: spawned.agentId,
500 description: e.description || e.subagentType,
501 type: e.subagentType,
502 model: spawned.model,
503 isBackground: e.background === true,
504 status: 'running',
505 startedAt: await $.clock.now(),
506 endedAt: null,
507 tools: 0,
508 lastTool: null,
509 tokens: 0,
510 player: 0,
511 }
512 // An agent whose description names a hero gets that hero; the rest board in roster order.
513 const named = heroNamed(row.description)
514 if (named !== undefined) {
515 row.player = named
516 } else {
517 row.player = await read($, signed)
518 await update($, signed, n => n + 1)
519 }
520 await update($, agents, list => [...list, row].slice(-50))
521 await update($, isStripHidden, () => false)
522 ticker ??= $.clock.every(1000, () => {
523 update($, tick, n => n + 1).catch(() => undefined)
524 })
525
526 return spawned
527 }).catch(($, e, next) => next(e))
528
529 on('tool.call', async ($, e, next) => {
530 if (e.agentId !== undefined) {
531 await patch($, e.agentId, row => ({ ...row, tools: row.tools + 1, lastTool: e.tool }))
532 }
533
534 return next(e)
535 }).catch(($, e, next) => next(e))
536
537 on('turn.step', async function* ($, e, next) {
538 const step = yield* next(e)
539 const usage = step.usage
540 if (e.agentId !== undefined && usage !== null) {
541 const spent = usage.input_tokens + usage.output_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens
542 await patch($, e.agentId, row => ({ ...row, tokens: row.tokens + spent }))
543 }
544
545 return step
546 })
547
548 on('turn.complete', async ($, e, next) => {
549 if (e.agentId !== undefined) {
550 const status: AgentStatus = e.isAborted ? 'stopped' : e.reason === 'answer' ? 'done' : 'failed'
551 const endedAt = await $.clock.now()
552 await patch($, e.agentId, row => (row.status === 'running' ? { ...row, status, endedAt } : row))
553 if ((await read($, agents)).every(row => row.status !== 'running')) {
554 ticker?.cancel()
555 ticker = null
556 }
557 }
558
559 return next(e)
560 })
561
562 // The strip: the Astral Express riding the star rail with the working subagents aboard, one click from the full party.
563 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
564 const list = await read($, agents)
565 if (e.props.hasSurvey || list.length === 0 || (await read($, isStripHidden)) || (await read($, isPaneUp))) {
566 return next(e)
567 }
568 const { Box, Button, Text } = $.ui.resolve(e)
569 const running = list.filter(row => row.status === 'running').length
570 const finished = list.length - running
571 const spent = list.reduce((sum, row) => sum + row.tokens, 0)
572 const live = list.filter(row => row.status === 'running')
573 // Only the heroes at work ride the Express, the latest four; with none at work it waits on the rail.
574 const party = live.slice(-4)
575
576 const scene = () => {
577 if (e.surface === 'terminal') {
578 if (party.length === 0) {
579 return <Text dimColor>🚂 parked</Text>
580 }
581 return party.map(row => (
582 <Text key={row.id} color={STATUS_COLOR[row.status]}>
583 {heroOf(spotOf(row, list)).name}
584 {STATUS_MARK[row.status]}
585 </Text>
586 ))
587 }
588 const { Svg } = $.ui.resolve({ ...e, surface: 'desktop' })
589 const heroes = party.map(row => heroOf(spotOf(row, list)))
590 const ride = train(heroes, heroes.length > 0)
591 const names = heroes.map(hero => hero.name).join(', ')
592 return (
593 <Svg
594 key="express"
595 source={ride.source}
596 alt={heroes.length > 0 ? `The Astral Express riding the star rail with ${names} aboard` : 'The Astral Express waiting on the rail'}
597 width={(ride.width * 64) / SCENE_H}
598 height={64}
599 isInteractive={heroes.length > 0}
600 />
601 )
602 }
603
604 return (
605 <Box flexDirection="row" gap={1} alignItems="center">
606 <Text weight="semibold" color="claude">
607 ✦ Astral Express
608 </Text>
609 <Box flexDirection="row" gap={1} alignItems="center">
610 {scene()}
611 </Box>
612 <Text color={running > 0 ? 'success' : 'subtle'}>
613 {running} aboard · {finished} done
614 </Text>
615 <Text dimColor tabular>
616 {tokens(spent)} tokens
617 </Text>
618 <Box flexGrow={1} />
619 <Button key="open" label="Open party" hotkey="a" onPress={() => openPane($)} />
620 <Button key="hide" label="×" onPress={() => update($, isStripHidden, () => true)} />
621 </Box>
622 )
623 })
624
625 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
626 const { Box, Button, Text } = $.ui.resolve(e)
627 const list = await read($, agents)
628 await read($, tick)
629 const now = await $.clock.now()
630 const running = list.filter(row => row.status === 'running')
631 const finished = list.filter(row => row.status !== 'running').reverse()
632 const spent = list.reduce((sum, row) => sum + row.tokens, 0)
633 const wins = list.filter(row => row.status === 'done').length
634 const losses = list.filter(row => row.status === 'failed' || row.status === 'stopped').length
635 const first = list[0]?.startedAt ?? now
636 const last = running.length > 0 ? now : Math.max(first, ...list.map(row => row.endedAt ?? first))
637 const isTerminal = e.surface === 'terminal'
638
639 const stat = (label: string, value: string) => (
640 <Box flexDirection="column" flexGrow={1} borderStyle="round" borderDimColor paddingX={1}>
641 <Text dimColor size="caption">
642 {label}
643 </Text>
644 <Text weight="bold" size="heading" tabular>
645 {value}
646 </Text>
647 </Box>
648 )
649
650 const board = () => {
651 const { Svg } = $.ui.resolve({ ...e, surface: 'desktop' })
652 const time = clock(last - first).padStart(5, '0')
653 return (
654 <Svg
655 key="scoreboard"
656 source={scoreboard({
657 wins,
658 losses,
659 clock: time,
660 roster: list.length,
661 tokens: tokens(spent),
662 party: running.slice(-4).map(row => heroOf(spotOf(row, list))),
663 })}
664 alt={`Astral Express ${wins} victories, ${losses} defeats, trailblaze time ${time}, ${running.length} at work, ${tokens(spent)} tokens`}
665 />
666 )
667 }
668
669 const sprite = (row: AgentRow, hero: Hero) => {
670 if (isTerminal) {
671 const { Raster } = $.ui.resolve({ ...e, surface: 'terminal' })
672 return <Raster key={`hero-${row.id}`} columns={W} rows={H / 2} cells={cells(hero)} />
673 }
674 const { Svg } = $.ui.resolve({ ...e, surface: 'desktop' })
675 const isLive = row.status === 'running'
676 const alt = `${hero.name}, ${hero.path}${isLive && hero.kit ? `, using ${hero.kit.join(', then ')}` : ''}`
677 return <Svg source={cardSvg(hero, isLive)} alt={alt} width={96} height={99} isInteractive={isLive} />
678 }
679
680 const card = (row: AgentRow) => {
681 const hero = heroOf(spotOf(row, list))
682 const isLive = row.status === 'running'
683 return (
684 <Box
685 key={row.id}
686 flexDirection="row"
687 gap={2}
688 alignItems="center"
689 borderStyle="round"
690 borderColor={isLive ? STATUS_COLOR[row.status] : undefined}
691 borderDimColor={!isLive}
692 paddingX={1}
693 >
694 {sprite(row, hero)}
695 <Box flexDirection="column" flexGrow={1} flexShrink={1}>
696 <Box flexDirection="row" justifyContent="space-between" gap={1}>
697 <Text weight="semibold" wrap="truncate-end">
698 {row.description}
699 </Text>
700 <Text color={STATUS_COLOR[row.status]} weight="semibold" size="caption">
701 {STATUS_MARK[row.status]} {STATUS_LABEL[row.status]}
702 </Text>
703 </Box>
704 <Text wrap="truncate-end">
705 <Text weight="bold" color="claude">
706 {hero.name}
707 </Text>
708 <Text dimColor>
709 {' '}
710 {hero.path} · {row.type} · {shortModel(row.model)}
711 {row.isBackground ? ' · bg' : ''}
712 </Text>
713 </Text>
714 <Box flexDirection="row" justifyContent="space-between" gap={1}>
715 <Text dimColor wrap="truncate-end">
716 {row.tools} tools{row.lastTool !== null && isLive ? ` · ${row.lastTool}` : ''}
717 </Text>
718 <Text dimColor tabular>
719 {tokens(row.tokens)} · {clock((row.endedAt ?? now) - row.startedAt)}
720 </Text>
721 </Box>
722 </Box>
723 </Box>
724 )
725 }
726
727 return (
728 <Box flexDirection="column" gap={1}>
729 {isTerminal ? (
730 <Box flexDirection="column" gap={1}>
731 <Box flexDirection="row" justifyContent="space-between" alignItems="center">
732 <Text weight="bold" color="claude" size="heading">
733 ✦ Astral Express
734 </Text>
735 <Text dimColor tabular>
736 W {wins} · L {losses}
737 </Text>
738 </Box>
739 <Box flexDirection="row" gap={1}>
740 {stat('Party', `${list.length}`)}
741 {stat('Tokens', tokens(spent))}
742 {stat('Battle time', clock(last - first))}
743 </Box>
744 </Box>
745 ) : (
746 board()
747 )}
748 {list.length === 0 && <Text dimColor>The Express is quiet. A Trailblazer boards as soon as a subagent starts.</Text>}
749 {running.length > 0 && (
750 <Text color="success" weight="semibold" size="caption">
751 In battle · {running.length}
752 </Text>
753 )}
754 {running.map(card)}
755 {finished.length > 0 && (
756 <Box flexDirection="row" justifyContent="space-between" alignItems="center">
757 <Text dimColor weight="semibold" size="caption">
758 Done · {finished.length}
759 </Text>
760 <Button
761 key="clear"
762 label="Clear"
763 onPress={() => update($, agents, rows => rows.filter(row => row.status === 'running'))}
764 />
765 </Box>
766 )}
767 {finished.map(card)}
768 </Box>
769 )
770 })
771}
772hooks/cast.ts 274 lines1import { BIG } from './big'
2import type { Big } from './big'
3
4// The Astral Express roster: Honkai: Star Rail fan-art chibis, 20×24, one per subagent in turn.
5// '.' is see-through; every other letter is a color in the hero's palette.
6
7export const W = 20
8export const H = 24
9
10export type Hero = {
11 name: string
12 path: string
13 element: string
14 move: string
15 accent: number
16 palette: Record<string, number>
17 rows: string[]
18 // The kit as the game names it: Basic ATK, Skill, Ultimate.
19 kit?: [basic: string, skill: string, ultimate: string]
20}
21
22const BASE: Record<string, number> = {
23 k: 0x16141c, // outline
24 s: 0xfde6d4, // skin
25 S: 0xe9bfa8, // ear
26 b: 0xf6a5a5, // blush
27 m: 0xe08a80, // mouth
28 w: 0xffffff, // eye shine
29}
30
31export const CAST: Hero[] = [
32 {
33 name: 'Imbibitor Lunae',
34 path: 'Destruction',
35 element: 'Imaginary',
36 move: 'Azure Dragon',
37 accent: 0x3ccfc0,
38 kit: ['Beneficent Lotus', 'Transcendence', "Azure's Aqua Ablutes All"],
39 palette: {
40 ...BASE,
41 h: 0x1d2130, H: 0x2b3145, t: 0x5fe3d2, T: 0x1f8f8a, e: 0x3fd0c4, E: 0x1a7f7a,
42 c: 0x2aa69c, C: 0x166b66, g: 0xe8c062, p: 0x15171f, o: 0xf3f4f8,
43 },
44 rows: [
45 '....t..........t....',
46 '....tt........tt....',
47 '.....tTkkkkkkTt.....',
48 '....khhhhhhhhhhk....',
49 '...khhhHhhhhhhhhk...',
50 '...khhhhhhhhhhHhk...',
51 '...khhshhhhhhshhk...',
52 '...khskksssskkshk...',
53 '..SkhswessssweshkS..',
54 '...khbEEssssEEbhk...',
55 '...khssssmmsssshk...',
56 '...hkhsssssssshkh...',
57 '...hhkkoottookkhh...',
58 '..hkooooottoooookh..',
59 '..hocooooggoooocoh..',
60 '..hoccccggggccccoh..',
61 '..hsCccccccccccCsh..',
62 '..CcpppppoopppppcC..',
63 '..CcpppppoopppppcC..',
64 '..Cccpppp..ppppccC..',
65 '...Cc.ppp..ppp.cC...',
66 '...C..pTp..pTp..C...',
67 '......kkk..kkk......',
68 '.....kkkk..kkkk.....',
69 ],
70 },
71 {
72 name: 'Permansor Terrae',
73 path: 'Preservation',
74 element: 'Physical',
75 move: 'Souldragon Aegis',
76 accent: 0xf2c14e,
77 kit: ['Aegis Vitae', 'Terra Omnibus', "A Dragon's Zenith Knows No Rue"],
78 palette: {
79 ...BASE,
80 h: 0x1a1c26, H: 0x2b2f40, g: 0xf2c14e, G: 0xb07d1e, e: 0xf0b94a, E: 0xa06a1a,
81 K: 0x262833, y: 0xd9a63a, p: 0x17181f, c: 0x2aa69c, t: 0x3fc7b5, o: 0xf3f4f8,
82 },
83 rows: [
84 '....g..g....g..g....',
85 '.....gg......gg.....',
86 '......gkkkkkkg......',
87 '....khhhhhhhhhhk....',
88 '...khhHhhhhhhhHhk...',
89 '...khhhhhhhhhhhhk...',
90 '...khhshhhshhhshk...',
91 '...khskksssskkshk...',
92 '..SkhswessssweshkS..',
93 '...khbEEssssEEbhk...',
94 '...khssssmmsssshk...',
95 '....kssssssssssk....',
96 '....kKKKKggKKKKk....',
97 '..GgkKKKKggKKKKkoc..',
98 '..GgKKKKKggKKKKKoc..',
99 '..GgKKKKKKKKKKKKoc..',
100 '..ssyyyyyyyyyyyysc..',
101 '...kpppppppppppkoc..',
102 '...kpppppppppppkoct.',
103 '....pppp..pppp..octt',
104 '....pppp..pppp...tt.',
105 '....pgpp..ppgp..tt..',
106 '....kkkk..kkkk......',
107 '...kkkkk..kkkkk.....',
108 ],
109 },
110 {
111 name: 'Castorice',
112 path: 'Remembrance',
113 element: 'Quantum',
114 move: 'Netherwing',
115 accent: 0xb07ae0,
116 kit: ["Lament, Nethersea's Ripple", "Silence, Wraithfly's Caress", "Doomshriek, Dawn's Chime"],
117 palette: {
118 ...BASE,
119 L: 0xe2d6f0, l: 0xffffff, P: 0x9a5cc8, f: 0xf29ac0, e: 0xb07ae0, E: 0x6a3a98,
120 v: 0x6e3aa8, V: 0x4a2380, o: 0xf6f4fb, O: 0xdcd6ea,
121 },
122 rows: [
123 '......fwffffwf......',
124 '.PvP.kLLLLLLLLk.....',
125 '..v.kLLLLLLLLLLk....',
126 '...kLLLlLLLLLLlLk...',
127 '...kLLLLLLLLLLLLk...',
128 '...kLLLLLLLLLLLLk...',
129 '...kLsLLLLLLLLsLk...',
130 '..LkLskksssskksLkL..',
131 '..LkLswesssswesLkL..',
132 '..LkLbEEssssEEbLkL..',
133 '..PkLssssmmssssLkP..',
134 '..PPkLssssssssLkPP..',
135 '..LPLkooofffookLPL..',
136 '..PPsooooffoooosPP..',
137 '..PPsoooovvoooosPP..',
138 '.PPvsoooooooooosvPP.',
139 '.PvvoooooooooooovvP.',
140 '.vvOoOoOoOoOoOoOovv.',
141 '..v...ooo..ooo...v..',
142 '..V...ooo..ooo...V..',
143 '......oOo..oOo......',
144 '......ooo..ooo......',
145 '......vvv..vvv......',
146 '.....vvvv..vvvv.....',
147 ],
148 },
149 {
150 name: 'Firefly',
151 path: 'Destruction',
152 element: 'Fire',
153 move: 'Complete Combustion',
154 accent: 0x7ff0c8,
155 kit: ['Order: Flare Propulsion', 'Order: Aerial Bombardment', 'Complete Combustion'],
156 palette: {
157 ...BASE,
158 a: 0xdcd8d2, A: 0xf4f2ee, K: 0x2a2a38, e: 0x9fdde8, E: 0x5a9fb8,
159 o: 0xf28a2e, q: 0x6fd6c8, Q: 0xb8ead8, Y: 0xf0e48a, n: 0x2a3f6f, N: 0x1b2440, x: 0xf6f7fb,
160 },
161 rows: [
162 '......kkkkkkkk......',
163 '.....kKKKKKKKKk.....',
164 '....kaaaaaaaaaak....',
165 '...kaaaaaaaaaaaak...',
166 '...kaaAaaaaaaAaak...',
167 '...kaaaaaaaaaaaak...',
168 '...kaasaaasaaasak...',
169 '..akaskksssskksaka..',
170 '..akaswesssswesaka..',
171 '.aakabEEssssEEbakaa.',
172 '.aakassssmmssssakaa.',
173 '.aaaaksssssssskaaaa.',
174 '.aaKKkxxxooxxxkKKaa.',
175 '.aaKKxxooooooxxKKaa.',
176 '..aKxKxxxqqxxxKxKa..',
177 '..asxqqqqqqqqqqxsa..',
178 '...qqqqqqqqqqqqqq...',
179 '...QQQQQQQQQQQQQQ...',
180 '...YYYYYYYYYYYYYY...',
181 '......qqq..qqq......',
182 '......nnn..nnn......',
183 '......NNN..NNN......',
184 '......xxx..xxx......',
185 '.....xxoo..ooxx.....',
186 ],
187 },
188]
189
190// The other side: a glitch beetle, 12×10.
191export const BUG: Hero = {
192 name: 'Bug',
193 path: '',
194 element: '',
195 move: '',
196 accent: 0xe0507a,
197 palette: { k: 0x16141c, R: 0xd0406a, r: 0x8a2048, w: 0xffffff },
198 rows: [
199 '..k......k..',
200 '...k....k...',
201 '..kkkkkkkk..',
202 '.kRRRRRRRRk.',
203 'kRRwkRRwkRRk',
204 'kRRRRRRRRRRk',
205 'kRrRRrRRrRRk',
206 '.kRRRRRRRRk.',
207 '.k.k.kk.k.k.',
208 'k..k....k..k',
209 ],
210}
211
212// The roster spot of the hero a text names, by any word of the name ("Lunae", "Permansor", "firefly").
213export function heroNamed(text: string): number | undefined {
214 const words = text.toLowerCase().split(/[^a-z]+/)
215 const spot = CAST.findIndex(hero => hero.name.toLowerCase().split(' ').some(part => words.includes(part)))
216 return spot < 0 ? undefined : spot
217}
218
219export function heroOf(spot: number): Hero {
220 return CAST[spot % CAST.length]!
221}
222
223export function hex(color: number): string {
224 return `#${color.toString(16).padStart(6, '0')}`
225}
226
227// Row-major, null see-through.
228export function pixelsOf(hero: Hero): (number | null)[] {
229 return hero.rows.flatMap(line => [...line].map(ch => (ch === '.' ? null : (hero.palette[ch] ?? null))))
230}
231
232// One rect per run of a color along a row, to keep the markup small.
233export function rects(pixels: (number | null)[], width: number): string {
234 const out: string[] = []
235 for (let y = 0; y * width < pixels.length; y++) {
236 let x = 0
237 while (x < width) {
238 const c = pixels[y * width + x] ?? null
239 let run = 1
240 while (x + run < width && (pixels[y * width + x + run] ?? null) === c) run++
241 if (c !== null) out.push(`<rect x="${x}" y="${y}" width="${run}" height="1" fill="${hex(c)}"/>`)
242 x += run
243 }
244 }
245 return out.join('')
246}
247
248export function spriteOf(hero: Hero): string {
249 return rects(pixelsOf(hero), hero.rows[0]!.length)
250}
251
252// The detailed sprite of a hero (or of SAM), as rects.
253export function bigOf(name: string): Big {
254 return BIG[name]!
255}
256
257export function bigSprite(name: string): string {
258 const big = bigOf(name)
259 return rects(
260 big.rows.flatMap(line => [...line].map(ch => (ch === '.' ? null : (big.palette[ch] ?? null)))),
261 big.rows[0]!.length,
262 )
263}
264
265// The head and shoulders, for a train window or the board: rows `top`..`top + height` of the detailed sprite.
266export function bustOf(name: string, top = 0, height = 22): string {
267 const big = bigOf(name)
268 const rows = big.rows.slice(top, top + height)
269 return rects(
270 rows.flatMap(line => [...line].map(ch => (ch === '.' ? null : (big.palette[ch] ?? null)))),
271 rows[0]!.length,
272 )
273}
274hooks/big.ts 232 lines1// Detailed 32×40 heroes for the desktop, and SAM, Firefly's armor: '.' is see-through, every other letter a palette color.
2
3export type Big = { palette: Record<string, number>; rows: string[] }
4
5export const BIG: Record<string, Big> = {
6 "Imbibitor Lunae": {
7 palette: { s: 0xfde9de, S: 0xf0cbbd, w: 0xffffff, b: 0xf6b3c4, m: 0xd9878f, k: 0x161824, h: 0x1e2232, H: 0x2e3550, j: 0x2e3550, t: 0x7ff3e2, T: 0x2bb3a3, U: 0x17706a, e: 0x45d6ca, E: 0x157a74, o: 0xf4f6fa, O: 0xcfd6e2, c: 0x2aa69c, C: 0x166b66, g: 0xf0c868, G: 0xa8802a, p: 0x16181f, P: 0x2c3040, r: 0xc8342b },
8 rows: [
9 '...t........................t...',
10 '...tt..t................t..tt...',
11 '....tTtt................ttTt....',
12 '.....tTT..kkkkkkkkkkkk..TTt.....',
13 '......TkkkhhhhhhhhhhhhkkkT......',
14 '.....kkhhhhhhhhhhhhhhhhhhkk.....',
15 '....khhhhhhhhjhhhhjhhhhhhhhk....',
16 '...khhHhhhhhhhhhhhhhhhhhhHhhk...',
17 '...khHhhhhhjhhhhhhhhjhhhhhHhk...',
18 '..khhHhhshhhhshhhhshhhhshhHhhk..',
19 '..khHhhsshhssshsshssshhsshhHhk..',
20 '..khHhhskkksssssssssskkkshhHhk..',
21 '.khHhSssweEssssssssssEewssShHhk.',
22 '.khhhSsseeEssssssssssEeessShhhk.',
23 '.khhhhsbEEEssssssssssEEEbshhhhk.',
24 '.khhhhsssssssssssssssssssshhhhk.',
25 '.khhhhhssssssssmmsssssssshhhhhk.',
26 '.khhhhhhSssssssssssssssShhhhhhk.',
27 '.khhhhhhhhhSSSssssSSShhhhhhhhhk.',
28 '.khhhhhhhkkoocsccscookkhhhhhhhk.',
29 '.khhhhhhkoooooccccoooookhhhhhhk.',
30 'khhhhhhkoOooooccccooooOokhhhhhhk',
31 'khhhhhkoOooooocggcoooooOokhhhhhk',
32 'khhhhhkoOoooocggggcooooOokhhhhhk',
33 'khhhhhkssOcccgGggGgcccOsskhhhhhk',
34 'khhhhCkssccccccccccccccsrkChhhhk',
35 'khhhCCkkCpppppoooopppppCrkCChhhk',
36 '.khCcCkCcPpppppoopppppPcCrCcChk.',
37 '.kCcCkCcCPpppppoopppppPCcCkCcCk.',
38 '..kcCkCcCPpppppoopppppPCcCkCck..',
39 '..kCckCcCPppppp..pppppPCcCkcCk..',
40 '...kCkCcCkPppp....pppPkCcCkCk...',
41 '...kCkCcCkPppp....pppPkCcCkCk...',
42 '....kCcCk.pppp....pppp.kCcCk....',
43 '....kCCk..pTpp....ppTp..kCCk....',
44 '.....kk...pppp....pppp...kk.....',
45 '..........kkkk....kkkk..........',
46 '.........kkkkk....kkkkk.........',
47 '.........kPkkk....kkkPk.........',
48 '........kkkkkk....kkkkkk........',
49 ],
50 },
51 "Permansor Terrae": {
52 palette: { s: 0xfde9de, S: 0xf0cbbd, w: 0xffffff, b: 0xf6b3c4, m: 0xd9878f, k: 0x15161e, h: 0x1a1c26, H: 0x2e3346, g: 0xf2c14e, G: 0xa8761c, e: 0x45d6ca, E: 0x157a74, K: 0x262833, y: 0xd9a63a, p: 0x17181f, P: 0x2a2c38, o: 0xf3f4f8, O: 0xcfd6e2, c: 0x2aa69c, C: 0x166b66, t: 0x3fc7b5, T: 0x1f8f8a },
53 rows: [
54 '....g..g................g..g....',
55 '....gg.gg..............gg.gg....',
56 '.....gGg................gGg.....',
57 '......gG..kkkkkkkkkkkk..Gg......',
58 '.......GkkhhhhhhhhhhhhkkG.......',
59 '.....kkhhhhhhhhhhhhhhhhhhkk.....',
60 '....khhhhhhhhHhhhhHhhhhhhhhk....',
61 '...khhHhhhhhhhhhhhhhhhhhhHhhk...',
62 '...khHhhhhhhhhhhhhhhhhhhhhHhk...',
63 '..khhHhhshhhhshhhhshhhhshhHhhk..',
64 '..khHhhsshhssshsshssshhsshhHhk..',
65 '..khHhhskkksssssssssskkkshhHhk..',
66 '..kSSsssweEssssssssssEewsssSSk..',
67 '...kSssseeEssssssssssEeesssSk...',
68 '...khssbEEEssssssssssEEEbsshk...',
69 '....kssssssssssssssssssssssk....',
70 '....kssssssssssmmssssssssssk....',
71 '.....kSssssssssssssssssssSk.....',
72 '......kSSSSSssssssssSSSSSk......',
73 '........kKKKKKggggKKKKKk........',
74 '......kkKKKKKKKggKKKKKKKkoc.....',
75 '....kKKKKKKKKKKggKKKKKKKooock...',
76 '...kKKKKKKKKKKKggKKKKKKKoooOck..',
77 '...kGgKKKKKKKKKKKKKKKKKKooOocck.',
78 '..kGgGKKKKKKKKKKKKKKKKKKooOocCk.',
79 '..kgGgkyyyyyyyyyyyyyyyyyooOocCk.',
80 '..ksSkppppppppppppppppppooOocCk.',
81 '...kkpppppppppPppPppppppkoOocCk.',
82 '....kppppppppPppppPpppppkoOocck.',
83 '....kppppppppp....ppppppkoOocCk.',
84 '.....kpppppppp....ppppppkOoccCk.',
85 '......kppppppp....ppppppkOoccCk.',
86 '......kppppppp....pppppppkOcctk.',
87 '.......kpppppp....ppppppk.kcttt.',
88 '.......kppgppp....pppgppk..kttT.',
89 '.......kpppppp....ppppppk...tTt.',
90 '.......kkkkkkk....kkkkkkk..tTt..',
91 '......kkkkkkkk....kkkkkkkktt....',
92 '......kkGkkkkk....kkkkkGkk......',
93 '.....kkkkkkkkk....kkkkkkkkk.....',
94 ],
95 },
96 "Castorice": {
97 palette: { s: 0xfde9de, S: 0xf0cbbd, w: 0xffffff, b: 0xf6b3c4, m: 0xd9878f, k: 0x2a1838, L: 0xe8def6, l: 0xfbf8ff, M: 0xcdbbe9, P: 0xa577d6, Q: 0x7b4fb0, R: 0x52307e, e: 0xb784ea, E: 0x6b3a9c, d: 0xfbf8ff, D: 0xdcd2f0, F: 0xb6a6da, c: 0x4a2f68, C: 0x8a64b0, f: 0xfff2fb, g: 0x9d6cdc, y: 0xf6d27c, v: 0xc996ff, V: 0x8650d4, o: 0xf6f3fc, O: 0xcfc6e6 },
98 rows: [
99 '............c......c............',
100 '..........c.cC.cc.Cc.c..........',
101 '..........cCcCcCCcCcCc..........',
102 '.......kkfgfcfgffgfcfgfkk.......',
103 '......kLfyfgffyffyffgfyfLk......',
104 '...VvkLLLfflLLLLLLLLlffLLLk.....',
105 '..VvVLLLLLLLLlLLLLlLLLLLLLLk....',
106 '...vLLMLLLLLLLLLLLLLLLLLLMLLk...',
107 '...kLMLLLLLlLLLLLLLLlLLLLLMLk...',
108 '..kLLMLLsLLLLsLLLLsLLLLsLLMLLk..',
109 '..kLMLLssLLsssLssLsssLLssLLMLk..',
110 '..kLMLLskkksssssssssskkksLLMLk..',
111 '.kLMLSssweEssssssssssEewssSLMLk.',
112 '.kLPLSsseeEssssssssssEeessSLPLk.',
113 '.kLPLLsbEEEssssssssssEEEbsLLPLk.',
114 '.kPPLLssssssssssssssssssssLLPPk.',
115 '.kPPLLLssssssssmmssssssssLLLPPk.',
116 '.kPQPLLLSssssssssssssssSLLLPQPk.',
117 '.kPQPLLLLLLSSSssssSSSLLLLLLPQPk.',
118 '.kPQPPLLLksssssssssssskLLLPPQPk.',
119 '.kPQPPLLkssssddddddsssskLLPvQvk.',
120 'kPQQPPLkssSDdffggffdDSsskLPVvVPk',
121 'kPQQPPLkssSDddfddfddDSsskLPPvQPk',
122 'kPQRQPLksSkDddddddddDkSskLPQRQPk',
123 'kPQRQPPksSkDddVggVddDkSskPPQRQPk',
124 'kPQRQPPkssvvvvVggVvvvvsskPPQRQPk',
125 'kQRRQPvkkvvDdddDDdddDvvkkvPQRRQk',
126 'kQRRQvvVVddDdddDDdddDddVVvvQRRQk',
127 '.kQRvvVVdddDdddDDdddDdddVVvvRQk.',
128 '.kQRvVVFdddDdddDDdddDdddFVVvRQk.',
129 '..kRvVvFFddDFddDDddFDddFFvVvRk..',
130 '..kVvV.kFFFDFFFDDFFFDFFFk.VvVk..',
131 '...Vv..kVvkVvkVvvVkvVkvVk..vV...',
132 '...v........sss..sss........v...',
133 '............sSs..sSs............',
134 '............oOo..oOo............',
135 '............oOo..oOo............',
136 '............oOo..oOo............',
137 '...........koOokkoOok...........',
138 '...........kkkk..kkkk...........',
139 ],
140 },
141 "Firefly": {
142 palette: { s: 0xfde9de, S: 0xf0cbbd, w: 0xffffff, b: 0xf6b3c4, m: 0xd9878f, k: 0x1a1a24, a: 0xdedad6, A: 0xffffff, K: 0x1e1e2a, x: 0xf6f7fb, o: 0xf2a03a, O: 0x1e1e2a, q: 0x6fd6c8, Q: 0xb8ead8, Y: 0xf0e48a, n: 0x2a3f6f, N: 0x1b2440, e: 0xa9b2f0, E: 0x6a6fc0 },
143 rows: [
144 '................................',
145 '..........kkkkkkkkkkkk..........',
146 '........kkKKKKKKKKKKKKkk........',
147 '.......kKKKKKKKKKKKKKKKKk.......',
148 '...xq.kaaaaaaaaaaaaaaaaaak......',
149 '..xqxkaaaaaaaaaaaaaaaaaaaak.....',
150 '...qkaaaaAaaaaaaaaaaaaAaaaak....',
151 '...kaaAaaaaaaaaaaaaaaaaaaAaak...',
152 '...kaAaaaaaaaaaaaaaaaaaaaaAak...',
153 '..kaaAaasaaaasaaaasaaaasaaAaak..',
154 '..kaAaassaasssassasssaassaaAak..',
155 '..kaAaaskkksssssssssskkksaaAak..',
156 '.kaAaSssweEssssssssssEewssSaAak.',
157 '.kaaaSsseeEssssssssssEeessSaaak.',
158 '.kaaaasbEEEssssssssssEEEbsaaaak.',
159 '.kaaaassssssssssssssssssssaaaak.',
160 '.kaaaaassssssssmmssssssssaaaaak.',
161 '.kaaaaaaSssssssssssssssSaaaaaak.',
162 '.kqaaaaaaaaSSSssssSSSaaaaaaaaqk.',
163 '.kqaaaakKKKxxxxxxxxxxKKKkaaaaqk.',
164 '.kqaaakKKKKxxxooooxxxKKKKkaaaqk.',
165 'kqqaaakKKKKxxoOooOoxxKKKKkaaaqqk',
166 'kqqaaakxKKKKxxxooxxxKKKKxkaaaqqk',
167 'kqqaaakxxKKKxxqqqqxxKKKxxkaaaqqk',
168 'kqqaakxxxKKqqqqqqqqqqKKxxxkaaqqk',
169 'kqqaakssxkqqqqqqqqqqqqkxsskaaqqk',
170 '.kqqkssqqqqqqqqqqqqqqqqqqsskqqk.',
171 '..kqkqqqqqqqqqqqqqqqqqqqqqqkqk..',
172 '...kQQQQQQQQQQQQQQQQQQQQQQQQk...',
173 '...kQQQQQQQQQQQQQQQQQQQQQQQQk...',
174 '...kYYYYYYYYYYYYYYYYYYYYYYYYk...',
175 '....kkkkkkkkkkkkkkkkkkkkkkkk....',
176 '..........qqqq....qqqq..........',
177 '..........nnnn....nnnn..........',
178 '..........nnnn....nnnn..........',
179 '..........NNNN....NNNN..........',
180 '..........NNNN....NNNN..........',
181 '..........xxxx....xxxx..........',
182 '........kxxoox....xooxxk........',
183 '........kkkkkk....kkkkkk........',
184 ],
185 },
186 "SAM": {
187 palette: { k: 0x0e1016, M: 0x5a6476, m: 0x2c323e, n: 0x1c2029, G: 0x6ff7d0, g: 0xc8fff0 },
188 rows: [
189 '...............gg...............',
190 '..............kggk..............',
191 '...........kkkMggMkkk...........',
192 '.........kkMMMMmmMMMMkk.........',
193 '........kMMmmmmmmmmmmMMk........',
194 '.......kMmmmmmmmmmmmmmmMk.......',
195 '.......kmmGGGGGGGGGGGGmmk.......',
196 '.......kmGggggggggggggGmk.......',
197 '.......kmmGGGGGGGGGGGGmmk.......',
198 '.......kmmmmmmmmmmmmmmmmk.......',
199 '........kmmmmMmmmmMmmmmk........',
200 '.........kkmmmmmmmmmmkk.........',
201 '...kkkkk..kmmmmmmmmmmk..kkkkk...',
202 '..kMMMMMk.kmmmmmmmmmmk.kMMMMMk..',
203 '.kMMmmmmMkkmmGmmmmGmmkkMmmmmMMk.',
204 '.kMmmmmmmkmmmGGmmGGmmmkmmmmmmMk.',
205 '.kmmGmmmmkmmGGGGGGGGmmkmmmmGmmk.',
206 '..kmmmmmkmmmmGGggGGmmmmkmmmmmk..',
207 '...kmmmkmmmmmmmmmmmmmmmmkmmmk...',
208 '...kmmmkmmmmmmmmmmmmmmmmkmmmk...',
209 '...kmmmkkmmmmmmmmmmmmmmkkmmmk...',
210 '...kMmGkkmmmmmmmmmmmmmmkkGmMk...',
211 '...kmmmk.kmmmmmmmmmmmmk.kmmmk...',
212 '...kmmmk.kmmmmmmmmmmmmk.kmmmk...',
213 '...kGGGk.kmmGmmmmmmGmmk.kGGGk...',
214 '....kkk..kmmmmmmmmmmmmk..kkk....',
215 '........kmmmmm....mmmmmk........',
216 '........kmmmmm....mmmmmk........',
217 '........kmGmmm....mmmGmk........',
218 '........kmmmmm....mmmmmk........',
219 '.......kMmmmmm....mmmmmMk.......',
220 '.......kmmmmmm....mmmmmmk.......',
221 '.......kmmGmmm....mmmGmmk.......',
222 '.......kmmmmmm....mmmmmmk.......',
223 '......kMmmmmmm....mmmmmmMk......',
224 '......kmmmmmmm....mmmmmmmk......',
225 '......kmmmmmmm....mmmmmmmk......',
226 '.....kmmmmmmmm....mmmmmmmmk.....',
227 '.....kGGGGGGGG....GGGGGGGGk.....',
228 '.....kkkkkkkkk....kkkkkkkkk.....',
229 ],
230 },
231}
232types/index.d.ts 24 lines1export type AgentStatus = 'running' | 'done' | 'stopped' | 'failed'
2
3export type AgentRow = {
4 id: string
5 description: string
6 type: string
7 model: string
8 isBackground: boolean
9 status: AgentStatus
10 startedAt: number
11 endedAt: number | null
12 tools: number
13 lastTool: string | null
14 tokens: number
15 /** Roster spot on the Astral Express: picks the hero. Absent on rows from before the roster. */
16 player?: number
17}
18
19declare module 'claude-code' {
20 interface PluginState {
21 'agents-pane': { agents: AgentRow[]; isStripHidden: boolean; signed: number; isPaneUp: boolean; tick: number }
22 }
23}
24