@g3nomaly/g3n-mods 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +17 -4
- package/mods/agents-panel/hooks/register.tsx +49 -21
- package/mods/agents-panel/types/index.d.ts +16 -1
- package/mods/agents-panel/view/view.css +51 -0
- package/mods/agents-panel/view/view.ts +126 -0
- package/mods/custom-crab-agents/hooks/register.tsx +108 -95
- package/mods/custom-crab-agents/hooks/sprite.ts +131 -0
- package/mods/custom-crab-agents/types/index.d.ts +35 -0
- package/mods/custom-crab-agents/view/view.css +74 -0
- package/mods/custom-crab-agents/view/view.ts +154 -0
- package/mods/delete-guard/hooks/register.tsx +115 -33
- package/mods/delete-guard/types/index.d.ts +2 -0
- package/mods/mini-game/.claude-plugin/plugin.json +1 -1
- package/mods/mini-game/hooks/bubbles.ts +27 -0
- package/mods/mini-game/hooks/minesweeper.ts +108 -0
- package/mods/mini-game/hooks/mole.ts +91 -0
- package/mods/mini-game/hooks/register.tsx +196 -152
- package/mods/mini-game/types/index.d.ts +18 -1
- package/mods/tool-timeline/hooks/register.tsx +141 -38
- package/mods/tool-timeline/types/index.d.ts +33 -2
- package/mods/tool-timeline/view/view.css +124 -0
- package/mods/tool-timeline/view/view.ts +164 -0
- package/mods/tsconfig.view.json +14 -0
- package/package.json +2 -2
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { atom, read, update } from 'claude-code'
|
|
3
3
|
import type { EngineInterface, Register, RenderElement } from 'claude-code'
|
|
4
4
|
|
|
5
|
-
import type { Crab, CrabSteps, PlannedItem, Role } from '../types'
|
|
5
|
+
import type { Crab, CrabSteps, CrabsViewData, PlannedItem, Role } from '../types'
|
|
6
|
+
import { PALETTE, PIXEL, SPRITE_COLUMNS, SPRITE_ROWS, crabSvg, phaseOf } from './sprite'
|
|
7
|
+
import type { Pose } from './sprite'
|
|
6
8
|
|
|
7
9
|
/** The agents-panel mod's pane, which this mod depends on and draws its crabs into. */
|
|
8
10
|
const PANEL = 'agents-panel'
|
|
@@ -22,7 +24,6 @@ const CHAT_PREFIX = 'tab-chat-'
|
|
|
22
24
|
const CHAT_ROWS = 300
|
|
23
25
|
|
|
24
26
|
const KEEP = 30
|
|
25
|
-
const PIXEL = 4
|
|
26
27
|
|
|
27
28
|
/** Effort level → tier name and the color of the crab's bar. */
|
|
28
29
|
const TIERS: Readonly<Record<string, { name: string; color: string }>> = {
|
|
@@ -55,50 +56,6 @@ const ROLES: Readonly<Record<Role, { name: string; emoji: string }>> = {
|
|
|
55
56
|
other: { name: 'helper', emoji: '' },
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/** Sprite rows, 12 pixels wide; each letter is a palette color, `.` is empty. */
|
|
59
|
-
const BODY = [
|
|
60
|
-
'...k....k...',
|
|
61
|
-
'C..b....b..C',
|
|
62
|
-
'CC.bbbbbb.CC',
|
|
63
|
-
'.CbbbbbbbbC.',
|
|
64
|
-
'..bbbbbbbb..',
|
|
65
|
-
'..bbbbbbbb..',
|
|
66
|
-
'..b.b..b.b..',
|
|
67
|
-
'.b..b..b..b.',
|
|
68
|
-
]
|
|
69
|
-
/** Each role's hat, four rows over the body: an inspector's deerstalker, a work helmet, a top hat, and so on. */
|
|
70
|
-
const HATS: Readonly<Record<Role, readonly string[]>> = {
|
|
71
|
-
review: ['............', '....hhhh....', '...hHhHhh...', '.hhhhhhhhhh.'],
|
|
72
|
-
test: ['.....aa.....', '...yyyyyy...', '..yyyyyyyy..', '.yyyyyyyyyy.'],
|
|
73
|
-
design: ['...tttttt...', '...tttttt...', '...vvvvvv...', '.tttttttttt.'],
|
|
74
|
-
explore: ['............', '....pppp....', '...pppppp...', '.pppppppppp.'],
|
|
75
|
-
plan: ['............', '.mmmmmmmmmmq', '...mmmmmm..q', '...mmmmmm...'],
|
|
76
|
-
build: ['............', '...gggg.....', '..gggggg....', '..ggggggggg.'],
|
|
77
|
-
other: ['............', '...wwwwww...', '..wwwwwwww..', '..uuuuuuuu..'],
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** What each role holds in its right claw, four pixels wide beside the body, by sprite row. */
|
|
81
|
-
const PROPS: Readonly<Record<Role, Readonly<Record<number, string>>>> = {
|
|
82
|
-
review: { 3: '.oo.', 4: 'oLLo', 5: 'oLLo', 6: '.oo.', 7: 's...' },
|
|
83
|
-
test: { 4: 'w.w.', 5: 'www.', 6: '.w..', 7: '.w..', 8: '.w..' },
|
|
84
|
-
design: { 2: '..P.', 3: '..P.', 4: '..f.', 5: '.s..', 6: '.s..', 7: 's...' },
|
|
85
|
-
explore: { 2: '.rrr', 3: '.rrr', 4: '.s..', 5: '.s..', 6: '.s..', 7: '.s..' },
|
|
86
|
-
plan: { 5: 'eee.', 6: 'eEe.', 7: 'eee.' },
|
|
87
|
-
build: { 3: 'mmm.', 4: 'mmm.', 5: '.s..', 6: '.s..', 7: '.s..' },
|
|
88
|
-
other: {},
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Width of the whole sprite in pixels: the 12-pixel crab and the 4-pixel item it holds. */
|
|
92
|
-
const SPRITE_COLUMNS = 16
|
|
93
|
-
|
|
94
|
-
const PALETTE: Readonly<Record<string, string>> = {
|
|
95
|
-
b: '#e07a5f', C: '#c8553d', k: '#1e1e1e',
|
|
96
|
-
g: '#3fa66b', w: '#f0f0f0', u: '#4c8bf5', y: '#f2c14e', a: '#fff3b0',
|
|
97
|
-
h: '#8b6b3e', H: '#c9a46a', t: '#22252b', v: '#9b59d0', p: '#d8c38f',
|
|
98
|
-
m: '#2f3542', q: '#f2c14e', o: '#c9d1d9', L: '#9fd3e6', s: '#7a4a2a',
|
|
99
|
-
P: '#4c8bf5', f: '#c9d1d9', r: '#e05a4f', e: '#efe6c8', E: '#b8a77a',
|
|
100
|
-
}
|
|
101
|
-
|
|
102
59
|
/** Price per million tokens: input, output, cache read, cache write. First match wins. */
|
|
103
60
|
const PRICES: ReadonlyArray<[RegExp, [number, number, number, number]]> = [
|
|
104
61
|
[/fable|mythos/, [10, 50, 0.25, 12.5]],
|
|
@@ -192,28 +149,6 @@ function minutes(ms: number): string {
|
|
|
192
149
|
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
|
|
193
150
|
}
|
|
194
151
|
|
|
195
|
-
/**
|
|
196
|
-
* Draws the crab as one SVG: its role's hat over the body, and the item it holds; a planned crab is drawn grey.
|
|
197
|
-
*
|
|
198
|
-
* @param role - Role whose hat and item to draw.
|
|
199
|
-
* @param isPlanned - Whether the crab is planned work, not a running or finished subagent.
|
|
200
|
-
* @returns The SVG markup as a string.
|
|
201
|
-
*/
|
|
202
|
-
function crabSvg(role: Role, isPlanned: boolean): string {
|
|
203
|
-
const rows = [...HATS[role], ...BODY].map((row, y) => row + (PROPS[role][y] ?? '....'))
|
|
204
|
-
const rects: string[] = []
|
|
205
|
-
rows.forEach((row, y) => {
|
|
206
|
-
for (let x = 0; x < row.length; x++) {
|
|
207
|
-
const color = PALETTE[row.charAt(x)]
|
|
208
|
-
if (color !== undefined) rects.push(`<rect x="${x * PIXEL}" y="${y * PIXEL}" width="${PIXEL}" height="${PIXEL}" fill="${color}"/>`)
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
const width = SPRITE_COLUMNS * PIXEL
|
|
212
|
-
const height = rows.length * PIXEL
|
|
213
|
-
const filter = isPlanned ? ' opacity="0.45" style="filter:grayscale(1)"' : ''
|
|
214
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"${filter}>${rects.join('')}</svg>`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
152
|
/**
|
|
218
153
|
* Draws a progress bar as one SVG, filled to `share` of its width.
|
|
219
154
|
*
|
|
@@ -414,6 +349,49 @@ async function readWindow($: EngineInterface): Promise<void> {
|
|
|
414
349
|
if (usage !== undefined && usage.context.window > 0) await update($, contextWindow, () => usage.context.window)
|
|
415
350
|
}
|
|
416
351
|
|
|
352
|
+
/**
|
|
353
|
+
* @brief Works out what a crab's card says: its tier and role, how full its bar is, its step line and its stats.
|
|
354
|
+
*
|
|
355
|
+
* @param crab - Crab to describe, see `Crab`.
|
|
356
|
+
* @param window - The session's context window, in tokens.
|
|
357
|
+
* @param now - Current time, the end of a crab still running.
|
|
358
|
+
* @returns The card's facts.
|
|
359
|
+
*/
|
|
360
|
+
function factsOf(crab: Crab, window: number, now: number) {
|
|
361
|
+
const tier = tierOf(crab.effort)
|
|
362
|
+
const role = roleOf(crab.title, crab.subagentType)
|
|
363
|
+
const elapsed = crab.durationMs ?? now - crab.startedAt
|
|
364
|
+
const tokens = crab.tokens ?? 0
|
|
365
|
+
const share = crab.steps.total > 0 ? crab.steps.done / crab.steps.total : crab.status === 'running' ? 0.08 + Math.min(0.8, crab.requests / 40) : 1
|
|
366
|
+
const stepLine = crab.steps.total > 0
|
|
367
|
+
? `${crab.steps.done}/${crab.steps.total}${crab.steps.current === undefined ? '' : ` · ${crab.steps.current}`}`
|
|
368
|
+
: crab.status === 'running' ? `step ${crab.requests}` : `${crab.requests} step${crab.requests === 1 ? '' : 's'}`
|
|
369
|
+
const stats = [
|
|
370
|
+
tokens > 0 ? `ctx ${Math.max(1, Math.round((tokens / window) * 100))}%` : '',
|
|
371
|
+
tokens > 0 ? formatTokens(tokens) : '',
|
|
372
|
+
crab.costUsd === undefined ? '' : `≈$${crab.costUsd.toFixed(2)}`,
|
|
373
|
+
minutes(elapsed),
|
|
374
|
+
].filter(part => part.length > 0).join(' · ')
|
|
375
|
+
return { tier, role, share, stepLine, stats }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Carries out an action the crabs' view sent: `openChat` with a crab's id.
|
|
380
|
+
*
|
|
381
|
+
* @param $ - Engine handle the action came through.
|
|
382
|
+
* @param value - The action as the view sent it, JSON of `{ name, arg }`.
|
|
383
|
+
*/
|
|
384
|
+
async function act($: EngineInterface, value: string): Promise<void> {
|
|
385
|
+
let action: { name?: unknown; arg?: unknown }
|
|
386
|
+
try {
|
|
387
|
+
action = JSON.parse(value) as { name?: unknown; arg?: unknown }
|
|
388
|
+
} catch {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
const arg = typeof action.arg === 'object' && action.arg !== null ? (action.arg as Record<string, unknown>) : {}
|
|
392
|
+
if (action.name === 'openChat' && typeof arg.id === 'string') await openChat($, arg.id)
|
|
393
|
+
}
|
|
394
|
+
|
|
417
395
|
/**
|
|
418
396
|
* Opens a crab's conversation as a pane of its own, which the viewer shows in a tab of its own.
|
|
419
397
|
*
|
|
@@ -552,7 +530,7 @@ export const register: Register = on => {
|
|
|
552
530
|
const parts = []
|
|
553
531
|
if (message.role === 'user' && message.text.trim() !== '') {
|
|
554
532
|
parts.push(
|
|
555
|
-
<Box key={`m${index}`} borderStyle="round" borderColor="#3a3f4b" paddingX={1} marginTop={1}>
|
|
533
|
+
<Box key={`m${index}`} borderStyle="round" borderColor="#3a3f4b" backgroundColor="#24272e" paddingX={1} marginTop={1}>
|
|
556
534
|
<Text wrap="wrap">{message.text}</Text>
|
|
557
535
|
</Box>,
|
|
558
536
|
)
|
|
@@ -571,6 +549,10 @@ export const register: Register = on => {
|
|
|
571
549
|
return parts
|
|
572
550
|
})
|
|
573
551
|
|
|
552
|
+
const working = crab?.status === 'running'
|
|
553
|
+
? <Box marginTop={1}><Text color="#d97757">✻ Working… <Text dimColor>{crab.steps.current ?? `step ${crab.requests}`}</Text></Text></Box>
|
|
554
|
+
: ''
|
|
555
|
+
|
|
574
556
|
const input = e.surface === 'mobile'
|
|
575
557
|
? ''
|
|
576
558
|
: (() => {
|
|
@@ -578,7 +560,7 @@ export const register: Register = on => {
|
|
|
578
560
|
return (
|
|
579
561
|
<Input
|
|
580
562
|
key="send"
|
|
581
|
-
placeholder={crab?.status === 'running' ?
|
|
563
|
+
placeholder={crab?.status === 'running' ? `Message ${chat.title}… it reads it after its current step` : `Reply to ${chat.title}…`}
|
|
582
564
|
submitLabel="Send"
|
|
583
565
|
onSubmit={value => sendToAgent($, chat.agentId, value)}
|
|
584
566
|
/>
|
|
@@ -589,6 +571,7 @@ export const register: Register = on => {
|
|
|
589
571
|
<Box flexDirection="column">
|
|
590
572
|
{header}
|
|
591
573
|
{rows.length === 0 ? <Text dimColor>Nothing in this conversation yet.</Text> : rows}
|
|
574
|
+
{working}
|
|
592
575
|
<Box marginTop={1}>{input}</Box>
|
|
593
576
|
</Box>
|
|
594
577
|
)
|
|
@@ -607,6 +590,40 @@ export const register: Register = on => {
|
|
|
607
590
|
const plan = await read($, planned)
|
|
608
591
|
const window = await read($, contextWindow)
|
|
609
592
|
const now = await $.clock.now()
|
|
593
|
+
|
|
594
|
+
// The viewer draws the crabs with the mod's own view: hand it the cards as data, inside the key the panel looks for.
|
|
595
|
+
if (e.surface === 'vscode') {
|
|
596
|
+
const { Input } = $.ui.resolve(e)
|
|
597
|
+
const data: CrabsViewData = {
|
|
598
|
+
crabs: list.map(crab => {
|
|
599
|
+
const facts = factsOf(crab, window, now)
|
|
600
|
+
return {
|
|
601
|
+
id: crab.id,
|
|
602
|
+
title: crab.title,
|
|
603
|
+
role: facts.role,
|
|
604
|
+
roleName: ROLES[facts.role].name,
|
|
605
|
+
color: facts.tier.color,
|
|
606
|
+
model: modelName(crab.model),
|
|
607
|
+
...(crab.effort === undefined ? {} : { effort: crab.effort }),
|
|
608
|
+
status: crab.status,
|
|
609
|
+
stepLine: facts.stepLine,
|
|
610
|
+
stats: facts.stats,
|
|
611
|
+
share: facts.share,
|
|
612
|
+
phase: phaseOf(crab.id),
|
|
613
|
+
}
|
|
614
|
+
}),
|
|
615
|
+
planned: plan.map(item => ({ index: item.index, content: item.content, after: item.after, role: roleOf(item.content, '') })),
|
|
616
|
+
}
|
|
617
|
+
return joined(
|
|
618
|
+
<Box key={`${ADDON_KEY}crabs`}>
|
|
619
|
+
<Box key="g3n-view:custom-crab-agents">
|
|
620
|
+
<Text>{JSON.stringify(data)}</Text>
|
|
621
|
+
<Input key="g3n-act:custom-crab-agents" onSubmit={value => act($, value)} />
|
|
622
|
+
</Box>
|
|
623
|
+
</Box>,
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
610
627
|
if (list.length === 0 && plan.length === 0) {
|
|
611
628
|
return joined(<Box key={`${ADDON_KEY}crabs`}><Text dimColor>No subagents yet. Each one shows up here as a crab.</Text></Box>)
|
|
612
629
|
}
|
|
@@ -616,13 +633,14 @@ export const register: Register = on => {
|
|
|
616
633
|
* Draws a crab sprite: an emoji in the terminal, an SVG elsewhere.
|
|
617
634
|
*
|
|
618
635
|
* @param role - Role whose hat and item to draw.
|
|
619
|
-
* @param
|
|
636
|
+
* @param pose - Walking while the agent runs, standing once done, grey while planned.
|
|
637
|
+
* @param id - The crab's id, which offsets its animations from its neighbours'.
|
|
620
638
|
* @returns The sprite element.
|
|
621
639
|
*/
|
|
622
|
-
const sprite = (role: Role,
|
|
623
|
-
if (e.surface === 'terminal') return <Text color={
|
|
640
|
+
const sprite = (role: Role, pose: Pose, id: string) => {
|
|
641
|
+
if (e.surface === 'terminal') return <Text color={pose === 'planned' ? '#6b7280' : PALETTE.b}>{ROLES[role].emoji}🦀</Text>
|
|
624
642
|
const { Svg } = $.ui.resolve(e)
|
|
625
|
-
return <Svg source={crabSvg(role,
|
|
643
|
+
return <Svg source={crabSvg(role, pose, phaseOf(id))} alt={`${ROLES[role].name} crab`} width={SPRITE_COLUMNS * PIXEL} height={SPRITE_ROWS * PIXEL} />
|
|
626
644
|
}
|
|
627
645
|
/**
|
|
628
646
|
* Draws a progress bar for a crab.
|
|
@@ -648,32 +666,25 @@ export const register: Register = on => {
|
|
|
648
666
|
* @returns The card element.
|
|
649
667
|
*/
|
|
650
668
|
const card = (crab: Crab) => {
|
|
651
|
-
const tier =
|
|
652
|
-
const role = roleOf(crab.title, crab.subagentType)
|
|
653
|
-
const elapsed = crab.durationMs ?? now - crab.startedAt
|
|
654
|
-
const tokens = crab.tokens ?? 0
|
|
655
|
-
const share = crab.steps.total > 0 ? crab.steps.done / crab.steps.total : crab.status === 'running' ? 0.08 + Math.min(0.8, crab.requests / 40) : 1
|
|
656
|
-
const stepLine = crab.steps.total > 0
|
|
657
|
-
? `${crab.steps.done}/${crab.steps.total}${crab.steps.current === undefined ? '' : ` · ${crab.steps.current}`}`
|
|
658
|
-
: crab.status === 'running' ? `step ${crab.requests}` : `${crab.requests} step${crab.requests === 1 ? '' : 's'}`
|
|
659
|
-
const stats = [
|
|
660
|
-
tokens > 0 ? `ctx ${Math.max(1, Math.round((tokens / window) * 100))}%` : '',
|
|
661
|
-
tokens > 0 ? formatTokens(tokens) : '',
|
|
662
|
-
crab.costUsd === undefined ? '' : `≈$${crab.costUsd.toFixed(2)}`,
|
|
663
|
-
minutes(elapsed),
|
|
664
|
-
].filter(part => part.length > 0).join(' · ')
|
|
669
|
+
const { tier, role, share, stepLine, stats } = factsOf(crab, window, now)
|
|
665
670
|
const mark = crab.status === 'running' ? <Text color="#2f6b4a">●</Text>
|
|
666
671
|
: crab.status === 'failed' ? <Text color="#e06c6c">✗</Text>
|
|
667
672
|
: <Text color="#7cc47f">✓</Text>
|
|
673
|
+
// The whole card opens the agent's chat: a Button holds only text, so a plain one covers the card. The terminal,
|
|
674
|
+
// where a cover would hide the card, gets a line of its own instead.
|
|
675
|
+
const opener = e.surface === 'terminal'
|
|
676
|
+
? <Button key={`chat:${crab.id}`} label="open chat" onPress={() => openChat($, crab.id)} />
|
|
677
|
+
: (
|
|
678
|
+
<Box position="absolute" top={0} left={0} right={0} bottom={0}>
|
|
679
|
+
<Button key={`chat:${crab.id}`} plain label="Open chat ↗" onPress={() => openChat($, crab.id)} />
|
|
680
|
+
</Box>
|
|
681
|
+
)
|
|
668
682
|
return (
|
|
669
|
-
<Box flexDirection="row" columnGap={2} paddingY={1}>
|
|
670
|
-
{sprite(role,
|
|
683
|
+
<Box key={`crab:${crab.id}`} flexDirection="row" columnGap={2} paddingY={1} paddingX={1}>
|
|
684
|
+
{sprite(role, crab.status === 'running' ? 'running' : 'idle', crab.id)}
|
|
671
685
|
<Box flexDirection="column" flexGrow={1}>
|
|
672
686
|
<Box flexDirection="row" justifyContent="space-between">
|
|
673
|
-
<
|
|
674
|
-
<Text bold wrap="truncate-end">{crab.title}</Text>
|
|
675
|
-
<Text dimColor> ↗</Text>
|
|
676
|
-
</Button>
|
|
687
|
+
<Text bold wrap="truncate-end">{crab.title}</Text>
|
|
677
688
|
{mark}
|
|
678
689
|
</Box>
|
|
679
690
|
<Text>
|
|
@@ -685,7 +696,9 @@ export const register: Register = on => {
|
|
|
685
696
|
<Text dimColor>{stats}</Text>
|
|
686
697
|
</Box>
|
|
687
698
|
{progress(share, tier.color)}
|
|
699
|
+
{e.surface === 'terminal' ? opener : ''}
|
|
688
700
|
</Box>
|
|
701
|
+
{e.surface === 'terminal' ? '' : opener}
|
|
689
702
|
</Box>
|
|
690
703
|
)
|
|
691
704
|
}
|
|
@@ -702,7 +715,7 @@ export const register: Register = on => {
|
|
|
702
715
|
{plan.length > 0 && <Text dimColor>Planned · {plan.length}</Text>}
|
|
703
716
|
{plan.map(item => (
|
|
704
717
|
<Box flexDirection="row" columnGap={2} paddingY={1}>
|
|
705
|
-
{sprite(roleOf(item.content, ''),
|
|
718
|
+
{sprite(roleOf(item.content, ''), 'planned', String(item.index))}
|
|
706
719
|
<Box flexDirection="column" flexGrow={1}>
|
|
707
720
|
<Box flexDirection="row" justifyContent="space-between">
|
|
708
721
|
<Text bold dimColor wrap="truncate-end">{item.index}. {item.content}</Text>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// packages/g3n-mods/mods/custom-crab-agents/hooks/sprite.ts
|
|
2
|
+
import type { Role } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The crab sprite, shared by the hooks module, which draws it as an Svg on surfaces without the mod's view, and by
|
|
6
|
+
* the view in `view/view.ts`. Plain data and string building only, so both can import it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Size of one sprite pixel, in CSS pixels. */
|
|
10
|
+
export const PIXEL = 4
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The body: Claude's mascot as the Claude Code logo draws it, 16 pixels wide. A wide body, two dark eye slits,
|
|
14
|
+
* arms out to both sides, four short legs. Each letter is a palette color, `.` is empty.
|
|
15
|
+
*/
|
|
16
|
+
export const BODY = [
|
|
17
|
+
'..bbbbbbbbbbbb..',
|
|
18
|
+
'..bbkbbbbbbkbb..',
|
|
19
|
+
'..bbkbbbbbbkbb..',
|
|
20
|
+
'CCbbbbbbbbbbbbCC',
|
|
21
|
+
'..bbbbbbbbbbbb..',
|
|
22
|
+
'...b.b....b.b...',
|
|
23
|
+
'...b.b....b.b...',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** Each role's hat, four rows over the body, 12 pixels wide and centred on it: an inspector's deerstalker, a work helmet, a top hat, and so on. */
|
|
27
|
+
export const HATS: Readonly<Record<Role, readonly string[]>> = {
|
|
28
|
+
review: ['............', '....hhhh....', '...hHhHhh...', '.hhhhhhhhhh.'],
|
|
29
|
+
test: ['.....aa.....', '...yyyyyy...', '..yyyyyyyy..', '.yyyyyyyyyy.'],
|
|
30
|
+
design: ['...tttttt...', '...tttttt...', '...vvvvvv...', '.tttttttttt.'],
|
|
31
|
+
explore: ['............', '....pppp....', '...pppppp...', '.pppppppppp.'],
|
|
32
|
+
plan: ['............', '.mmmmmmmmmmq', '...mmmmmm..q', '...mmmmmm...'],
|
|
33
|
+
build: ['............', '...gggg.....', '..gggggg....', '..ggggggggg.'],
|
|
34
|
+
other: ['............', '...wwwwww...', '..wwwwwwww..', '..uuuuuuuu..'],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What each role holds in its right arm, four pixels wide beside the body, by sprite row; the arm is on row 7. */
|
|
38
|
+
export const PROPS: Readonly<Record<Role, Readonly<Record<number, string>>>> = {
|
|
39
|
+
review: { 3: '.oo.', 4: 'oLLo', 5: 'oLLo', 6: '.oo.', 7: 's...' },
|
|
40
|
+
test: { 3: '.w.w', 4: '.www', 5: '..w.', 6: '.w..', 7: 'w...' },
|
|
41
|
+
design: { 2: '...P', 3: '..P.', 4: '..f.', 5: '.s..', 6: '.s..', 7: 's...' },
|
|
42
|
+
explore: { 2: '.rrr', 3: '.rrr', 4: '.s..', 5: '.s..', 6: '.s..', 7: 'ss..' },
|
|
43
|
+
plan: { 5: 'eee.', 6: 'eEe.', 7: 'eee.' },
|
|
44
|
+
build: { 3: 'mmm.', 4: 'mmm.', 5: '.s..', 6: '.s..', 7: 's...' },
|
|
45
|
+
other: {},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Size of the whole sprite in pixels: the 16-pixel body and the 4-pixel item it holds, under a 4-row hat. */
|
|
49
|
+
export const SPRITE_COLUMNS = 20
|
|
50
|
+
export const SPRITE_ROWS = 11
|
|
51
|
+
|
|
52
|
+
export const PALETTE: Readonly<Record<string, string>> = {
|
|
53
|
+
b: '#d97757', C: '#c8613f', k: '#1e1e1e',
|
|
54
|
+
g: '#3fa66b', w: '#f0f0f0', u: '#4c8bf5', y: '#f2c14e', a: '#fff3b0',
|
|
55
|
+
h: '#8b6b3e', H: '#c9a46a', t: '#22252b', v: '#9b59d0', p: '#d8c38f',
|
|
56
|
+
m: '#2f3542', q: '#f2c14e', o: '#c9d1d9', L: '#9fd3e6', s: '#7a4a2a',
|
|
57
|
+
P: '#4c8bf5', f: '#c9d1d9', r: '#e05a4f', e: '#efe6c8', E: '#b8a77a',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The sprite rows of the legs, below the hat and the body above them, and the second leg pose of a walking crab. */
|
|
61
|
+
const LEG_ROWS = [9, 10]
|
|
62
|
+
const STEP_LEGS = '..b...b..b...b..'
|
|
63
|
+
|
|
64
|
+
/** The sprite cells of the eyes, as column and row, which close for a moment when the crab blinks. */
|
|
65
|
+
const EYES: ReadonlyArray<[number, number]> = [[4, 5], [4, 6], [11, 5], [11, 6]]
|
|
66
|
+
|
|
67
|
+
/** How a crab is drawn: walking while its agent runs, standing and blinking once done, grey while only planned. */
|
|
68
|
+
export type Pose = 'running' | 'idle' | 'planned'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Turns an id into a start offset for the crab's animations, so crabs side by side do not move in step.
|
|
72
|
+
*
|
|
73
|
+
* @param id - The crab's id, e.g. its Agent call's tool_use_id.
|
|
74
|
+
* @returns An offset in seconds, from 0 up to 4.
|
|
75
|
+
*/
|
|
76
|
+
export function phaseOf(id: string): number {
|
|
77
|
+
let hash = 0
|
|
78
|
+
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0
|
|
79
|
+
return (hash % 4000) / 1000
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Draws pixels as SVG rects, one per colored cell.
|
|
84
|
+
*
|
|
85
|
+
* @param rows - Sprite rows; each letter is a `PALETTE` color, `.` is empty.
|
|
86
|
+
* @param only - The row numbers to draw; all when absent.
|
|
87
|
+
* @returns The rects as markup.
|
|
88
|
+
*/
|
|
89
|
+
function pixels(rows: readonly string[], only?: readonly number[]): string {
|
|
90
|
+
const rects: string[] = []
|
|
91
|
+
rows.forEach((row, y) => {
|
|
92
|
+
if (only !== undefined && !only.includes(y)) return
|
|
93
|
+
for (let x = 0; x < row.length; x++) {
|
|
94
|
+
const color = PALETTE[row.charAt(x)]
|
|
95
|
+
if (color !== undefined) rects.push(`<rect x="${x * PIXEL}" y="${y * PIXEL}" width="${PIXEL}" height="${PIXEL}" fill="${color}"/>`)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
return rects.join('')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @brief Draws the crab as one SVG: its role's hat over the body and the item it holds. A running crab bobs and walks,
|
|
103
|
+
* every crab but a planned one blinks now and then, and a planned crab is drawn grey and still.
|
|
104
|
+
*
|
|
105
|
+
* The motion is the SVG's own animation (SMIL), so it needs nothing from the surface drawing it.
|
|
106
|
+
*
|
|
107
|
+
* @param role - Role whose hat and item to draw.
|
|
108
|
+
* @param pose - Walking, standing or planned.
|
|
109
|
+
* @param phase - Seconds into its animations the crab starts, see `phaseOf`.
|
|
110
|
+
* @returns The SVG markup as a string.
|
|
111
|
+
*/
|
|
112
|
+
export function crabSvg(role: Role, pose: Pose, phase = 0): string {
|
|
113
|
+
const rows = [...HATS[role].map(row => `..${row}..`), ...BODY].map((row, y) => row + (PROPS[role][y] ?? '....'))
|
|
114
|
+
const width = SPRITE_COLUMNS * PIXEL
|
|
115
|
+
const height = SPRITE_ROWS * PIXEL
|
|
116
|
+
// crispEdges keeps the pixels sharp, with no seams between them, while the crab bobs.
|
|
117
|
+
const open = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges"`
|
|
118
|
+
if (pose === 'planned') return `${open} opacity="0.45" style="filter:grayscale(1)">${pixels(rows)}</svg>`
|
|
119
|
+
|
|
120
|
+
const begin = `begin="-${phase.toFixed(2)}s"`
|
|
121
|
+
const above = rows.map((_, y) => y).filter(y => !LEG_ROWS.includes(y))
|
|
122
|
+
const eyelids = EYES.map(([x, y]) => `<rect x="${x * PIXEL}" y="${y * PIXEL}" width="${PIXEL}" height="${PIXEL}" fill="${PALETTE.b}"/>`).join('')
|
|
123
|
+
const blink = `<g opacity="0">${eyelids}<animate attributeName="opacity" values="0;1;0" keyTimes="0;0.94;0.97" calcMode="discrete" dur="${pose === 'running' ? 3.2 : 4.6}s" repeatCount="indefinite" ${begin}/></g>`
|
|
124
|
+
if (pose === 'idle') return `${open}>${pixels(rows)}${blink}</svg>`
|
|
125
|
+
|
|
126
|
+
// Running: the body bobs half a pixel and the legs swap between two poses, as a walk.
|
|
127
|
+
const stepped = rows.map((row, y) => (LEG_ROWS.includes(y) ? STEP_LEGS + row.slice(STEP_LEGS.length) : row))
|
|
128
|
+
const swap = (from: number) => `<animate attributeName="opacity" values="${from};${1 - from}" calcMode="discrete" dur="0.5s" repeatCount="indefinite" ${begin}/>`
|
|
129
|
+
const bob = `<animateTransform attributeName="transform" type="translate" values="0 0;0 -2;0 0" dur="0.5s" repeatCount="indefinite" ${begin}/>`
|
|
130
|
+
return `${open}><g>${pixels(rows, above)}${blink}${bob}</g><g>${pixels(rows, LEG_ROWS)}${swap(1)}</g><g opacity="0">${pixels(stepped, LEG_ROWS)}${swap(0)}</g></svg>`
|
|
131
|
+
}
|
|
@@ -49,6 +49,41 @@ export type ChatPane = {
|
|
|
49
49
|
title: string
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** One crab as the view draws it. */
|
|
53
|
+
export type CrabView = {
|
|
54
|
+
id: string
|
|
55
|
+
title: string
|
|
56
|
+
role: Role
|
|
57
|
+
roleName: string
|
|
58
|
+
/** The bar's color, from the effort tier */
|
|
59
|
+
color: string
|
|
60
|
+
model: string
|
|
61
|
+
effort?: string
|
|
62
|
+
status: 'running' | 'finished' | 'failed'
|
|
63
|
+
/** e.g. `3/5 · Writing hero copy`, or `step 4` */
|
|
64
|
+
stepLine: string
|
|
65
|
+
/** e.g. `ctx 4% · 38k · ≈$0.05 · 0:26` */
|
|
66
|
+
stats: string
|
|
67
|
+
/** Filled part of the bar, from 0 to 1 */
|
|
68
|
+
share: number
|
|
69
|
+
/** Seconds into its animations the crab starts, so neighbours do not move in step */
|
|
70
|
+
phase: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One planned item as the view draws it: a grey crab. */
|
|
74
|
+
export type PlannedView = {
|
|
75
|
+
index: number
|
|
76
|
+
content: string
|
|
77
|
+
after: number[]
|
|
78
|
+
role: Role
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What the crabs' view gets. */
|
|
82
|
+
export type CrabsViewData = {
|
|
83
|
+
crabs: CrabView[]
|
|
84
|
+
planned: PlannedView[]
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
declare module 'claude-code' {
|
|
53
88
|
interface PluginState {
|
|
54
89
|
'custom-crab-agents': { crabs: Crab[]; planned: PlannedItem[]; window: number; chats: ChatPane[] }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* packages/g3n-mods/mods/custom-crab-agents/view/view.css */
|
|
2
|
+
.cr {
|
|
3
|
+
--dim: var(--vscode-descriptionForeground, #9d9d9d);
|
|
4
|
+
--line: color-mix(in srgb, var(--vscode-foreground) 10%, transparent);
|
|
5
|
+
--mono: var(--vscode-editor-font-family, ui-monospace, monospace);
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 8px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.cr-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
12
|
+
.cr-title { font-weight: 600; font-size: 14px; }
|
|
13
|
+
.cr-count { color: var(--dim); font-size: 12px; }
|
|
14
|
+
|
|
15
|
+
.cr-list { display: flex; flex-direction: column; gap: 4px; }
|
|
16
|
+
|
|
17
|
+
/* A card is one real button: the crab on the left, its text on the right; the whole of it opens the chat. */
|
|
18
|
+
.cr-card {
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-columns: 80px minmax(0, 1fr);
|
|
21
|
+
align-items: center;
|
|
22
|
+
column-gap: 14px;
|
|
23
|
+
width: 100%;
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 10px 12px;
|
|
26
|
+
border: 1px solid transparent;
|
|
27
|
+
border-radius: 10px;
|
|
28
|
+
background: transparent;
|
|
29
|
+
color: inherit;
|
|
30
|
+
font: inherit;
|
|
31
|
+
text-align: left;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
|
34
|
+
}
|
|
35
|
+
button.cr-card:hover { background: color-mix(in srgb, var(--vscode-foreground) 6%, transparent); border-color: var(--line); }
|
|
36
|
+
button.cr-card:active { transform: scale(0.995); background: color-mix(in srgb, var(--vscode-foreground) 9%, transparent); }
|
|
37
|
+
button.cr-card:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; }
|
|
38
|
+
.cr-card.is-planned { cursor: default; opacity: 0.7; }
|
|
39
|
+
|
|
40
|
+
.cr-sprite { display: flex; align-items: flex-end; justify-content: center; height: 44px; }
|
|
41
|
+
.cr-sprite svg { display: block; image-rendering: pixelated; }
|
|
42
|
+
|
|
43
|
+
.cr-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
44
|
+
.cr-top { display: flex; align-items: center; gap: 10px; }
|
|
45
|
+
.cr-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-weight: 600; font-size: 13px; }
|
|
46
|
+
.cr-mark { flex-shrink: 0; margin-left: auto; font-size: 13px; color: #7cc47f; }
|
|
47
|
+
.cr-card[data-status="failed"] .cr-mark { color: #e06c6c; }
|
|
48
|
+
.cr-card[data-status="running"] .cr-mark { width: 8px; height: 8px; border-radius: 50%; background: #3fa66b; box-shadow: 0 0 0 0 color-mix(in srgb, #3fa66b 60%, transparent); animation: cr-pulse 1.6s ease-out infinite; }
|
|
49
|
+
@keyframes cr-pulse { 70% { box-shadow: 0 0 0 6px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } }
|
|
50
|
+
|
|
51
|
+
/* An arrow shows on hover, at the end of the title row, as the card's only sign it opens something. */
|
|
52
|
+
button.cr-card .cr-top::after { content: '↗'; flex-shrink: 0; margin-left: -4px; color: var(--vscode-textLink-foreground); font-size: 12px; opacity: 0; transition: opacity 120ms ease; }
|
|
53
|
+
button.cr-card:hover .cr-top::after, button.cr-card:focus-visible .cr-top::after { opacity: 1; }
|
|
54
|
+
|
|
55
|
+
.cr-meta { display: flex; gap: 8px; font-size: 12px; color: var(--dim); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
56
|
+
.cr-role { color: var(--tier); font-weight: 600; }
|
|
57
|
+
|
|
58
|
+
.cr-line { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
|
|
59
|
+
.cr-step { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
60
|
+
.cr-stats { flex-shrink: 0; color: var(--dim); font-family: var(--mono); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
61
|
+
|
|
62
|
+
.cr-bar { position: relative; height: 4px; margin-top: 3px; border-radius: 2px; background: var(--line); overflow: hidden; }
|
|
63
|
+
.cr-fill { position: absolute; inset: 0 auto 0 0; border-radius: 2px; background: var(--tier); transition: width 400ms ease; }
|
|
64
|
+
.cr-card[data-status="running"] .cr-fill { background-image: linear-gradient(90deg, transparent, color-mix(in srgb, #fff 25%, transparent), transparent); background-size: 60px 100%; background-repeat: no-repeat; background-color: var(--tier); animation: cr-shine 1.8s linear infinite; }
|
|
65
|
+
@keyframes cr-shine { from { background-position: -60px 0; } to { background-position: 260px 0; } }
|
|
66
|
+
|
|
67
|
+
.cr-group { display: flex; flex-direction: column; gap: 4px; }
|
|
68
|
+
.cr-group-label { color: var(--dim); font-size: 12px; padding: 4px 2px; cursor: default; list-style: none; }
|
|
69
|
+
summary.cr-group-label { cursor: pointer; }
|
|
70
|
+
summary.cr-group-label::before { content: '▾'; display: inline-block; width: 1.2em; transition: transform 120ms ease; }
|
|
71
|
+
details:not([open]) > summary.cr-group-label::before { transform: rotate(-90deg); }
|
|
72
|
+
summary.cr-group-label::-webkit-details-marker { display: none; }
|
|
73
|
+
|
|
74
|
+
.cr-empty { margin: 0; color: var(--dim); font-size: 12px; }
|