Usage band above the prompt: 5h/7d limits, token totals and session cost

A Claude Code mod: a usage band above the prompt showing the 5-hour and 7-day limits, token totals (input, output, cache) and session cost.
Token totals come from a small script (scripts/tokens.mjs) that sums usage from the session's local transcript files under ~/.claude/projects. Nothing is sent anywhere.
git clone https://github.com/joydeep-pm/usage-band ~/.claude/mods/usage-band
claude plugin validate ~/.claude/mods/usage-band
Load it for one session:
claude --plugin-dir ~/.claude/mods/usage-band
Or load it in every session by adding the folder to CLAUDE_CODE_PLUGIN_DIRS (colon separated) in the env block of ~/.claude/settings.json.
tsconfig.json expects the engine's type declarations in .claude/types. Generate them with /plugin-types inside Claude Code. They are not committed.
hooks/register.tsx 564 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type {
5 UsageContext,
6 UsageFallback,
7 UsageLimits,
8 UsageTokens,
9 UsageWindow,
10} from '../types'
11
12const limitsAtom = atom({ plugin: 'usage-band', key: 'limits' } as const, null)
13const costAtom = atom({ plugin: 'usage-band', key: 'cost' } as const, null)
14const contextAtom = atom({ plugin: 'usage-band', key: 'context' } as const, null)
15const tokensAtom = atom({ plugin: 'usage-band', key: 'tokens' } as const, null)
16const fallbackAtom = atom({ plugin: 'usage-band', key: 'fallback' } as const, {
17 uncachedInput: 0,
18 cacheWrite: 0,
19 output: 0,
20 cacheRead: 0,
21 turns: 0,
22})
23const hiddenAtom = atom({ plugin: 'usage-band', key: 'isHidden' } as const, false)
24const nowAtom = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
25
26const FIVE_HOURS = 5 * 3600 * 1000
27const SEVEN_DAYS = 7 * 24 * 3600 * 1000
28const NODES = ['node', '/usr/local/bin/node', '/opt/homebrew/bin/node']
29
30type Kind = 'h5' | 'd7' | 'in' | 'out' | 'cache' | 'cost'
31
32type Model = {
33 limits: UsageLimits | null
34 cost: number | null
35 context: UsageContext | null
36 tokens: UsageTokens | null
37 fallback: UsageFallback
38 now: number
39}
40
41// ---------- formatting ----------
42
43export const fmtTokens = (n: number): string => {
44 if (n < 1000) return String(Math.round(n))
45 if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`
46 return `${(n / 1_000_000).toFixed(2)}M`
47}
48
49export const fmtLeft = (ms: number): string => {
50 const minutes = Math.max(0, Math.floor(ms / 60000))
51 const d = Math.floor(minutes / 1440)
52 const h = Math.floor((minutes % 1440) / 60)
53 const m = minutes % 60
54 if (d > 0) return `${d}d ${h}h`
55 if (h > 0) return `${h}h ${m}m`
56 return `${m}m`
57}
58
59const fmtUsd = (usd: number): string => `$${usd.toFixed(2)}`
60
61const fmtInt = (n: number): string => Math.round(n).toLocaleString('en-US')
62
63const level = (pct: number): 'ok' | 'warn' | 'bad' =>
64 pct >= 90 ? 'bad' : pct >= 70 ? 'warn' : 'ok'
65
66const elapsedFraction = (w: UsageWindow, length: number, now: number): number | null => {
67 if (w.resetsAt === null) return null
68 const left = Math.min(length, Math.max(0, w.resetsAt - now))
69 return 1 - left / length
70}
71
72const esc = (s: string): string =>
73 s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
74
75// ---------- SVG pills (desktop) ----------
76
77const FONT = 12
78const CHAR = 7.3
79const H = 22
80const PAD = 8
81
82const STYLE = `
83.bg{rx:11}
84.h5 .bg{fill:#d3f1ea}.h5{color:#0d4a40}
85.d7 .bg{fill:#e9e1fb}.d7{color:#3b2775}
86.in .bg{fill:#fbe1dc}.in{color:#7a2317}
87.out .bg{fill:#dcf2db}.out{color:#1e5821}
88.cache .bg{fill:#dce8fb}.cache{color:#1a427f}
89.cost .bg{fill:#fbefc9}.cost{color:#664a00}
90text{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:${FONT}px;fill:currentColor}
91.b{font-weight:700}
92.ic{fill:none;stroke:currentColor;stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round}
93.trk{fill:currentColor;opacity:.18}
94.sep{stroke:currentColor;opacity:.35}
95.tl{stroke:currentColor;stroke-width:1.4}
96.ok{fill:#2f9e5c}.warn{fill:#d39e00}.bad{fill:#d64545}
97@media (prefers-color-scheme: dark){
98.h5 .bg{fill:#103a34}.h5{color:#b5eee2}
99.d7 .bg{fill:#2a2146}.d7{color:#d8cbff}
100.in .bg{fill:#45201a}.in{color:#ffc8bd}
101.out .bg{fill:#1c3a1e}.out{color:#c1eec2}
102.cache .bg{fill:#19294a}.cache{color:#c3daff}
103.cost .bg{fill:#3d3112}.cost{color:#f4da8c}
104.ok{fill:#4cc27a}.warn{fill:#f0c419}.bad{fill:#ff6b6b}
105}`
106
107const ICONS: Record<string, string> = {
108 clock: '<circle class="ic" cx="6" cy="6" r="5"/><path class="ic" d="M6 3v3l2 1.5"/>',
109 calendar:
110 '<rect class="ic" x="1" y="2" width="10" height="9" rx="1.5"/><path class="ic" d="M1 5h10M4 0.8v2.4M8 0.8v2.4"/>',
111 hourglass: '<path class="ic" d="M3 1h6M3 11h6M3.5 1c0 3 5 3 5 5s-5 2-5 5M8.5 1c0 3-5 3-5 5s5 2 5 5"/>',
112 up: '<path class="ic" d="M6 11V1.5M2 5.5l4-4 4 4"/>',
113 down: '<path class="ic" d="M6 1v9.5M2 6.5l4 4 4-4"/>',
114 layers: '<path class="ic" d="M6 1l5 2.6-5 2.6-5-2.6z"/><path class="ic" d="M1 6.3l5 2.6 5-2.6M1 8.8l5 2.6 5-2.6"/>',
115 dollar:
116 '<path class="ic" d="M6 0.5v11M8.6 3.2C8.2 2.3 7.2 1.9 6 1.9c-1.5 0-2.6.8-2.6 1.9 0 2.6 5.3 1.4 5.3 4.2 0 1.1-1.1 2-2.7 2-1.3 0-2.4-.5-2.8-1.5"/>',
117}
118
119type Part =
120 | { t: 'icon'; name: string }
121 | { t: 'text'; s: string; bold?: boolean }
122 | { t: 'bar'; pct: number; elapsed: number | null }
123 | { t: 'sep' }
124
125const partWidth = (p: Part): number =>
126 p.t === 'icon' ? 12 : p.t === 'text' ? p.s.length * CHAR : p.t === 'bar' ? 44 : 9
127
128const pill = (kind: Kind, parts: Part[], tip: string): { source: string; width: number } => {
129 const GAP = 5
130 const inner = parts.reduce((sum, p) => sum + partWidth(p), 0) + GAP * (parts.length - 1)
131 const width = Math.ceil(inner + PAD * 2)
132 let x = PAD
133 const body: string[] = []
134 for (const p of parts) {
135 if (p.t === 'icon') {
136 body.push(`<g transform="translate(${x.toFixed(1)} 5)">${ICONS[p.name] ?? ''}</g>`)
137 } else if (p.t === 'text') {
138 body.push(
139 `<text x="${x.toFixed(1)}" y="15"${p.bold ? ' class="b"' : ''}>${esc(p.s)}</text>`,
140 )
141 } else if (p.t === 'bar') {
142 const w = 44
143 const fill = (Math.min(100, Math.max(0, p.pct)) / 100) * w
144 body.push(`<rect class="trk" x="${x.toFixed(1)}" y="8.5" width="${w}" height="5" rx="2.5"/>`)
145 if (fill > 0) {
146 body.push(
147 `<rect class="${level(p.pct)}" x="${x.toFixed(1)}" y="8.5" width="${Math.max(2.5, fill).toFixed(1)}" height="5" rx="2.5"/>`,
148 )
149 }
150 if (p.elapsed !== null) {
151 const tx = x + Math.min(1, Math.max(0, p.elapsed)) * w
152 body.push(`<line class="tl" x1="${tx.toFixed(1)}" y1="4" x2="${tx.toFixed(1)}" y2="16"/>`)
153 }
154 } else {
155 const sx = x + 4.5
156 body.push(`<line class="sep" x1="${sx.toFixed(1)}" y1="6" x2="${sx.toFixed(1)}" y2="16"/>`)
157 }
158 x += partWidth(p) + GAP
159 }
160 const source =
161 `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${H}" viewBox="0 0 ${width} ${H}">` +
162 `<style>${STYLE}</style>` +
163 `<g class="${kind}"><title>${esc(tip)}</title>` +
164 `<rect class="bg" x="0" y="0" width="${width}" height="${H}" rx="11"/>` +
165 body.join('') +
166 '</g></svg>'
167 return { source, width }
168}
169
170// ---------- pill model (shared by both surfaces) ----------
171
172type PillSpec = {
173 kind: Kind
174 group: 0 | 1 | 2
175 parts: Part[]
176 tip: string
177 alt: string
178}
179
180const windowSpec = (
181 kind: 'h5' | 'd7',
182 w: UsageWindow,
183 now: number,
184): PillSpec => {
185 const isFive = kind === 'h5'
186 const length = isFive ? FIVE_HOURS : SEVEN_DAYS
187 const label = isFive ? '5h' : '7d'
188 const elapsed = elapsedFraction(w, length, now)
189 const left = w.resetsAt === null ? null : fmtLeft(w.resetsAt - now)
190 const pct = `${Math.round(w.percentUsed)}%`
191 const parts: Part[] = [
192 { t: 'icon', name: isFive ? 'clock' : 'calendar' },
193 { t: 'text', s: label },
194 { t: 'bar', pct: w.percentUsed, elapsed },
195 { t: 'text', s: pct, bold: true },
196 ]
197 if (left !== null) parts.push({ t: 'sep' }, { t: 'icon', name: 'hourglass' }, { t: 'text', s: left })
198 const resetAt = w.resetsAt === null ? 'unknown' : new Date(w.resetsAt).toLocaleString()
199 const tip = [
200 `${isFive ? '5-hour' : '7-day'} limit: ${w.percentUsed}% used`,
201 `Resets in ${left ?? '?'} (${resetAt})`,
202 elapsed === null ? 'Window elapsed: ?' : `Window elapsed: ${Math.round(elapsed * 100)}%`,
203 ].join('\n')
204 return { kind, group: 0, parts, tip, alt: `${label} ${pct}${left ? `, resets in ${left}` : ''}` }
205}
206
207const specs = (m: Model): PillSpec[] => {
208 const out: PillSpec[] = []
209 if (m.limits?.fiveHour) out.push(windowSpec('h5', m.limits.fiveHour, m.now))
210 if (m.limits?.sevenDay) out.push(windowSpec('d7', m.limits.sevenDay, m.now))
211
212 const t: UsageTokens | null =
213 m.tokens ??
214 (m.fallback.turns > 0
215 ? {
216 uncachedInput: m.fallback.uncachedInput,
217 cacheWrite: m.fallback.cacheWrite,
218 output: m.fallback.output,
219 cacheRead: m.fallback.cacheRead,
220 requests: m.fallback.turns,
221 isApprox: true,
222 dump: '',
223 subagentFiles: 0,
224 }
225 : null)
226 if (t && t.requests > 0) {
227 const pre = t.isApprox ? '~' : ''
228 const ctx = m.context
229 const ctxLine = ctx
230 ? `Context: ${ctx.percent === null ? '?' : `${Math.round(ctx.percent)}%`} of ${fmtTokens(ctx.window)}${ctx.tokens === null ? '' : ` (${fmtInt(ctx.tokens)} tokens)`}`
231 : 'Context: unknown'
232 const source = t.isApprox
233 ? `Source: turn.complete totals (${t.requests} turns, approximate)`
234 : `Dump: ${t.dump}${t.subagentFiles ? ` + ${t.subagentFiles} subagent file(s)` : ''}`
235 const reqLine = t.isApprox ? `Turns: ${t.requests}` : `Requests: ${t.requests}`
236 const input = t.uncachedInput + t.cacheWrite
237 out.push({
238 kind: 'in',
239 group: 1,
240 parts: [{ t: 'icon', name: 'up' }, { t: 'text', s: pre + fmtTokens(input) }],
241 tip: [
242 `Input tokens: ${fmtInt(input)}`,
243 ` uncached ${fmtInt(t.uncachedInput)} + cache writes ${fmtInt(t.cacheWrite)}`,
244 reqLine,
245 ctxLine,
246 source,
247 ].join('\n'),
248 alt: `input ${pre}${fmtTokens(input)}`,
249 })
250 out.push({
251 kind: 'out',
252 group: 1,
253 parts: [{ t: 'icon', name: 'down' }, { t: 'text', s: pre + fmtTokens(t.output) }],
254 tip: [`Output tokens: ${fmtInt(t.output)}`, reqLine, ctxLine, source].join('\n'),
255 alt: `output ${pre}${fmtTokens(t.output)}`,
256 })
257 out.push({
258 kind: 'cache',
259 group: 1,
260 parts: [{ t: 'icon', name: 'layers' }, { t: 'text', s: pre + fmtTokens(t.cacheRead) }],
261 tip: [`Cache read tokens: ${fmtInt(t.cacheRead)}`, reqLine, ctxLine, source].join('\n'),
262 alt: `cache read ${pre}${fmtTokens(t.cacheRead)}`,
263 })
264 }
265 if (m.cost !== null && out.length > 0) {
266 out.push({
267 kind: 'cost',
268 group: 2,
269 parts: [{ t: 'icon', name: 'dollar' }, { t: 'text', s: m.cost.toFixed(2) }],
270 tip: `Session cost: $${m.cost.toFixed(4)}\nat API list price (as /cost totals it)`,
271 alt: `cost ${fmtUsd(m.cost)}`,
272 })
273 }
274 return out
275}
276
277export const renderSvgPills = (m: Model) =>
278 specs(m).map(s => ({ ...s, ...pill(s.kind, s.parts, s.tip) }))
279
280// ---------- terminal ----------
281
282const TERM_COLOR: Record<Kind, string> = {
283 h5: '#2bb3a0',
284 d7: '#9b7be0',
285 in: '#e0705f',
286 out: '#4caf50',
287 cache: '#4a8fe0',
288 cost: '#d4a72c',
289}
290const LEVEL_COLOR = { ok: '#4caf50', warn: '#e0b000', bad: '#e05050' }
291const TERM_ICON: Record<string, string> = {
292 clock: '◷',
293 calendar: '▦',
294 hourglass: '⧗',
295 up: '↑',
296 down: '↓',
297 layers: '≋',
298 dollar: '$',
299}
300
301const termBar = (pct: number, elapsed: number | null): { fill: string; mark: number; rest: string } => {
302 const cells = 10
303 const filled = Math.round((Math.min(100, Math.max(0, pct)) / 100) * cells)
304 const mark = elapsed === null ? -1 : Math.min(cells - 1, Math.round(elapsed * cells))
305 return { fill: '█'.repeat(filled), mark, rest: '░'.repeat(cells - filled) }
306}
307
308// ---------- data ----------
309
310const toWindow = (r: SessionRateLimit | undefined): UsageWindow | null => {
311 if (!r) return null
312 const at = r.resetsAt ? Date.parse(r.resetsAt) : NaN
313 return { percentUsed: r.percentUsed, resetsAt: Number.isFinite(at) ? at : null }
314}
315
316const toLimits = (list: readonly SessionRateLimit[]): UsageLimits | null => {
317 const fiveHour = toWindow(list.find(r => r.kind === 'five_hour'))
318 const sevenDay = toWindow(list.find(r => r.kind === 'seven_day'))
319 return fiveHour || sevenDay ? { fiveHour, sevenDay } : null
320}
321
322type ScriptOut = {
323 uncachedInput: number
324 cacheWrite: number
325 output: number
326 cacheRead: number
327 requests: number
328 mainPath: string
329 subDir: string
330 subagentFiles: number
331 sig: string
332}
333
334let nodePath: string | null = null
335let lastSig: string | null = null
336let lastPaths: { mainPath: string; subDir: string } | null = null
337let busy = false
338
339const currentSig = async ($: EngineInterface): Promise<string | null> => {
340 if (!lastPaths) return null
341 try {
342 const main = await $.fs.stat(lastPaths.mainPath)
343 const parts = [`main:${main.size}:${Math.trunc(main.mtimeMs)}`]
344 if (await $.fs.exists(lastPaths.subDir)) {
345 const files = (await $.fs.list(lastPaths.subDir))
346 .filter(f => f.kind === 'file' && f.name.endsWith('.jsonl'))
347 .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
348 for (const f of files) parts.push(`${f.name}:${f.size}:${Math.trunc(f.mtimeMs)}`)
349 }
350 return parts.join('|')
351 } catch {
352 return null
353 }
354}
355
356const runScript = async ($: EngineInterface, id: string): Promise<ScriptOut | null> => {
357 const script = `${$.plugin.root}/scripts/tokens.mjs`
358 const candidates = nodePath ? [nodePath] : NODES
359 for (const node of candidates) {
360 try {
361 const ran = await $.process.run([node, script, id], { timeoutMs: 20000 })
362 if (ran.exitCode !== 0) return null
363 nodePath = node
364 return JSON.parse(ran.stdout.trim()) as ScriptOut
365 } catch {
366 continue
367 }
368 }
369 return null
370}
371
372const refreshTokens = async ($: EngineInterface, force = false): Promise<void> => {
373 if (busy) return
374 busy = true
375 try {
376 if (!force && lastSig !== null && (await currentSig($)) === lastSig) return
377 const id = await $.session.id()
378 const out = await runScript($, id)
379 if (!out) {
380 lastSig = null
381 await update($, tokensAtom, () => null)
382 return
383 }
384 lastSig = out.sig
385 lastPaths = { mainPath: out.mainPath, subDir: out.subDir }
386 const tokens: UsageTokens = {
387 uncachedInput: out.uncachedInput,
388 cacheWrite: out.cacheWrite,
389 output: out.output,
390 cacheRead: out.cacheRead,
391 requests: out.requests,
392 isApprox: false,
393 dump: out.mainPath.split('/').pop() ?? out.mainPath,
394 subagentFiles: out.subagentFiles,
395 }
396 await update($, tokensAtom, () => tokens)
397 } finally {
398 busy = false
399 }
400}
401
402const refreshUsage = async ($: EngineInterface): Promise<void> => {
403 const u = await $.session.usage()
404 await applyUsage($, u.rateLimits, u.cost?.usd ?? null, u.context)
405}
406
407const applyUsage = async (
408 $: EngineInterface,
409 rateLimits: readonly SessionRateLimit[],
410 usd: number | null,
411 ctx: { tokens?: number; window: number; percent?: number },
412): Promise<void> => {
413 const limits = toLimits(rateLimits)
414 if (limits) await update($, limitsAtom, () => limits)
415 await update($, costAtom, () => usd)
416 const context: UsageContext = {
417 tokens: ctx.tokens ?? null,
418 window: ctx.window,
419 percent: ctx.percent ?? null,
420 }
421 await update($, contextAtom, () => context)
422}
423
424const refreshAll = async ($: EngineInterface, force = false): Promise<void> => {
425 const now = await $.clock.now()
426 await update($, nowAtom, () => now)
427 await refreshUsage($)
428 await refreshTokens($, force)
429}
430
431const readModel = async ($: EngineInterface): Promise<Model> => ({
432 limits: await read($, limitsAtom),
433 cost: await read($, costAtom),
434 context: await read($, contextAtom),
435 tokens: await read($, tokensAtom),
436 fallback: await read($, fallbackAtom),
437 now: (await read($, nowAtom)) || (await $.clock.now()),
438})
439
440const summary = (m: Model): string => {
441 const parts: string[] = []
442 for (const s of specs(m)) {
443 const text = s.parts
444 .filter((p): p is Extract<Part, { t: 'text' }> => p.t === 'text')
445 .map(p => p.s)
446 if (s.kind === 'h5' || s.kind === 'd7') {
447 parts.push(`${text[0]} ${text[1]}${text[2] ? ` (${text[2]})` : ''}`)
448 } else {
449 const label = { in: 'in', out: 'out', cache: 'cache', cost: 'cost' }[s.kind]
450 parts.push(`${label} ${s.kind === 'cost' ? '$' : ''}${text[0]}`)
451 }
452 }
453 return parts.length ? parts.join(' · ') : 'No usage yet: figures appear after the first model response.'
454}
455
456// ---------- hooks ----------
457
458export const register: Register = on => {
459 on('session.start', async ($, e, next) => {
460 await $.command.register({
461 name: 'kullanim',
462 description: 'Usage band: refresh and summarize (gizle hides, goster shows)',
463 })
464 $.clock.every(30_000, () => void refreshAll($).catch(() => undefined))
465 void refreshAll($, true).catch(() => undefined)
466 return next(e)
467 })
468
469 on('session.measure', async ($, e, next) => {
470 await applyUsage($, e.rateLimits, e.cost?.usd ?? null, e.context)
471 void refreshTokens($).catch(() => undefined)
472 return next(e)
473 })
474
475 on('turn.complete', async ($, e, next) => {
476 const u = e.usage
477 if (u) {
478 await update($, fallbackAtom, f => ({
479 uncachedInput: f.uncachedInput + u.input_tokens,
480 cacheWrite: f.cacheWrite + u.cache_creation_input_tokens,
481 output: f.output + u.output_tokens,
482 cacheRead: f.cacheRead + u.cache_read_input_tokens,
483 turns: f.turns + 1,
484 }))
485 }
486 void refreshTokens($).catch(() => undefined)
487 return next(e)
488 })
489
490 on('command.run', { command: 'kullanim' }, async ($, e) => {
491 const arg = e.args.trim().toLowerCase()
492 if (arg === 'gizle') {
493 await update($, hiddenAtom, () => true)
494 return { text: 'Usage band hidden. /kullanim goster brings it back.' }
495 }
496 if (arg === 'goster' || arg === 'göster') {
497 await update($, hiddenAtom, () => false)
498 return { text: 'Usage band shown.' }
499 }
500 await refreshAll($, true)
501 return { text: summary(await readModel($)) }
502 })
503
504 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
505 if (e.props.hasSurvey || (await read($, hiddenAtom))) return next(e)
506 const m = await readModel($)
507 if (specs(m).length === 0) return next(e)
508
509 if (e.surface === 'terminal') {
510 const { Box, Text } = $.ui.resolve(e)
511 const groups: PillSpec[][] = [[], [], []]
512 for (const s of specs(m)) groups[s.group]?.push(s)
513 return (
514 <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
515 {groups
516 .filter(g => g.length > 0)
517 .map((g, gi) => (
518 <Box key={`g${gi}`} flexDirection="row" flexWrap="wrap" columnGap={2}>
519 {g.map(s => (
520 <Text key={s.kind} color={TERM_COLOR[s.kind]}>
521 {s.parts.map((p, i) => {
522 const gap = i < s.parts.length - 1 ? ' ' : ''
523 if (p.t === 'icon') return `${TERM_ICON[p.name] ?? ''}${gap}`
524 if (p.t === 'sep') return `·${gap}`
525 if (p.t === 'text') {
526 return p.bold ? <Text bold>{p.s + gap}</Text> : p.s + gap
527 }
528 const b = termBar(p.pct, p.elapsed)
529 const cells = (b.fill + b.rest).split('')
530 return (
531 <Text color={LEVEL_COLOR[level(p.pct)]}>
532 {cells.map((c, ci) => (ci === b.mark ? '│' : c)).join('') + gap}
533 </Text>
534 )
535 })}
536 </Text>
537 ))}
538 </Box>
539 ))}
540 </Box>
541 )
542 }
543
544 const els = $.ui.resolve(e)
545 if (!('Svg' in els)) return next(e)
546 const { Box, Svg } = els
547 const pills = renderSvgPills(m)
548 const groups = [0, 1, 2]
549 .map(g => pills.filter(p => p.group === g))
550 .filter(g => g.length > 0)
551 return (
552 <Box flexDirection="row" flexWrap="wrap" columnGap={3} rowGap={1}>
553 {groups.map((g, gi) => (
554 <Box key={`g${gi}`} flexDirection="row" flexWrap="wrap" columnGap={1}>
555 {g.map(p => (
556 <Svg key={p.kind} source={p.source} alt={p.alt} width={p.width} height={H} isInteractive />
557 ))}
558 </Box>
559 ))}
560 </Box>
561 )
562 })
563}
564types/index.d.ts 39 lines1export type UsageWindow = { percentUsed: number; resetsAt: number | null }
2
3export type UsageLimits = { fiveHour: UsageWindow | null; sevenDay: UsageWindow | null }
4
5export type UsageContext = { tokens: number | null; window: number; percent: number | null }
6
7export type UsageTokens = {
8 uncachedInput: number
9 cacheWrite: number
10 output: number
11 cacheRead: number
12 requests: number
13 isApprox: boolean
14 dump: string
15 subagentFiles: number
16}
17
18export type UsageFallback = {
19 uncachedInput: number
20 cacheWrite: number
21 output: number
22 cacheRead: number
23 turns: number
24}
25
26declare module 'claude-code' {
27 interface PluginState {
28 'usage-band': {
29 limits: UsageLimits | null
30 cost: number | null
31 context: UsageContext | null
32 tokens: UsageTokens | null
33 fallback: UsageFallback
34 isHidden: boolean
35 now: number
36 }
37 }
38}
39