Elizabeth (Gintama) walks a context-window progress bar above the prompt, with the usage breakdown, turns left to auto-compact, and rate limits. Unofficial fan…

Elizabeth walks your context window. A Claude Code mod: above the prompt, Elizabeth (from Gintama) stands on a progress bar of the context window, holding up a sign with how full it is, and walks right as the conversation grows.

tokens / window (percent).It shares the band: whatever other plugins draw above the prompt stays, and Elizabeth goes underneath. Dark mode is supported.

Both pictures are drawn from made-up figures.
Needs Claude Code 2.1.287 or later. No key and no options.
/plugin marketplace add Yaxin9Luo/spending-effort-with-jev
/plugin install elizabeth-progress@spending-effort-with-jev
Then /reload-plugins. The bar appears after Claude's first reply, when Claude Code first measures the context. The breakdown, the auto-compact zone and turns left fill in as turns end.
It is independent of the effort advisor in the same marketplace: install either or both.
All from Claude Code itself; nothing leaves your machine.
session.measure event, which Claude Code raises whenever it measures new figures (after each API reply). Nothing is polled.$.session.usage({ breakdown: 'summary' }) at the end of each main-loop turn, a local estimate that makes no request.claude plugin test plugins/elizabeth-progress
claude plugin validate plugins/elizabeth-progress
hooks/elizabeth.ts draws the character (a pure function returning an SVG group), hooks/band.ts lays out the band, and hooks/register.tsx gathers the figures and draws the band on each surface.
Elizabeth is a character from Gintama by Hideaki Sorachi (Shueisha). The drawing here is hand-made, unofficial fan art; this plugin is not affiliated with or endorsed by the rights holders, and its MIT license covers the code and drawing only, granting no rights in the character. If you hold those rights and want it taken down, open an issue.
MIT, for the code and drawing; see LICENSE.
hooks/register.tsx 91 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Ctx, Limit, Part } from '../types'
5import { H, k, svg, tone } from './band'
6
7const ctx = atom({ plugin: 'elizabeth-progress', key: 'ctx' } as const, null as Ctx | null)
8const parts = atom({ plugin: 'elizabeth-progress', key: 'parts' } as const, [] as Part[])
9const limits = atom({ plugin: 'elizabeth-progress', key: 'limits' } as const, [] as Limit[])
10const cost = atom({ plugin: 'elizabeth-progress', key: 'cost' } as const, null as number | null)
11const hist = atom({ plugin: 'elizabeth-progress', key: 'hist' } as const, [] as number[])
12const compactAt = atom({ plugin: 'elizabeth-progress', key: 'compactAt' } as const, null as number | null)
13
14export const register: Register = on => {
15 // The engine sends new figures each time it measures them (every API reply): no polling.
16 on('session.measure', async ($, e, next) => {
17 const c = e.context
18 if (c.tokens !== undefined && c.percent !== undefined) {
19 await update($, ctx, () => ({ tokens: c.tokens ?? 0, window: c.window, percent: c.percent ?? 0 }))
20 }
21 await update($, limits, () => e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })))
22 if (e.cost) {
23 const usd = e.cost.usd
24 await update($, cost, () => usd)
25 }
26 return next(e)
27 })
28
29 // After each main-loop turn: the breakdown (a local estimate, no request),
30 // the auto-compact threshold, and one more point of history.
31 on('turn.complete', async ($, e, next) => {
32 const result = await next(e)
33 if (e.agentId !== undefined) return result // a subagent's turn leaves the main context as it was
34 const { context } = await $.session.usage({ breakdown: 'summary' })
35 const b = context.breakdown
36 const rows = (b?.categories ?? [])
37 .filter(r => r.kind === 'used' && r.tokens > 0)
38 .sort((x, y) => y.tokens - x.tokens)
39 .slice(0, 5)
40 .map(r => ({ name: r.name, tokens: r.tokens }))
41 await update($, parts, () => rows)
42 const threshold = b?.isAutoCompactEnabled && b.autoCompactThreshold ? b.autoCompactThreshold : null
43 await update($, compactAt, () => (threshold !== null ? (threshold / context.window) * 100 : null))
44 if (context.percent !== undefined) {
45 const p = context.percent
46 await update($, hist, h => [...h, p].slice(-40))
47 }
48 return result
49 })
50
51 // The band holds one tree, and plugins draw it in a chain: this one goes
52 // under whatever the plugins beneath drew, never in its place.
53 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
54 const c = await read($, ctx)
55 if (e.props.hasSurvey || c === null) return next(e)
56
57 const below = await next(e)
58 const ui = $.ui.resolve(e)
59 const { Box, Text } = ui
60
61 if (e.surface !== 'terminal' && 'Svg' in ui && ui.Svg) {
62 const source = svg({
63 c,
64 ps: await read($, parts),
65 ls: await read($, limits),
66 usd: await read($, cost),
67 h: await read($, hist),
68 at: await read($, compactAt),
69 now: await $.clock.now(),
70 })
71 return (
72 <Box flexDirection="column">
73 {below ?? null}
74 <ui.Svg key="eliz" source={source} alt={`context ${c.percent}%`} height={H} isInteractive />
75 </Box>
76 )
77 }
78
79 const n = Math.round(c.percent / 4)
80 return (
81 <Box flexDirection="column">
82 {below ?? null}
83 <Text key="eliz" color={tone(c.percent / 100)}>
84 {'━'.repeat(n)}
85 <Text dimColor>{'─'.repeat(25 - n)} {k(c.tokens)} / {k(c.window)} ({c.percent}%)</Text>
86 </Text>
87 </Box>
88 )
89 })
90}
91hooks/band.ts 143 lines1// The band's picture: one SVG at 100% width and 1:1 pixels, a fixed height,
2// every horizontal position a percentage so it fits any window width.
3// Left: Elizabeth standing on the progress bar. Right: a row of compact figures.
4import type { Ctx, Limit, Part } from '../types'
5import { elizabeth } from './elizabeth'
6import type { Mood } from './elizabeth'
7
8export const H = 84
9const L0 = 1.6
10const L1 = 53
11const R0 = 56
12const R1 = 99.4
13const TY = 54
14const SCALE = 0.4
15const PALETTE = ['#5b8def', '#e07a5f', '#3d9a8b', '#e0a43c', '#8c7ae6', '#9aa0a6']
16
17export const k = (n: number) =>
18 n >= 1e6 ? `${(n / 1e6).toFixed(2).replace(/\.?0+$/, '')}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
19export const tone = (r: number) => (r < 0.6 ? '#5b8def' : r < 0.85 ? '#e0a43c' : '#d4523c')
20const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<')
21const until = (iso: string | undefined, now: number) => {
22 if (!iso) return ''
23 const m = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
24 return m >= 1440 ? `${Math.floor(m / 1440)}d ${Math.floor((m % 1440) / 60)}h` : m >= 60 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${m}m`
25}
26const LIMIT_NAME: Record<string, string> = { five_hour: '5h limit', seven_day: 'Weekly', spend_limit: 'Spend' }
27
28const STYLE = `<style>
29 html,body{margin:0;padding:0;overflow:hidden;background:transparent}
30 text{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
31 .t{fill:#2a2a2a}.m{fill:#8b8780}.trk{fill:#e2dfd8}.div{stroke:#dcd8d0}.panel{fill:#fff;fill-opacity:.62}
32 @media (max-width: 900px){.wide,.only-wide{display:none}}
33 @media (min-width: 901px){.only-narrow{display:none}}
34 @media (prefers-color-scheme: dark){.t{fill:#ece9e2}.m{fill:#9b978f}.trk{fill:#3a3835}.div{stroke:#45423e}.panel{fill:#3a3835;fill-opacity:.55}}
35</style>`
36
37export type View = { c: Ctx; ps: Part[]; ls: Limit[]; usd: number | null; h: number[]; at: number | null; now: number }
38
39export const svg = ({ c, ps, ls, usd, h, at, now }: View) => {
40 const p = Math.min(100, c.percent)
41 const span = L1 - L0
42 const px = (v: number) => L0 + (Math.min(100, v) / 100) * span
43 const pc = (v: number) => `${v.toFixed(3)}%`
44 const ratio = p / (at ?? 100)
45 const out: string[] = [STYLE]
46
47 // Two light panels hold the left and right halves.
48 out.push(`<rect class="panel" x="0.3%" y="4" width="${pc(L1 + 1.4)}" height="${H - 8}" rx="12"/>`)
49 out.push(`<rect class="panel" x="${pc(R0 - 0.9)}" y="4" width="${pc(R1 - R0 + 1.2)}" height="${H - 8}" rx="12"/>`)
50
51 // The bar: the track, the used part coloured by category, the auto-compact zone
52 // (hatched), and ticks at 25/50/75.
53 out.push(`<defs><clipPath id="trk"><rect x="${pc(L0)}" y="${TY}" width="${pc(span)}" height="7" rx="3.5"/></clipPath>
54 <pattern id="hatch" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="5" height="5" fill="#d4523c" fill-opacity="0.08"/><line x1="0" y1="0" x2="0" y2="5" stroke="#d4523c" stroke-opacity="0.4" stroke-width="1.6"/></pattern></defs>`)
55 const g: string[] = [`<rect class="trk" x="${pc(L0)}" y="${TY}" width="${pc(span)}" height="7"/>`]
56 if (at !== null && at < 100) g.push(`<rect x="${pc(px(at))}" y="${TY}" width="${pc(L1 - px(at))}" height="7" fill="url(#hatch)"/>`)
57 const sum = ps.reduce((a, r) => a + r.tokens, 0)
58 if (sum > 0) {
59 let x = L0
60 ps.forEach((r, i) => {
61 const w = (r.tokens / sum) * (px(p) - L0)
62 g.push(`<rect x="${pc(x)}" y="${TY}" width="${pc(w + 0.05)}" height="7" fill="${PALETTE[i % PALETTE.length]}"/>`)
63 x += w
64 })
65 } else {
66 g.push(`<rect x="${pc(L0)}" y="${TY}" width="${pc(px(p) - L0)}" height="7" fill="${tone(ratio)}"/>`)
67 }
68 for (const t of [25, 50, 75]) g.push(`<rect x="${pc(px(t))}" y="${TY}" width="1.2" height="7" fill="#fff" fill-opacity="0.65"/>`)
69 out.push(`<g clip-path="url(#trk)">${g.join('')}</g>`)
70 // Steps aside as Elizabeth comes near, so the two never overlap.
71 if (at !== null && at < 100 && p < at - 22) {
72 out.push(`<text class="m" x="${pc(L1)}" y="${TY - 7}" font-size="10" text-anchor="end">auto-compact ${Math.round(at)}%</text>`)
73 }
74
75 // The legend: one line under the bar; a narrow window keeps the first three.
76 const lg: string[] = []
77 let lx = 0
78 ps.forEach((r, i) => {
79 const name = r.name.replace(/^System /, 'Sys ')
80 lg.push(`<g${i >= 3 ? ' class="wide"' : ''}><circle cx="${lx + 3}" cy="${TY + 18.5}" r="3" fill="${PALETTE[i % PALETTE.length]}"/>
81 <text x="${lx + 10}" y="${TY + 22}" font-size="10.5"><tspan class="m">${esc(name)} </tspan><tspan class="t" font-weight="600">${k(r.tokens)}</tspan></text></g>`)
82 lx += 22 + (name.length + 1 + k(r.tokens).length) * 5.6
83 })
84 out.push(`<svg x="${pc(L0)}" y="0" overflow="visible">${lg.join('')}</svg>`)
85
86 // Elizabeth stands on the top edge of the bar and walks with the usage.
87 const mood: Mood = ratio >= 0.95 ? 'crying' : ratio >= 0.8 ? 'angry' : 'calm'
88 const ex = Math.min(L1 - 3.2, Math.max(L0 + 2.6, px(p)))
89 out.push(`<svg x="${pc(ex)}" y="0" overflow="visible"><g transform="translate(0 ${TY - 1.5}) scale(${SCALE})">${elizabeth({ sign: `${c.percent}%`, mood, lw: 2.2, signSize: 20 })}</g></svg>`)
90
91 // The row of figures on the right.
92 const deltas = h.slice(1).map((v, i) => v - (h[i] ?? v)).filter(d => d > 0)
93 const recent = deltas.slice(-5)
94 const avg = recent.length ? recent.reduce((a, b) => a + b, 0) / recent.length : 0
95 const room = (at ?? 100) - p
96 type Col = { label: string; value: string; sub: string; frac?: number; spark?: number[] }
97 const cols: Col[] = [
98 { label: 'Context', value: k(c.tokens), sub: `${c.percent}% of ${k(c.window)}`, frac: ratio },
99 {
100 label: 'Turns left',
101 value: avg > 0 ? (room / avg < 1 ? (room > 0 ? '<1' : '0') : `≈${Math.floor(room / avg)}`) : '—',
102 sub: avg > 0 ? `+${avg.toFixed(1)}%/turn` : 'need 2 turns',
103 spark: h.slice(-24),
104 },
105 ...ls.slice(0, 2).map(l => ({
106 label: LIMIT_NAME[l.kind] ?? l.kind,
107 value: `${l.percentUsed}%`,
108 sub: l.resetsAt ? `↻ ${until(l.resetsAt, now)}` : '',
109 frac: l.percentUsed / 100,
110 })),
111 ...(usd !== null ? [{ label: 'Cost', value: `$${usd.toFixed(2)}`, sub: 'this session' }] : []),
112 ]
113 const drawCols = (list: Col[], cls: string) => {
114 const g: string[] = []
115 const cw = (R1 - R0) / list.length
116 list.forEach((col, i) => {
117 const x0 = R0 + i * cw
118 const x = x0 + (i ? 1.3 : 0.6)
119 const inner = cw - (i ? 2.6 : 1.9)
120 if (i) g.push(`<line class="div" x1="${pc(x0)}" y1="16" x2="${pc(x0)}" y2="${H - 16}"/>`)
121 const hot = col.frac !== undefined && col.frac >= 0.85
122 g.push(`<text class="m" x="${pc(x)}" y="23" font-size="9.5" font-weight="500" letter-spacing="0.5">${esc(col.label.toUpperCase())}</text>`)
123 g.push(`<text ${hot ? `fill="${tone(col.frac ?? 0)}"` : 'class="t"'} x="${pc(x)}" y="45" font-size="19" font-weight="600" letter-spacing="-0.2">${esc(col.value)}</text>`)
124 g.push(`<text class="m" x="${pc(x)}" y="60" font-size="10.5">${esc(col.sub)}</text>`)
125 if (col.frac !== undefined) {
126 g.push(`<rect class="trk" x="${pc(x)}" y="66" width="${pc(inner)}" height="3" rx="1.5"/>`)
127 g.push(`<rect x="${pc(x)}" y="66" width="${pc(inner * Math.min(1, col.frac))}" height="3" rx="1.5" fill="${tone(col.frac)}"/>`)
128 }
129 if (col.spark && col.spark.length > 1) {
130 const sp = col.spark
131 const top = Math.max(...sp, 1)
132 const pts = sp.map((v, j) => `${((j / (sp.length - 1)) * 100).toFixed(1)},${(10 - (v / top) * 9).toFixed(1)}`).join(' ')
133 g.push(`<svg x="${pc(x)}" y="61" width="${pc(inner)}" height="10" viewBox="0 0 100 10" preserveAspectRatio="none" overflow="visible"><polyline points="${pts}" fill="none" stroke="#5b8def" stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg>`)
134 }
135 })
136 return `<g class="${cls}">${g.join('')}</g>`
137 }
138 out.push(drawCols(cols, 'only-wide'))
139 out.push(drawCols(cols.filter(col => col.label !== 'Cost'), 'only-narrow'))
140
141 return `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="${H}">${out.join('')}</svg>`
142}
143hooks/elizabeth.ts 173 lines1// Elizabeth (Gintama), drawn by hand from a front-view acrylic stand and
2// screenshots of the anime, with two-step cel shading. Unofficial fan art.
3// Coordinates: the origin is between the feet; the top of the head is at
4// y=-111, the hem at y≈-11; the body is about 62 wide.
5// The sign: the hand on the right of the picture holds up a white stick, the
6// board above it to the right, behind the head.
7
8export type Mood = 'calm' | 'angry' | 'crying'
9
10export type ElizabethOptions = {
11 sign: string
12 mood?: Mood
13 /** Line width, in the character's units; raise it when drawn small so lines stay about 1px on screen. */
14 lw?: number
15 animate?: boolean
16 /** Font size on the sign, in the character's units; raise it when drawn small so it stays legible. */
17 signSize?: number
18}
19
20const OUT = '#4a4a4a'
21const SHADE = '#e1e5ec'
22const SHADE_DEEP = '#d3d8e0'
23const BEAK_LINE = '#6a4b1a'
24const TOE_LINE = '#8a5f17'
25const SIGN_FONT = `'Marker Felt', 'Chalkboard SE', 'Comic Sans MS', sans-serif`
26
27const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<')
28
29const DEFS = `<defs>
30 <linearGradient id="eliz-toe" x1="0" y1="-16" x2="0" y2="4" gradientUnits="userSpaceOnUse">
31 <stop offset="0" stop-color="#f6c24c"/><stop offset="0.6" stop-color="#eba636"/><stop offset="1" stop-color="#d4891f"/>
32 </linearGradient>
33 <linearGradient id="eliz-beak-top" x1="0" y1="-88.4" x2="0" y2="-79.2" gradientUnits="userSpaceOnUse">
34 <stop offset="0" stop-color="#f9cf63"/><stop offset="1" stop-color="#f1b33c"/>
35 </linearGradient>
36 <linearGradient id="eliz-beak-low" x1="0" y1="-80.6" x2="0" y2="-73.8" gradientUnits="userSpaceOnUse">
37 <stop offset="0" stop-color="#eda93b"/><stop offset="1" stop-color="#d68c26"/>
38 </linearGradient>
39</defs>`
40
41// An eye: a slightly tall circle, a small black dot in the middle, three lashes flicked outward on top.
42const eye = (cx: number, cy: number, lw: number) => {
43 const rx = 5.9
44 const ry = 6.3
45 const lashes = [-122, -90, -58]
46 .map(d => {
47 const t = (d * Math.PI) / 180
48 const x1 = cx + rx * Math.cos(t)
49 const y1 = cy + ry * Math.sin(t)
50 const tilt = t + (d + 90) * 0.006
51 return `M${x1.toFixed(2)} ${y1.toFixed(2)} l${(2.4 * Math.cos(tilt)).toFixed(2)} ${(2.4 * Math.sin(tilt)).toFixed(2)}`
52 })
53 .join(' ')
54 return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="#fff" stroke="${OUT}" stroke-width="${lw * 0.8}"/>
55<circle cx="${cx}" cy="${cy + 0.2}" r="0.75" fill="#151515"/>
56<path d="${lashes}" stroke="${OUT}" stroke-width="${lw * 0.7}" stroke-linecap="round" fill="none"/>`
57}
58
59// A toe: a plump ellipse from the heel (bx,by) to the tip (tx,ty), as ellipse attributes.
60const toe = (bx: number, by: number, tx: number, ty: number, w: number) => {
61 const len = Math.hypot(bx - tx, by - ty)
62 const ang = (Math.atan2(by - ty, bx - tx) * 180) / Math.PI
63 const cx = ((bx + tx) / 2).toFixed(2)
64 const cy = ((by + ty) / 2).toFixed(2)
65 return `cx="${cx}" cy="${cy}" rx="${(len / 2).toFixed(2)}" ry="${w / 2}" transform="rotate(${ang.toFixed(1)} ${cx} ${cy})"`
66}
67
68const TOES = [toe(-16, -12.8, -39.6, -6.8, 7.8), toe(-15, -10.2, -36.6, 0.6, 8.2), toe(-13.6, -7.6, -28.8, 3.4, 7.8)]
69const WEB = 'M-11 -14.4 L-37 -8.6 L-35 -0.4 L-28 3 C-20 1.6,-13.4 -0.6,-10.4 -4 C-8.8 -7,-9 -11,-11 -14.4 Z'
70
71// The left foot (on the left of the picture): a big webbed fan with three round toes
72// spread down and to the left. The right foot is its mirror image.
73const foot = (mirror: boolean, lw: number) => {
74 const parts = (attrs: string) =>
75 `<path d="${WEB}" ${attrs}/>` + TOES.map(t => `<ellipse ${t} ${attrs}/>`).join('')
76 return `<g${mirror ? ' transform="scale(-1 1)"' : ''}>
77 ${parts(`fill="${TOE_LINE}" stroke="${TOE_LINE}" stroke-width="${lw * 1.5}" stroke-linejoin="round"`)}
78 ${parts('fill="url(#eliz-toe)"')}
79 <path d="M-33.2 -3.2 C-30 -5,-26.6 -6.6,-23 -7.6 M-28 0.6 C-25.4 -1.8,-22.6 -3.6,-19.6 -4.8" stroke="#c27c1e" stroke-width="${lw * 0.7}" fill="none" stroke-linecap="round" opacity="0.75"/>
80 <path d="M-36.6 -9.4 C-31 -11.4,-24 -12.8,-18 -13.4" stroke="#fbd877" stroke-width="${lw * 1.4}" fill="none" stroke-linecap="round" opacity="0.9"/>
81 <path d="M-34.6 -2.6 C-29.6 -5.2,-24.6 -7,-19.6 -8.2" stroke="#fbd877" stroke-width="${lw * 1.2}" fill="none" stroke-linecap="round" opacity="0.7"/>
82</g>`
83}
84
85// 💢 the anger mark: four arcs curling toward the centre.
86const anger = (cx: number, cy: number, s: number, lw: number, color: string) => {
87 const q = (sx: number, sy: number) =>
88 `M${cx + sx * s} ${cy + sy * s * 0.28} Q${cx + sx * s * 0.28} ${cy + sy * s * 0.28} ${cx + sx * s * 0.28} ${cy + sy * s}`
89 return `<path d="${q(-1, -1)} ${q(1, -1)} ${q(-1, 1)} ${q(1, 1)}" stroke="${color}" stroke-width="${lw * 1.6}" fill="none" stroke-linecap="round"/>`
90}
91
92// The beak: a lemon shape pointed at both ends. The upper half is bright yellow,
93// larger, with a highlight; the lower half is more orange and darker toward the
94// bottom; a shadow at the right end gives it thickness.
95const beak = (lw: number) => {
96 const top = 'M-14 -80.6 C-12.6 -86.4,-5.6 -88.6,1 -88.4 C8 -88.2,13.8 -85.6,14.2 -81'
97 const lipFwd = 'M-14 -80.6 C-9 -80.4,-3 -78.8,2 -79.2 C7 -79.6,11 -80.8,14.2 -81'
98 const lipBack = 'C11 -80.8,7 -79.6,2 -79.2 C-3 -78.8,-9 -80.4,-14 -80.6'
99 const bottom = 'C13.4 -76.2,8 -74.2,0.6 -74.2 C-6.4 -74.2,-11.8 -76.4,-14 -80.6'
100 return `<g transform="translate(0 1.4)">
101 <clipPath id="eliz-beak"><path d="${top} ${bottom} Z"/></clipPath>
102 <path d="M-9 -73.2 C-4 -71.4,5 -71.4,10 -73.2 C6 -72,-4 -72,-9 -73.2 Z" fill="${SHADE_DEEP}"/>
103 <ellipse cx="0.6" cy="-72.6" rx="9.6" ry="1.5" fill="${SHADE_DEEP}" opacity="0.6"/>
104 <g clip-path="url(#eliz-beak)">
105 <path d="${lipFwd} ${bottom} Z" fill="url(#eliz-beak-low)"/>
106 <path d="M15 -84 C15 -78,12 -74.6,7 -73.4 L16 -72 Z" fill="#c47b1c" opacity="0.5"/>
107 <path d="M-12 -76.6 C-7 -74.6,1 -74.4,7 -75.4" stroke="#c47b1c" stroke-width="${lw * 0.7}" fill="none" stroke-linecap="round" opacity="0.45"/>
108 <path d="${top} ${lipBack} Z" fill="url(#eliz-beak-top)"/>
109 <path d="M-4 -86.6 C1 -87.6,7 -87.2,10.6 -85.4 C6.6 -85,1.4 -85.2,-4 -86.6 Z" fill="#fde7a6" opacity="0.9"/>
110 <path d="M-14.4 -80 C-13 -84,-10 -86.4,-6.4 -87.4" stroke="#dc9d2e" stroke-width="${lw * 0.8}" fill="none" stroke-linecap="round" opacity="0.55"/>
111 <path d="M14.6 -82.6 C14 -80.6,13.2 -80,12 -79.6" stroke="#c47b1c" stroke-width="${lw * 0.8}" fill="none" opacity="0.6"/>
112 </g>
113 <path d="${lipFwd}" stroke="${BEAK_LINE}" stroke-width="${lw * 0.65}" fill="none" stroke-linecap="round"/>
114 <path d="${top} ${bottom} Z" fill="none" stroke="${BEAK_LINE}" stroke-width="${lw * 0.75}" stroke-linejoin="round"/>
115</g>`
116}
117
118const BODY =
119 'M-27 -11 C-30.5 -14,-31.5 -24,-31.2 -36 C-30.8 -52,-29.4 -72,-28.4 -86 C-27.2 -102,-15.6 -111,0 -111 C15.6 -111,27.2 -102,28.4 -86 C29.4 -72,30.8 -52,31.2 -36 C31.5 -24,30.5 -14,27 -11 C20 -8.6,9 -9.6,4 -13.4 C2.4 -14.6,1.2 -15.2,0 -15.2 C-1.2 -15.2,-2.4 -14.6,-4 -13.4 C-9 -9.6,-20 -8.6,-27 -11 Z'
120
121export const elizabeth = ({ sign, mood = 'calm', lw = 1, animate = true, signSize = 14 }: ElizabethOptions) => {
122 const bob = animate
123 ? '<animateTransform attributeName="transform" type="translate" values="0 0;0 -1.6;0 0" dur="1.6s" repeatCount="indefinite"/>'
124 : ''
125 const sway = animate
126 ? '<animateTransform attributeName="transform" type="rotate" values="-2.5 30 -66;2.5 30 -66;-2.5 30 -66" dur="2.6s" repeatCount="indefinite"/>'
127 : ''
128
129 // The sign: when angry it alternates between the percentage and 💢.
130 const text = `<text x="45" y="${-116.8 + signSize * 0.36}" font-size="${signSize}" font-weight="700" text-anchor="middle" fill="#1d1d1d" font-family="${SIGN_FONT}">${esc(sign)}</text>`
131 const signFace =
132 mood === 'calm' || !animate
133 ? text + (mood === 'calm' ? '' : anger(66, -127, 3.2, lw, '#c8402d'))
134 : `<g>${text}<animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.55;0.6;0.95;1" dur="3.2s" repeatCount="indefinite"/></g>
135<g opacity="0">${anger(45, -116.8, 7, lw, '#c8402d')}<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.55;0.6;0.95;1" dur="3.2s" repeatCount="indefinite"/></g>`
136
137 const tears =
138 mood === 'crying'
139 ? `<path d="M-15.4 -86.6 c-2.6 0.4,-3.4 3,-1.2 3.6 c1.6 0.4,3 -0.6,3.2 -2 c0.2 -1.2,-0.8 -1.8,-2 -1.6 Z" fill="#a9c8f0" opacity="0.9"/>
140<path d="M15.4 -86.6 c2.6 0.4,3.4 3,1.2 3.6 c-1.6 0.4,-3 -0.6,-3.2 -2 c-0.2 -1.2,0.8 -1.8,2 -1.6 Z" fill="#a9c8f0" opacity="0.9"/>`
141 : ''
142
143 return `<g class="elizabeth">${DEFS}
144 <ellipse cx="0" cy="1.6" rx="41" ry="3.4" fill="#000" opacity="0.09"/>
145 <g>${bob}
146 ${foot(false, lw)}
147 ${foot(true, lw)}
148 <g>${sway}
149 <rect x="43.6" y="-104" width="2.2" height="20" rx="0.9" fill="#fff" stroke="${OUT}" stroke-width="${lw * 0.7}"/>
150 <rect x="44.6" y="-104" width="1.2" height="20" fill="${SHADE}"/>
151 <path d="M20 -130 H70 V-107.5 L66.5 -103.5 H20 Z" fill="#fff" stroke="${OUT}" stroke-width="${lw * 0.9}" stroke-linejoin="round"/>
152 <path d="M20.6 -105.6 H66.2 L64.6 -104 H20.6 Z" fill="${SHADE}"/>
153 <path d="M70 -107.5 L66.5 -103.5 L66.9 -107.2 Z" fill="#dfe2e7" stroke="${OUT}" stroke-width="${lw * 0.6}" stroke-linejoin="round"/>
154 ${signFace}
155 <path d="M28.4 -81 C31.6 -85.6,35.4 -90.4,39.2 -93.6 C41.8 -95.8,46.8 -95.2,47.8 -91.6 C48.6 -88.6,47.2 -85.8,44.8 -84 C42.6 -78.4,38.4 -70.4,31 -61 Z" fill="#fff" stroke="${OUT}" stroke-width="${lw}" stroke-linejoin="round"/>
156 <path d="M47.6 -90 C47.4 -87.6,46.2 -85.6,44.8 -84 C42.6 -78.4,38.4 -70.4,31 -61 L30.4 -66.6 C35.4 -72.4,40.2 -79.6,43 -85.6 C44.6 -86.4,46.4 -87.8,47.6 -90 Z" fill="${SHADE}"/>
157 </g>
158 <path d="M-28.4 -79 C-35 -72.4,-41.8 -58,-45 -43 C-45.4 -40.6,-43.4 -39.6,-41.8 -41 C-38 -44.6,-34 -49.4,-30.9 -54 Z" fill="#fff" stroke="${OUT}" stroke-width="${lw}" stroke-linejoin="round"/>
159 <path d="M-44.6 -41.4 C-43.6 -40,-42.6 -40.2,-41.8 -41 C-38 -44.6,-34 -49.4,-30.9 -54 L-30.4 -60.4 C-33.6 -55,-38.6 -48,-44.6 -41.4 Z" fill="${SHADE}"/>
160 <path d="${BODY}" fill="#fff"/>
161 <path d="M19.6 -105 C25.8 -99,28 -92,28.4 -86 C29.4 -72,30.8 -52,31.2 -36 C31.5 -24,30.5 -14,27 -11 C24.6 -10.2,22.2 -9.8,20 -9.8 C23.4 -16,25 -28,25.2 -40 C25.4 -58,24.8 -76,24.2 -86 C23.8 -94,22.4 -100,19.6 -105 Z" fill="${SHADE}"/>
162 <path d="M-30.9 -54 C-30.6 -50,-30 -47.6,-28.6 -45.6 C-28.8 -50,-28.8 -55,-28.6 -60 Z" fill="${SHADE}"/>
163 <path d="M-30 -18 C-27 -13.4,-20 -11.8,-12 -12.6 C-18 -14.4,-24 -15.6,-30 -18 Z" fill="${SHADE}"/>
164 <path d="M-9 -24 C-5.4 -18,-2.2 -15.6,0 -15.4 C2.2 -15.6,5.4 -18,9 -24 C5 -20.2,2.4 -18.8,0 -18.8 C-2.4 -18.8,-5 -20.2,-9 -24 Z" fill="${SHADE_DEEP}" opacity="0.7"/>
165 <path d="${BODY}" fill="none" stroke="${OUT}" stroke-width="${lw * 1.1}" stroke-linejoin="round"/>
166 ${eye(-12.8, -94.2, lw)}
167 ${eye(12.8, -94.2, lw)}
168 ${tears}
169 ${beak(lw)}
170 </g>
171</g>`
172}
173types/index.d.ts 17 lines1export type Ctx = { tokens: number; window: number; percent: number }
2export type Part = { name: string; tokens: number }
3export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
4
5declare module 'claude-code' {
6 interface PluginState {
7 'elizabeth-progress': {
8 ctx: Ctx | null
9 parts: Part[]
10 limits: Limit[]
11 cost: number | null
12 hist: number[]
13 compactAt: number | null
14 }
15 }
16}
17