SLOPSHOPPER

usage-panel

Right-hand pane with live token usage, context window, cost, plan limits and lifetime totals

panecommandprompt
v0.2.4no licenseupdated 2026-10-17catflinger5000-netizen/Claude-Usage-Panel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-panel
│ ┃ Usage ✕ › fix the failing auth test and add an audit log call │ ┃ │ ┃ Context window ⏺ Read(src/auth.ts) │ ┃ 49% ⎿ Read 6 lines │ ┃ 97.4K of 200.0K tokens · 102.6K left ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ Current session ⏺ Bash(bun test) │ ┃ 31% used ⎿ 3 pass, 1 fail │ ┃ Resetting now │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ Usage credits │ ┃ Copy your balance from Settings → Usage → ✻ Worked for 42s · done 4:20 PM │ ┃ Current balance, then run /usage-panel │ ┃ credits 12.78. The pane counts down from › /usage-panel │ ┃ it. ⎿ usage-panel: Context window: 49% (97.4K of 200.0K tokens) │ ┃ ⎿ usage-panel: Current session: 31% used · Resetting now │ ┃ Cost ⎿ usage-panel: Cost: $0.42 this session · $0.42 all time │ ┃ This session $0.42 ⎿ usage-panel: Tokens: 98.9K this session · 98.9K all time │ ┃ Per reply (avg) $0.42 ⎿ usage-panel: Messages: 1 · Replies: 1 · Sessions: 1 │ ┃ All time $0.42 ⎿ usage-panel: Model: opus-5-5 │ ┃ Per session (avg) $0.42 │ ┃ At API rates; all time counts sessions │ ┃ since 2026-10-18. On a Pro or Max plan you │ ┃ pay the plan price, plus any usage │ ┃ credits. │ ┃ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Usage
Context window 49% 97.4K of 200.0K tokens · 102.6K left Current session 31% used Resetting now Usage credits Copy your balance from Settings → Usage → Current balance, then run /usage-panel credits 12.78. The pane counts down from it. Cost This session $0.42 Per reply (avg) $0.42 All time $0.42 Per session (avg) $0.42 At API rates; all time counts sessions since 2026-10-18. On a Pro or Max plan you pay the plan price, plus any usage credits. Tokens Session All time Input 2100 2100 Output 1480 1480 Cache read 91.0K 91.0K Cache write 4300 4300 Total 98.9K 98.9K Activity Your messages 1 · 1 all time Claude replies 1 · 1 all time Last reply 98.9K tokens Sessions 1 Running for 8981h 32m Model opus-5-5
README

Usage Panel

A Claude Code mod that docks a Usage pane on the right with live stats for the session you're in, plus totals kept across every session.

What it shows

  • Context window: how full it is, as a bar, with the tokens used and left.
  • Plan usage: your current-session (5-hour) and weekly limits, with when each resets.
  • Usage credits: an estimated balance that counts down while a plan limit is used up, plus your monthly spend limit if you have one.
  • Cost: this session, average per reply, all time and average per session, at API rates.
  • Tokens: input, output, cache read and cache write, for this session and all time.
  • Activity: your messages, Claude's replies, sessions, how long this one has run, and the model.

Subagent tokens are counted in the totals but not as replies.

Install

At the prompt of a Claude Code terminal session, including a cloud session:

/plugin install usage-panel --marketplace catflinger5000-netizen/claude-usage-panel

Answer y to add the marketplace, then press Enter to pick the user scope. The pane opens straight away, and in every session after that.

From a shell:

claude plugin install usage-panel --marketplace catflinger5000-netizen/claude-usage-panel

In the desktop app, where /plugin doesn't run: click + next to the prompt box, then Plugins → Add marketplace with catflinger5000-netizen/claude-usage-panel, then Add plugin.

This repository is private, so the install only works where git already has a GitHub login that can read it (an SSH key, or gh auth login then gh auth setup-git).

Commands

CommandWhat it does
/usage-panelOpens the pane if you closed it
/usage-panel credits 12.78Sets your usage-credit balance (copy it from Settings → Usage → Current balance)
/usage-panel creditsShows the estimated balance left

Claude Code doesn't give mods the credit balance, so you enter it once and the pane counts down from there. Auto-reload top-ups aren't seen, so re-enter the balance after one.

Starting totals

On first run, the all-time totals are seeded from baseline.json. It ships as zeros; put in your own past numbers if you want the totals to include history from before you installed the mod.

Development

hooks/register.tsx    the mod: hooks, slash command and pane
types/index.d.ts      state types
tests/usage.test.tsx  tests
Source 2 files
hooks/register.tsx 430 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Credits, Lifetime, Limit, Live, Tokens } from '../types'
5
6const PANE = 'usage'
7const TITLE = 'Usage'
8const STORE_KEY = 'lifetime'
9const CREDITS_KEY = 'credits'
10// Shown in /usage-panel's reply, so a cloud session's copy can be told apart from an older one.
11const VERSION = '0.2.4'
12
13const ZERO: Tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
14
15const live = atom({ plugin: 'usage-panel', key: 'live' } as const, {
16  ...ZERO,
17  messages: 0,
18  replies: 0,
19  turns: 0,
20  lastTurnTokens: 0,
21  model: '',
22  contextWindow: 0,
23  costUsd: 0,
24  limits: [],
25  startedAt: 0,
26  isCounted: false,
27} as Live)
28
29const lifetime = atom({ plugin: 'usage-panel', key: 'lifetime' } as const, {
30  ...ZERO,
31  messages: 0,
32  replies: 0,
33  sessions: 0,
34  costUsd: 0,
35  since: '',
36} as Lifetime)
37
38// Claude Code does not give mods the credit balance, so the person enters it once
39// (/usage-panel credits 12.78) and the pane counts down from it.
40const credits = atom({ plugin: 'usage-panel', key: 'credits' } as const, null as Credits | null)
41
42const total = (t: Tokens) => t.input + t.output + t.cacheRead + t.cacheWrite
43
44function short(n: number): string {
45  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`
46  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`
47  if (n >= 1e4) return `${(n / 1e3).toFixed(1)}K`
48  return String(Math.round(n))
49}
50
51function usd(n: number): string {
52  if (n >= 1000) return `$${Math.round(n).toLocaleString('en-US')}`
53  return n > 0 && n < 0.01 ? '<$0.01' : `$${n.toFixed(2)}`
54}
55
56function fillColor(percent: number): string {
57  return percent >= 90 ? 'error' : percent >= 75 ? 'warning' : 'rate_limit_fill'
58}
59
60function resetText(iso?: string): string {
61  if (!iso) return ''
62  const at = new Date(iso)
63  const ms = at.getTime() - Date.now()
64  if (!(ms > 0)) return 'Resetting now'
65  const time = at.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).replace(':00', '').replace(' ', '').toLowerCase()
66  const sameDay = at.toDateString() === new Date().toDateString()
67  const when = sameDay ? time : `${at.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}, ${time}`
68  const h = Math.floor(ms / 3_600_000)
69  const m = Math.floor((ms % 3_600_000) / 60_000)
70  const left = h >= 24 ? `${Math.floor(h / 24)}d ${h % 24}h` : h > 0 ? `${h}h ${m}m` : `${m}m`
71  return `Resets ${when} · in ${left}`
72}
73
74// The same titles Claude's /usage screen uses.
75function limitTitle(kind: string): string {
76  if (kind === 'five_hour') return 'Current session'
77  if (kind === 'seven_day') return 'Current week (all models)'
78  if (kind === 'spend_limit') return 'Monthly spend limit'
79  return kind.replace(/_/g, ' ').replace(/^./, c => c.toUpperCase())
80}
81
82function duration(ms: number): string {
83  const minutes = Math.max(0, Math.round(ms / 60_000))
84  return minutes >= 60 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${minutes}m`
85}
86
87const toLimits = (rateLimits: Limit[]): Limit[] =>
88  rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt }))
89
90// Adds a delta to the lifetime totals kept across sessions. Re-reads the store
91// first so two open sessions do not overwrite each other's counts.
92type Counter = Exclude<keyof Lifetime, 'since' | 'sessionCosts' | 'costSince'>
93
94async function bump($: EngineInterface, delta: Partial<Record<Counter, number>>) {
95  const stored = ((await $.store.get(STORE_KEY)) as Lifetime | undefined) ?? (await read($, lifetime))
96  const next: Lifetime = { ...stored }
97  for (const k of Object.keys(delta) as Counter[]) {
98    next[k] = (stored[k] ?? 0) + (delta[k] ?? 0)
99  }
100  await $.store.set(STORE_KEY, next)
101  await update($, lifetime, () => next)
102}
103
104// The all-time cost is the sum of each session's own total, as /cost reports it, so
105// reloads, restarts and missed measurements cannot lose or double-count any of it.
106async function recordSessionCost($: EngineInterface, usd: number) {
107  const id = await $.session.id()
108  const stored = ((await $.store.get(STORE_KEY)) as Lifetime | undefined) ?? (await read($, lifetime))
109  // Totals counted before v0.2.1 were increments that cannot be split by session; they restart here.
110  const costs = stored.sessionCosts ?? {}
111  if ((costs[id] ?? -1) >= usd && stored.sessionCosts) return
112  const sessionCosts = { ...costs, [id]: Math.max(costs[id] ?? 0, usd) }
113  const next: Lifetime = {
114    ...stored,
115    sessionCosts,
116    costUsd: Object.values(sessionCosts).reduce((a, b) => a + b, 0),
117    costSince: stored.sessionCosts ? stored.costSince : new Date().toISOString(),
118  }
119  await $.store.set(STORE_KEY, next)
120  await update($, lifetime, () => next)
121}
122
123// Credits pay for usage only while a plan limit is used up; then the session's
124// cost, at API rates, is what they are charged.
125const onCredits = (limits: Limit[]) =>
126  limits.some(l => (l.kind === 'five_hour' || l.kind === 'seven_day') && l.percentUsed >= 100)
127
128async function spendCredits($: EngineInterface, usd: number) {
129  const stored = (await $.store.get(CREDITS_KEY)) as Credits | undefined
130  if (!stored) return
131  const next = { ...stored, spentUsd: stored.spentUsd + usd }
132  await $.store.set(CREDITS_KEY, next)
133  await update($, credits, () => next)
134}
135
136async function loadLifetime($: EngineInterface): Promise<Lifetime> {
137  const stored = (await $.store.get(STORE_KEY)) as Lifetime | undefined
138  if (stored) return stored
139  // First run: seed from the transcripts counted when the mod was made.
140  let seed: Partial<Lifetime> = {}
141  try {
142    seed = JSON.parse(await $.fs.read(`${$.plugin.root}/baseline.json`))
143  } catch {}
144  const first: Lifetime = {
145    input: seed.input ?? 0,
146    output: seed.output ?? 0,
147    cacheRead: seed.cacheRead ?? 0,
148    cacheWrite: seed.cacheWrite ?? 0,
149    messages: seed.messages ?? 0,
150    replies: seed.replies ?? 0,
151    sessions: seed.sessions ?? 0,
152    costUsd: 0,
153    since: new Date().toISOString(),
154  }
155  await $.store.set(STORE_KEY, first)
156  return first
157}
158
159// The pane's main figures as plain lines, for /usage-panel where no pane can be drawn.
160async function summary($: EngineInterface): Promise<string> {
161  const s = await read($, live)
162  const life = await read($, lifetime)
163  const wallet = await read($, credits)
164  const lines = [
165    s.contextPercent === undefined
166      ? 'Context window: waiting for the first reply'
167      : `Context window: ${s.contextPercent}% (${short(s.contextTokens ?? 0)} of ${short(s.contextWindow)} tokens)`,
168    ...s.limits.map(l => `${limitTitle(l.kind)}: ${l.percentUsed}% used${l.resetsAt ? ` · ${resetText(l.resetsAt)}` : ''}`),
169  ]
170  if (wallet) lines.push(`Usage credits (est.): ${usd(Math.max(0, wallet.balanceUsd - wallet.spentUsd))}`)
171  lines.push(
172    `Cost: ${usd(s.costUsd)} this session · ${usd(life.costUsd)} all time`,
173    `Tokens: ${short(total(s))} this session · ${short(total(life))} all time`,
174    `Messages: ${s.messages} · Replies: ${s.replies} · Sessions: ${short(life.sessions)}`,
175  )
176  if (s.model !== '') lines.push(`Model: ${s.model.replace(/^claude-/, '')}`)
177  return lines.join('\n')
178}
179
180export const register: Register = on => {
181  on('session.start', async ($, e, next) => {
182    const result = await next(e)
183
184    await $.command.register({
185      name: 'usage-panel',
186      description: 'Open the usage stats pane, or set your usage-credit balance',
187      argumentHint: '[credits <balance>]',
188    })
189    const savedCredits = (await $.store.get(CREDITS_KEY)) as Credits | undefined
190    await update($, credits, () => savedCredits ?? null)
191
192    const loaded = await loadLifetime($)
193    await update($, lifetime, () => loaded)
194
195    const now = await read($, live)
196    if (!now.isCounted) {
197      const usage = await $.session.usage()
198      await update($, live, l => ({
199        ...l,
200        isCounted: true,
201        startedAt: usage.startedAt,
202        contextTokens: usage.context.tokens,
203        contextWindow: usage.context.window,
204        contextPercent: usage.context.percent,
205        costUsd: usage.cost?.usd ?? 0,
206        limits: toLimits(usage.rateLimits),
207      }))
208      await bump($, { sessions: 1 })
209    }
210    const opening = await $.session.usage()
211    if (opening.cost) await recordSessionCost($, opening.cost.usd)
212
213    void $.ui.open({ id: PANE, title: TITLE })
214
215    return result
216  })
217
218  on('command.run', { command: 'usage-panel' }, async ($, e) => {
219    const set = /^\s*credits\s+\$?(\d+(?:\.\d{1,2})?)\s*$/i.exec(e.args)
220    if (set) {
221      const next: Credits = { balanceUsd: Number(set[1]), setAt: new Date().toISOString(), spentUsd: 0 }
222      await $.store.set(CREDITS_KEY, next)
223      await update($, credits, () => next)
224      return { text: `Usage-credit balance set to ${usd(next.balanceUsd)}. The pane counts down from here as credits are spent.` }
225    }
226    if (/^\s*credits\s*$/i.test(e.args)) {
227      const c = await read($, credits)
228      return {
229        text: c
230          ? `About ${usd(Math.max(0, c.balanceUsd - c.spentUsd))} left (${usd(c.balanceUsd)} entered ${c.setAt.slice(0, 10)}, ${usd(c.spentUsd)} spent since).`
231          : 'No balance set. Copy it from Settings → Usage → Current balance: /usage-panel credits 12.78',
232      }
233    }
234    const opened = await $.ui.open({ id: PANE, title: TITLE })
235
236    // The figures always come as text too: a cloud session's page can count the pane as
237    // placed and still not show it, and a -p run draws no panes at all.
238    const note = opened.isPlaced ? 'Usage pane opened.' : `The pane can't be shown here (${opened.reason}).`
239    return { text: `${await summary($)}\n\n${note} (usage-panel ${VERSION})` }
240  })
241
242  on('prompt.submit', async ($, e, next) => {
243    const result = await next(e)
244    if (!e.isMeta) {
245      try {
246        await update($, live, l => ({ ...l, messages: l.messages + 1 }))
247        await bump($, { messages: 1 })
248      } catch {}
249    }
250
251    return result
252  })
253
254  on('turn.complete', async ($, e, next) => {
255    const result = await next(e)
256    const u = e.usage
257    if (u) {
258      const delta: Tokens = {
259        input: u.input_tokens,
260        output: u.output_tokens,
261        cacheRead: u.cache_read_input_tokens,
262        cacheWrite: u.cache_creation_input_tokens,
263      }
264      const isMain = !e.agentId
265      await update($, live, l => ({
266        ...l,
267        input: l.input + delta.input,
268        output: l.output + delta.output,
269        cacheRead: l.cacheRead + delta.cacheRead,
270        cacheWrite: l.cacheWrite + delta.cacheWrite,
271        turns: l.turns + (isMain ? 1 : 0),
272        replies: l.replies + (isMain ? 1 : 0),
273        lastTurnTokens: isMain ? total(delta) : l.lastTurnTokens,
274        model: isMain ? u.model : l.model,
275      }))
276      await bump($, { ...delta, replies: isMain ? 1 : 0 })
277    }
278
279    return result
280  })
281
282  on('session.measure', async ($, e, next) => {
283    const before = await read($, live)
284    const cost = e.cost?.usd ?? before.costUsd
285    const spent = cost - before.costUsd
286    if (spent > 0 && onCredits(toLimits(e.rateLimits))) await spendCredits($, spent)
287    await update($, live, l => ({
288      ...l,
289      contextTokens: e.context.tokens,
290      contextWindow: e.context.window,
291      contextPercent: e.context.percent,
292      costUsd: cost,
293      limits: toLimits(e.rateLimits),
294    }))
295    if (e.cost) await recordSessionCost($, e.cost.usd)
296
297    return next(e)
298  })
299
300  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
301    const { Box, Text } = $.ui.resolve(e)
302    const s = await read($, live)
303    const life = await read($, lifetime)
304    const wallet = await read($, credits)
305
306    const heading = (text: string) => (
307      <Box marginTop={1}>
308        <Text bold color="claude">
309          {text}
310        </Text>
311      </Box>
312    )
313    const row = (label: string, value: string, color?: string) => (
314      <Box flexDirection="row" justifyContent="space-between">
315        <Text dimColor>{label}</Text>
316        <Text color={color} tabular>
317          {value}
318        </Text>
319      </Box>
320    )
321    // Claude's usage bar: a solid fill on a rate_limit_empty track, the figure to its right.
322    const meter = (percent: number, label: string) => {
323      const filled = Math.round(Math.max(0, Math.min(100, percent)))
324      return (
325        <Box flexDirection="row" alignItems="center" gap={1}>
326          <Box flexGrow={1} height={1} backgroundColor="rate_limit_empty">
327            {filled > 0 && <Box width={`${filled}%`} height={1} backgroundColor={fillColor(percent)} />}
328          </Box>
329          <Text tabular>{label}</Text>
330        </Box>
331      )
332    }
333    const note = (text: string) => (
334      <Text dimColor wrap="wrap">
335        {text}
336      </Text>
337    )
338    // Token table: label, this session, all time, in right-aligned columns.
339    const cell = (text: string, bold?: boolean, dim?: boolean) => (
340      <Box width={9} justifyContent="flex-end">
341        <Text tabular bold={bold} dimColor={dim}>
342          {text}
343        </Text>
344      </Box>
345    )
346    const tokenRow = (label: string, session: number, all: number, bold?: boolean) => (
347      <Box flexDirection="row" justifyContent="space-between">
348        <Text dimColor={!bold} bold={bold}>
349          {label}
350        </Text>
351        <Box flexDirection="row">
352          {cell(short(session), bold)}
353          {cell(short(all), bold)}
354        </Box>
355      </Box>
356    )
357
358    const pct = s.contextPercent ?? 0
359    const plan = s.limits.filter(l => l.kind !== 'spend_limit')
360    const spend = s.limits.find(l => l.kind === 'spend_limit')
361    const perReply = s.replies > 0 ? s.costUsd / s.replies : 0
362    const costedSessions = Object.keys(life.sessionCosts ?? {}).length
363    const perSession = costedSessions > 0 ? life.costUsd / costedSessions : 0
364
365    return (
366      <Box flexDirection="column" paddingX={1}>
367        {heading('Context window')}
368        {s.contextPercent === undefined ? (
369          note('Waiting for the first reply.')
370        ) : (
371          <Box flexDirection="column">
372            {meter(pct, `${pct}%`)}
373            {note(`${short(s.contextTokens ?? 0)} of ${short(s.contextWindow)} tokens · ${short(Math.max(0, s.contextWindow - (s.contextTokens ?? 0)))} left`)}
374          </Box>
375        )}
376
377        {plan.length === 0 && heading('Plan usage')}
378        {plan.length === 0 && note('No limit reading yet.')}
379        {plan.map(l => (
380          <Box flexDirection="column" key={l.kind}>
381            {heading(limitTitle(l.kind))}
382            {meter(l.percentUsed, `${l.percentUsed}% used`)}
383            {l.resetsAt && note(resetText(l.resetsAt))}
384          </Box>
385        ))}
386
387        {heading('Usage credits')}
388        {onCredits(s.limits) && <Text color="warning">● Paying from usage credits now</Text>}
389        {wallet && row('Balance (est.)', usd(Math.max(0, wallet.balanceUsd - wallet.spentUsd)), 'claude')}
390        {wallet && row('Spent since set', usd(wallet.spentUsd))}
391        {wallet &&
392          note(`${usd(wallet.balanceUsd)} entered ${wallet.setAt.slice(0, 10)}, less what was spent while a limit was used up. Auto-reload top-ups are not seen: update it with /usage-panel credits <balance>.`)}
393        {!wallet && note('Copy your balance from Settings → Usage → Current balance, then run /usage-panel credits 12.78. The pane counts down from it.')}
394        {spend && (
395          <Box flexDirection="column">
396            {meter(spend.percentUsed, `${spend.percentUsed}% used`)}
397            {note(`${limitTitle(spend.kind)}${spend.resetsAt ? ` · ${resetText(spend.resetsAt)}` : ''}`)}
398          </Box>
399        )}
400
401        {heading('Cost')}
402        {row('This session', usd(s.costUsd), 'claude')}
403        {row('Per reply (avg)', usd(perReply))}
404        {row('All time', usd(life.costUsd))}
405        {row('Per session (avg)', usd(perSession))}
406        {note(`At API rates${life.costSince ? `; all time counts sessions since ${life.costSince.slice(0, 10)}` : ''}. On a Pro or Max plan you pay the plan price, plus any usage credits.`)}
407
408        {heading('Tokens')}
409        <Box flexDirection="row" justifyContent="flex-end">
410          {cell('Session', false, true)}
411          {cell('All time', false, true)}
412        </Box>
413        {tokenRow('Input', s.input, life.input)}
414        {tokenRow('Output', s.output, life.output)}
415        {tokenRow('Cache read', s.cacheRead, life.cacheRead)}
416        {tokenRow('Cache write', s.cacheWrite, life.cacheWrite)}
417        {tokenRow('Total', total(s), total(life), true)}
418
419        {heading('Activity')}
420        {row('Your messages', `${s.messages} · ${short(life.messages)} all time`)}
421        {row('Claude replies', `${s.replies} · ${short(life.replies)} all time`)}
422        {row('Last reply', `${short(s.lastTurnTokens)} tokens`)}
423        {row('Sessions', short(life.sessions))}
424        {s.startedAt > 0 && row('Running for', duration(Date.now() - s.startedAt))}
425        {s.model !== '' && row('Model', s.model.replace(/^claude-/, ''))}
426      </Box>
427    )
428  })
429}
430
types/index.d.ts 44 lines
1export type Tokens = { input: number; output: number; cacheRead: number; cacheWrite: number }
2
3export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
4
5export type Live = Tokens & {
6  messages: number
7  replies: number
8  turns: number
9  lastTurnTokens: number
10  model: string
11  contextTokens?: number
12  contextWindow: number
13  contextPercent?: number
14  costUsd: number
15  limits: Limit[]
16  startedAt: number
17  isCounted: boolean
18}
19
20export type Lifetime = Tokens & {
21  messages: number
22  replies: number
23  sessions: number
24  costUsd: number
25  since: string
26  /** Each session's own running cost, by session id; costUsd is their sum. */
27  sessionCosts?: Record<string, number>
28  /** When per-session cost counting began. */
29  costSince?: string
30}
31
32/** The usage-credit balance the person entered, and what has been paid from credits since. */
33export type Credits = {
34  balanceUsd: number
35  setAt: string
36  spentUsd: number
37}
38
39declare module 'claude-code' {
40  interface PluginState {
41    'usage-panel': { live: Live; lifetime: Lifetime; credits: Credits | null }
42  }
43}
44