SLOPSHOPPER

party-pane

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

newpaneguardcommandprompttimer
v0.1.0no licenseupdated 2026-10-03shuletadev/certbuddycr/mods/party-pane
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · party-pane
│ ┃ Party ✕ › fix the failing auth test and add an audit log call │ ┃ Party all quiet [ Clear finished ] │ ┃ ⏺ Read(src/auth.ts) │ ┃ ● You ⎿ Read 6 lines │ ┃ The boss ⏺ Update(src/auth.ts) │ ┃ Your move ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ ○ Claude ⎿ 3 pass, 1 fail │ ┃ Main conversation │ ┃ Waiting for you ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /party │ ⎿ party-pane: Party pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Party
Party all quiet [ Clear finished ] ● You The boss Your move ○ Claude Main conversation Waiting for you
README

party-pane

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.

  • You and Claude (the Sage) is always shown, and animates while Claude is working.
  • Every subagent Claude calls in gets a character, a label for its task, and a live line saying what it is doing right now ("Reading i18n.tsx", "Running npm run build").
  • Finished agents get a green check and leave after 30 seconds. Clear finished removes them now.
Agent typeCharacter
main Claude, PlanSage
ExploreScout
general-purposeSmith
code-reviewer, security-auditorGuardian
test-engineerAlchemist
anything elseWisp

Open it with /party. It also opens on session start.

Art

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

Try it

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).

Source 3 files
hooks/register.tsx 189 lines
1import { 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}
189
hooks/art.js 206 lines
1// 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}
206
types/index.d.ts 15 lines
1export 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