A side pane that shows the agents working on your requests as illustrated characters

Status (2026-10-02): working prototype, art to be replaced. The agent tracking and the pane layout work, but the owner does not like the vector characters in
hooks/art.js. The plan is to replace them with real pixel art once the owner supplies art sources. Do not polish the vector art further.
A side pane that shows the agents working on your requests as illustrated characters. Built for the Claude desktop app (Code tab), where it draws vector art. In a terminal it falls back to plain symbols.
| Agent type | Character |
|---|---|
| main Claude, Plan | Sage |
| Explore | Scout |
| general-purpose | Smith |
| code-reviewer, security-auditor | Guardian |
| test-engineer | Alchemist |
| anything else | Wisp |
Open it with /party. It also opens on session start.
The characters are original designs drawn as SVG in hooks/art.js, in the style of the commissioned reference art (thick dark outlines, flat cel shading, cyan rim light). They are not copies of any game's characters. To add a character: add a function to CHARACTERS, a line in CAST and NAMES. To preview all characters and states in a browser:
cd mods/party-pane python -m http.server 8765 # open http://127.0.0.1:8765/preview/index.html
claude --plugin-dir ./mods/party-pane
Validate with Claude Code 2.1.286 or newer (see docs/mods/03-workflow.md for the version trap).
hooks/register.tsx 189 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Member } from '../types'
5import { NAMES, characterFor, portrait } from './art.js'
6
7const PANE = 'party'
8const KEEP_DONE_MS = 30000
9const MAX_MEMBERS = 8
10
11const MAIN: Member = {
12 id: 'main',
13 kind: 'main',
14 label: 'Main conversation',
15 status: 'idle',
16 action: 'Waiting for you',
17 endedAt: null,
18}
19
20const members = atom({ plugin: 'party-pane', key: 'members' } as const, [MAIN])
21
22// Descriptions of subagents about to start, oldest first, matched to the
23// SubagentStart that follows by agent type.
24let pending: { type: string; description: string }[] = []
25
26// Changes one member, adding it first if it is new.
27async function patch($: EngineInterface, id: string, change: Partial<Member>, fallback?: Member) {
28 await update($, members, list => {
29 const has = list.some(m => m.id === id)
30 const base = has ? list : fallback ? [...list, fallback] : list
31 return base.map(m => (m.id === id ? { ...m, ...change } : m)).slice(-MAX_MEMBERS)
32 })
33}
34
35// Drops finished agents once they have been on show for a while.
36async function prune($: EngineInterface) {
37 const now = await $.clock.now()
38 const list = await read($, members)
39 const kept = list.filter(m => m.endedAt === null || now - m.endedAt < KEEP_DONE_MS)
40 if (kept.length !== list.length) await update($, members, () => kept)
41}
42
43// One short phrase for what a tool call is doing.
44function describeCall(call: Record<string, unknown>): string {
45 const text = (key: string) => (typeof call[key] === 'string' ? (call[key] as string) : '')
46 const base = (path: string) => path.split(/[\\/]/).pop() ?? path
47 const clip = (s: string) => (s.length > 48 ? s.slice(0, 47) + '…' : s)
48 switch (call.tool) {
49 case 'Bash':
50 return clip('Running ' + text('command').split('\n')[0])
51 case 'Read':
52 return 'Reading ' + base(text('file_path'))
53 case 'Edit':
54 case 'Write':
55 return 'Writing ' + base(text('file_path'))
56 case 'Grep':
57 return clip('Searching for ' + text('pattern'))
58 case 'Glob':
59 return clip('Finding ' + text('pattern'))
60 case 'Agent':
61 return 'Calling in help'
62 default:
63 return String(call.tool ?? 'Working')
64 }
65}
66
67export const register: Register = on => {
68 on('session.start', async ($, e, next) => {
69 await $.command.register({ name: 'party', description: 'Open the party pane' })
70 void $.ui.open({ id: PANE, title: 'Party' })
71 $.clock.every(5000, () => prune($))
72 return next(e)
73 })
74
75 on('command.run', { command: 'party' }, async $ => {
76 await $.ui.open({ id: PANE, title: 'Party' })
77 return { text: 'Party pane opened.' }
78 })
79
80 on('prompt.submit', async ($, e, next) => {
81 await patch($, 'main', { status: 'working', action: 'Thinking', endedAt: null })
82 return next(e)
83 })
84
85 on('turn.complete', async ($, e, next) => {
86 // A subagent's own turn also ends here; the SubagentStop hook handles those.
87 if (!(e as { agentId?: string }).agentId) {
88 await patch($, 'main', { status: 'idle', action: 'Waiting for you' })
89 }
90 return next(e)
91 })
92
93 on('agent.spawn', async ($, e, next) => {
94 pending = [...pending, { type: e.subagentType, description: e.description }]
95 return next(e)
96 })
97
98 on('classic.SubagentStart', async ($, e, next) => {
99 const at = pending.findIndex(p => p.type === e.agent_type)
100 const label = at >= 0 ? pending[at].description : e.agent_type
101 if (at >= 0) pending = pending.filter((_, i) => i !== at)
102 await patch(
103 $,
104 e.agent_id,
105 { status: 'working', action: 'Getting started', endedAt: null },
106 { id: e.agent_id, kind: e.agent_type, label, status: 'working', action: '', endedAt: null },
107 )
108 return next(e)
109 })
110
111 on('classic.SubagentStop', async ($, e, next) => {
112 await patch($, e.agent_id, { status: 'done', action: 'Finished', endedAt: await $.clock.now() })
113 return next(e)
114 })
115
116 on('tool.call', async ($, e, next) => {
117 const agentId = (e as { agentId?: string }).agentId
118 const id = agentId ?? 'main'
119 await patch($, id, { status: 'working', action: describeCall(e as Record<string, unknown>) })
120 const result = await next(e)
121 return result
122 })
123
124 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
125 const { Box, Text, Button, Svg } = $.ui.resolve(e)
126 const list = await read($, members)
127 const busy = list.filter(m => m.status === 'working').length
128
129 const claudeIsBusy = list.some(m => m.id === 'main' && m.status === 'working')
130 // You sit at the top; the ring shows whose turn it is.
131 const you: Member = {
132 id: 'you',
133 kind: 'you',
134 label: 'The boss',
135 status: claudeIsBusy ? 'idle' : 'working',
136 action: claudeIsBusy ? 'Waiting on Claude' : 'Your move',
137 endedAt: null,
138 }
139 const ordered = [
140 you,
141 ...[...list].sort((a, b) => {
142 if (a.id === 'main') return -1
143 if (b.id === 'main') return 1
144 return a.status === 'working' ? -1 : b.status === 'working' ? 1 : 0
145 }),
146 ]
147
148 return (
149 <Box flexDirection="column" rowGap={1}>
150 <Box flexDirection="row" columnGap={2}>
151 <Text bold>Party</Text>
152 <Text dimColor>{busy === 0 ? 'all quiet' : busy + ' at work'}</Text>
153 <Button
154 key="clear"
155 label="Clear finished"
156 onPress={() => update($, members, l => l.filter(m => m.endedAt === null))}
157 />
158 </Box>
159 {ordered.map(m => {
160 const kind = m.kind === 'you' ? 'you' : m.kind === 'main' ? 'sage' : characterFor(m.kind)
161 const name = m.kind === 'main' ? 'Claude' : NAMES[kind]
162 return (
163 <Box key={m.id} flexDirection="row" columnGap={1}>
164 {Svg ? (
165 <Svg
166 alt={name + ' is ' + m.status}
167 width={72}
168 height={76}
169 isInteractive={true}
170 source={portrait(kind, m.status)}
171 />
172 ) : (
173 <Text>{m.status === 'working' ? '●' : m.status === 'done' ? '✓' : '○'}</Text>
174 )}
175 <Box flexDirection="column">
176 <Text bold>{name}</Text>
177 <Text dimColor wrap="truncate">{m.label}</Text>
178 <Text color={m.status === 'working' ? 'success' : undefined} dimColor={m.status !== 'working'} wrap="truncate">
179 {m.action}
180 </Text>
181 </Box>
182 </Box>
183 )
184 })}
185 </Box>
186 )
187 })
188}
189hooks/art.js 206 lines1// Vector busts for the party pane. Each character is an original design in the
2// commissioned style: thick dark outlines, flat cel shading, cyan rim light.
3const O = '#1b2830'
4const RIM = '#37e8e6'
5const S = `stroke="${O}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"`
6const T = `stroke="${O}" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"`
7
8// One function per character. Each returns the drawing's inner markup for a
9// 120 x 120 box; the head sits in a group with class "head" so it can bob
10// on its own while the shoulders stay put.
11const CHARACTERS = {
12 // The orchestrating Claude: a white-bearded sage with one glowing eye.
13 sage: () => `
14 <path d="M6 121 C8 94 30 84 60 84 C90 84 112 94 114 121Z" fill="#2f4f8f" ${S}/>
15 <path d="M6 121 C8 100 20 90 36 86 C28 98 28 110 30 121Z" fill="#223a70"/>
16 <path d="M44 86 L60 104 L76 86" fill="#e9c441" ${S}/>
17 <g class="head">
18 <path d="M25 62 C17 28 40 7 62 7 C88 7 103 30 95 62 C97 84 84 100 60 104 C36 100 23 84 25 62Z" fill="#e3eff5" ${S}/>
19 <path d="M25 62 C17 28 40 7 62 7" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
20 <path d="M76 12 C90 22 98 40 94 62 C92 50 88 38 76 12Z" fill="#a9bdc8"/>
21 <path d="M41 46 C41 33 79 33 79 46 L77 66 C73 77 47 77 43 66Z" fill="#dcb9c8" ${T}/>
22 <path d="M32 44 C38 18 82 14 90 44 C80 32 52 28 32 44Z" fill="#ffffff" ${T}/>
23 <path d="M36 64 C36 92 50 108 60 109 C70 108 84 92 84 64 C76 77 68 71 60 78 C52 71 44 77 36 64Z" fill="#e3eff5" ${S}/>
24 <path d="M52 86 C54 96 57 102 60 106 M68 86 C66 96 63 102 60 106" fill="none" stroke="#9db3be" stroke-width="2.2" stroke-linecap="round"/>
25 <path d="M45 63 C52 57 58 62 60 67 C62 62 68 57 75 63 C70 73 62 71 60 69 C58 71 50 73 45 63Z" fill="#ffffff" ${T}/>
26 <path d="M44 46 L55 49 M76 46 L66 49" stroke="#7f929c" stroke-width="3.2" stroke-linecap="round"/>
27 <circle class="glow" cx="50" cy="52" r="4.6" fill="#7ff6ff" stroke="${O}" stroke-width="1.6"/>
28 <path d="M66 53 Q72 57 78 53" fill="none" stroke="${O}" stroke-width="2.6" stroke-linecap="round"/>
29 <path d="M71 44 L77 60" stroke="#8d5a6e" stroke-width="2.2" stroke-linecap="round"/>
30 </g>`,
31
32 // Explore: a hooded scout with glowing goggle lenses.
33 scout: () => `
34 <path d="M8 121 C10 96 30 86 60 86 C90 86 110 96 112 121Z" fill="#1d7f87" ${S}/>
35 <path d="M8 121 C10 102 22 92 38 88 C30 100 30 112 32 121Z" fill="#145a61"/>
36 <path d="M36 88 C46 100 74 100 84 88 L86 100 C74 110 46 110 34 100Z" fill="#e6a53a" ${S}/>
37 <g class="head">
38 <path d="M26 70 C18 30 40 8 60 8 C82 8 102 30 94 70 C90 88 78 94 60 94 C42 94 30 88 26 70Z" fill="#1d7f87" ${S}/>
39 <path d="M26 70 C18 30 40 8 60 8" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
40 <path d="M38 52 C38 38 82 38 82 52 L80 72 C74 84 46 84 40 72Z" fill="#c78b6e" ${T}/>
41 <path d="M36 50 C38 36 82 36 84 50 C78 44 44 44 36 50Z" fill="#0f3d43"/>
42 <path d="M36 54 L84 54 L82 60 L38 60Z" fill="#26343c" ${T}/>
43 <circle class="glow" cx="49" cy="57" r="8" fill="#7ff6ff" stroke="#e9b83a" stroke-width="3.2"/>
44 <circle class="glow" cx="71" cy="57" r="8" fill="#7ff6ff" stroke="#e9b83a" stroke-width="3.2"/>
45 <circle cx="46.5" cy="54.5" r="2.4" fill="#fff"/><circle cx="68.5" cy="54.5" r="2.4" fill="#fff"/>
46 <path d="M52 74 Q60 79 68 74" fill="none" stroke="${O}" stroke-width="2.6" stroke-linecap="round"/>
47 </g>`,
48
49 // Code writing: a horned, orange-bearded smith in an iron helm.
50 smith: () => `
51 <path d="M6 121 C8 96 28 86 60 86 C92 86 112 96 114 121Z" fill="#7d8b96" ${S}/>
52 <path d="M6 121 C8 102 20 92 36 88 C28 100 28 112 30 121Z" fill="#5b6872"/>
53 <circle cx="26" cy="100" r="6" fill="#e9c441" ${T}/><circle cx="94" cy="100" r="6" fill="#e9c441" ${T}/>
54 <g class="head">
55 <path d="M30 36 C16 30 10 16 14 6 C26 10 34 20 40 30Z" fill="#f3e8c6" ${S}/>
56 <path d="M90 36 C104 30 110 16 106 6 C94 10 86 20 80 30Z" fill="#f3e8c6" ${S}/>
57 <path d="M28 66 C22 90 40 112 60 112 C80 112 98 90 92 66 C84 78 70 74 60 80 C50 74 36 78 28 66Z" fill="#e0782c" ${S}/>
58 <path d="M28 66 C22 90 40 112 60 112" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
59 <path d="M44 96 C50 104 56 108 60 110 M76 96 C70 104 64 108 60 110" fill="none" stroke="#a8480f" stroke-width="2.4" stroke-linecap="round"/>
60 <path d="M36 52 C36 40 84 40 84 52 L82 70 C76 80 44 80 38 70Z" fill="#e3ab8c" ${T}/>
61 <path d="M26 46 C24 18 44 8 60 8 C76 8 96 18 94 46 L84 44 C80 32 40 32 36 44Z" fill="#8b98a3" ${S}/>
62 <path d="M26 46 C24 18 44 8 60 8" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
63 <path d="M56 8 L64 8 L64 42 L56 42Z" fill="#b6c2cb" ${T}/>
64 <path d="M43 54 L55 57 M77 54 L65 57" stroke="#a8480f" stroke-width="3.4" stroke-linecap="round"/>
65 <circle class="glow" cx="49" cy="61" r="3.6" fill="#1b2830"/><circle class="glow" cx="71" cy="61" r="3.6" fill="#1b2830"/>
66 <path d="M47 76 Q60 84 73 76 Q60 80 47 76Z" fill="#fff6e0" ${T}/>
67 </g>`,
68
69 // Review: a gold-masked guardian with a purple plume and glaring red eyes.
70 guardian: () => `
71 <path d="M8 121 C10 96 30 86 60 86 C90 86 110 96 112 121Z" fill="#5b2d96" ${S}/>
72 <path d="M8 121 C10 102 22 92 38 88 C30 100 30 112 32 121Z" fill="#3e1f6b"/>
73 <path d="M34 90 L60 108 L86 90 L80 80 L40 80Z" fill="#e9b82e" ${S}/>
74 <g class="head">
75 <path d="M18 56 L4 28 L28 40Z" fill="#2fc4d6" ${S}/><path d="M102 56 L116 28 L92 40Z" fill="#2fc4d6" ${S}/>
76 <path d="M30 40 L24 6 L44 28 L52 2 L60 26 L68 2 L76 28 L96 6 L90 40Z" fill="#7b3fb8" ${S}/>
77 <path d="M26 54 C26 34 94 34 94 54 C96 76 80 100 60 106 C40 100 24 76 26 54Z" fill="#eab72d" ${S}/>
78 <path d="M26 54 C26 36 44 30 60 30" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
79 <path d="M46 28 C54 22 66 22 74 28 L72 44 C66 40 54 40 48 44Z" fill="#f3d36a" ${T}/>
80 <circle cx="60" cy="33" r="4.4" fill="#e03131" stroke="${O}" stroke-width="2"/>
81 <path d="M33 56 L53 62 L51 70 L35 64Z" fill="#1b2830"/><path d="M87 56 L67 62 L69 70 L85 64Z" fill="#1b2830"/>
82 <path class="glow" d="M38 59 L51 63 L50 67 L39 63Z" fill="#ff4a4a"/><path class="glow" d="M82 59 L69 63 L70 67 L81 63Z" fill="#ff4a4a"/>
83 <path d="M40 84 Q60 98 80 84 L76 92 Q60 102 44 92Z" fill="#fff" ${T}/>
84 <path d="M48 87 L50 92 M55 90 L56 95 M65 90 L64 95 M72 87 L70 92" stroke="${O}" stroke-width="1.8" stroke-linecap="round"/>
85 </g>`,
86
87 // Tests: a blue-mohawked goblin alchemist holding a bubbling flask.
88 alchemist: () => `
89 <path d="M10 121 C12 98 30 88 60 88 C90 88 108 98 110 121Z" fill="#2c3a44" ${S}/>
90 <path d="M10 121 C12 104 22 94 38 90 C30 102 30 112 32 121Z" fill="#1e2a31"/>
91 <g class="head">
92 <path d="M44 22 C40 10 48 2 52 0 C54 8 56 12 58 18 C60 8 64 2 70 0 C72 10 72 16 70 22 C66 24 50 24 44 22Z" fill="#2fb0e0" ${S}/>
93 <path d="M26 56 L8 46 L28 70Z" fill="#7bc65a" ${S}/><path d="M94 56 L112 46 L92 70Z" fill="#7bc65a" ${S}/>
94 <path d="M28 52 C28 30 92 30 92 52 L90 74 C82 92 38 92 30 74Z" fill="#7bc65a" ${S}/>
95 <path d="M28 52 C28 34 44 28 60 28" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
96 <path d="M36 80 C40 100 50 108 60 108 C70 108 80 100 84 80 C76 88 68 86 60 90 C52 86 44 88 36 80Z" fill="#8fb2c4" ${S}/>
97 <path d="M32 40 C40 30 80 30 88 40 L86 48 C76 42 44 42 34 48Z" fill="#e9b82e" ${S}/>
98 <circle cx="46" cy="42" r="9" fill="#c8f4ff" stroke="${O}" stroke-width="3"/><circle cx="74" cy="42" r="9" fill="#c8f4ff" stroke="${O}" stroke-width="3"/>
99 <circle class="glow" cx="47" cy="58" r="5" fill="#fff6a8" stroke="${O}" stroke-width="2.2"/><circle class="glow" cx="73" cy="58" r="5" fill="#fff6a8" stroke="${O}" stroke-width="2.2"/>
100 <circle cx="48" cy="59" r="2.2" fill="${O}"/><circle cx="72" cy="59" r="2.2" fill="${O}"/>
101 <path d="M48 72 Q60 80 72 72" fill="#fff" ${T}/>
102 </g>
103 <path d="M80 98 L80 86 L96 86 L96 98 L102 114 C102 120 74 120 74 114Z" fill="#a6f06a" ${S}/>
104 <path d="M78 106 C86 102 92 110 100 106" fill="none" stroke="#e8ffd0" stroke-width="2.4" stroke-linecap="round"/>`,
105
106 // You: the bearded, black-haired boss with the cream streak, from the
107 // commissioned head art.
108 you: () => `
109 <path d="M8 121 C10 96 30 88 60 88 C90 88 110 96 112 121Z" fill="#26262e" ${S}/>
110 <path d="M8 121 C10 102 20 94 34 90 C28 102 28 112 30 121Z" fill="#0fb5b5"/>
111 <path d="M46 82 L46 98 C54 106 66 106 74 98 L74 82Z" fill="#b9808a" ${T}/>
112 <g class="head">
113 <ellipse cx="25" cy="62" rx="7" ry="10" fill="#c98f86" ${T}/>
114 <path d="M20 56 C19 64 21 70 25 72" fill="none" stroke="${RIM}" stroke-width="3" stroke-linecap="round"/>
115 <ellipse cx="95" cy="62" rx="6" ry="9" fill="#dca593" ${T}/>
116 <path d="M28 50 C28 14 92 14 92 50 L91 70 C86 94 34 94 29 70Z" fill="#dca593" ${S}/>
117 <path d="M29 50 C28 70 31 84 42 92 C34 80 36 62 40 40Z" fill="#c4878b"/>
118 <path d="M30 66 C28 94 44 110 60 110 C76 110 92 94 90 66 C84 80 72 78 60 84 C48 78 36 80 30 66Z" fill="#1c1c25" ${S}/>
119 <path d="M88 72 C90 88 84 102 74 108" fill="none" stroke="#f2edc8" stroke-width="3" stroke-linecap="round"/>
120 <path d="M42 76 C50 71 56 75 60 78 C64 75 70 71 78 76 C74 82 64 81 60 80 C56 81 46 82 42 76Z" fill="#1c1c25" ${T}/>
121 <path d="M49 86 Q60 99 71 86 Q60 91 49 86Z" fill="#d58a7a" ${T}/>
122 <path d="M52 87 Q60 94 68 87 Q60 90 52 87Z" fill="#3a0d0d"/>
123 <path d="M53 87 Q60 89 67 87 L66 89 Q60 91 54 89Z" fill="#fff"/>
124 <ellipse cx="48" cy="58" rx="8" ry="6" fill="#eef3f6" ${T}/><ellipse cx="73" cy="58" rx="8" ry="6" fill="#eef3f6" ${T}/>
125 <circle class="glow" cx="50" cy="59" r="3.8" fill="#3b2a24"/><circle class="glow" cx="75" cy="59" r="3.8" fill="#3b2a24"/>
126 <circle cx="51.5" cy="57.5" r="1.4" fill="#fff"/><circle cx="76.5" cy="57.5" r="1.4" fill="#fff"/>
127 <path d="M37 50 Q47 43 57 50 M64 50 Q74 43 84 50" fill="none" stroke="#17161c" stroke-width="4" stroke-linecap="round"/>
128 <path d="M62 60 L58 70 Q61 73 65 70" fill="none" stroke="#a86a62" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
129 <path d="M22 54 C12 30 28 10 50 8 C58 2 74 4 82 10 C98 14 102 32 98 52 C94 38 86 32 76 30 C60 26 44 28 36 34 C30 40 27 46 22 54Z" fill="#17161c" ${S}/>
130 <path d="M22 54 C12 30 28 10 50 8" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
131 <path d="M56 8 L63 -3 L72 8Z" fill="#17161c" ${T}/>
132 <path d="M70 9 C86 12 98 24 98 44 C94 32 86 24 74 20 C68 18 66 12 70 9Z" fill="#f2edc8" ${T}/>
133 </g>`,
134
135 // Fallback: a floating spirit with a red jewel eye and crescent horns.
136 wisp: () => `
137 <g class="head">
138 <path d="M22 70 C8 50 14 24 34 14 C26 34 32 50 44 58Z" fill="#7fd8f0" ${S}/>
139 <path d="M98 70 C112 50 106 24 86 14 C94 34 88 50 76 58Z" fill="#7fd8f0" ${S}/>
140 <path d="M60 24 C84 24 96 46 92 70 C88 94 72 108 60 108 C48 108 32 94 28 70 C24 46 36 24 60 24Z" fill="#e8f7fc" ${S}/>
141 <path d="M28 70 C24 46 36 24 60 24" fill="none" stroke="${RIM}" stroke-width="3.5" stroke-linecap="round"/>
142 <path d="M72 30 C88 40 92 60 88 78 C82 56 80 44 72 30Z" fill="#b9dbe6"/>
143 <circle class="glow" cx="60" cy="58" r="12" fill="#e03131" stroke="${O}" stroke-width="3"/>
144 <circle cx="56" cy="54" r="3.6" fill="#fff"/>
145 <path d="M44 84 Q60 92 76 84" fill="none" stroke="${O}" stroke-width="2.6" stroke-linecap="round"/>
146 </g>`,
147}
148
149// Which character plays which agent type. Anything unknown is a wisp.
150export const CAST = {
151 main: 'sage',
152 Explore: 'scout',
153 'general-purpose': 'smith',
154 Plan: 'sage',
155 'code-reviewer': 'guardian',
156 'agent-skills:code-reviewer': 'guardian',
157 'agent-skills:security-auditor': 'guardian',
158 'agent-skills:test-engineer': 'alchemist',
159}
160
161export const NAMES = {
162 sage: 'Sage',
163 scout: 'Scout',
164 smith: 'Smith',
165 guardian: 'Guardian',
166 alchemist: 'Alchemist',
167 you: 'You',
168 wisp: 'Wisp',
169}
170
171export function characterFor(type) {
172 return CAST[type] ?? 'wisp'
173}
174
175// state: 'working' | 'done' | 'failed' | 'idle'
176export function portrait(kind, state) {
177 const body = (CHARACTERS[kind] ?? CHARACTERS.wisp)()
178 const badge =
179 state === 'done'
180 ? `<circle cx="100" cy="100" r="13" fill="#3ccf6e" ${S}/><path d="M93 100 L98 106 L108 94" fill="none" stroke="#fff" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>`
181 : state === 'failed'
182 ? `<circle cx="100" cy="100" r="13" fill="#e5484d" ${S}/><path d="M100 92 L100 102 M100 107 L100 108" stroke="#fff" stroke-width="4.5" stroke-linecap="round"/>`
183 : ''
184 const ring =
185 state === 'working'
186 ? `<ellipse class="ring" cx="60" cy="116" rx="44" ry="6" fill="none" stroke="${RIM}" stroke-width="3"/>`
187 : ''
188 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-4 -6 128 134" class="${state}">
189 <style>
190 .head{transform-origin:60px 100px;animation:bob 3.2s ease-in-out infinite}
191 .working .head{animation:work .55s ease-in-out infinite}
192 .done .head{animation:none}
193 .failed .head{animation:shake .5s ease-in-out 2}
194 .glow{animation:glow 1.6s ease-in-out infinite}
195 .done .glow,.failed .glow{animation:none}
196 .ring{animation:ring 1.1s ease-out infinite;transform-origin:60px 116px}
197 @keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
198 @keyframes work{0%,100%{transform:translateY(0) rotate(-1.5deg)}50%{transform:translateY(-3px) rotate(1.5deg)}}
199 @keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-3px)}75%{transform:translateX(3px)}}
200 @keyframes glow{0%,100%{opacity:1}50%{opacity:.55}}
201 @keyframes ring{0%{opacity:.9;transform:scale(.6)}100%{opacity:0;transform:scale(1.15)}}
202 </style>
203 ${ring}${body}${badge}
204</svg>`
205}
206types/index.d.ts 15 lines1export type Member = {
2 id: string
3 kind: string
4 label: string
5 status: 'idle' | 'working' | 'done' | 'failed'
6 action: string
7 endedAt: number | null
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'party-pane': { members: Member[] }
13 }
14}
15