SLOPSHOPPER

usage-panel

Battery-style usage bars above the prompt: context, rate limits, cost

newbandcommand
v0.1.0no licenseupdated 2026-10-26psychemisss/claude-usage-panel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-panel
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /usage-panel ⎿ usage-panel: Usage bars hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

claude-usage-panel

A Claude Code mod that shows your usage as battery-style bars above the prompt: context window fill, the 5-hour and 7-day rate-limit windows (with reset countdowns, and the 7-day reset date and time), and the session's estimated API cost.

 ctx ▰▰▰▱▱▱▱▱▱▱ 34%   5h ▰▰▱▱▱▱▱▱▱▱ 18% ↻2h14m   7d ▰▱▱▱▱▱▱▱▱▱ 9% ↻4d3h (Thu 30 Oct 14:00)   ~$1.47 API

Bars turn yellow at 70% and red at 90%. They refresh after each turn. Rate limits are account-wide, so usage from other sessions shows up after this session's next reply. Reset times are hidden below 100 columns.

/usage-panel shows or hides the bars.

Install

At the prompt of a Claude Code terminal session:

/plugin install usage-panel --marketplace psychemisss/claude-usage-panel

From a shell:

claude plugin install usage-panel --marketplace psychemisss/claude-usage-panel

In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with psychemisss/claude-usage-panel, then Add plugin.

Source 2 files
hooks/register.tsx 107 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Usage } from '../types'
5
6const usage = atom({ plugin: 'usage-panel', key: 'usage' } as const, null, {
7  shape: 'usage-1',
8})
9const isHidden = atom({ plugin: 'usage-panel', key: 'isHidden' } as const, false)
10
11const LABELS: Record<string, string> = {
12  five_hour: '5h',
13  seven_day: '7d',
14  spend_limit: 'spend',
15}
16
17// one cell per 10%, battery style
18const cells = (pct: number) => Math.round(Math.min(100, Math.max(0, pct)) / 10)
19
20const tone = (pct: number) =>
21  pct >= 90 ? 'error' : pct >= 70 ? 'warning' : 'success'
22
23const span = (ms: number) => {
24  const m = Math.max(0, Math.round(ms / 60000))
25  const d = Math.floor(m / 1440)
26  const h = Math.floor((m % 1440) / 60)
27  return d ? `${d}d${h}h` : h ? `${h}h${m % 60}m` : `${m}m`
28}
29
30// local time, e.g. "Thu 30 Oct 14:00"
31const when = (iso: string) => {
32  const t = new Date(iso)
33  const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][t.getDay()]
34  const mon = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][t.getMonth()]
35  const pad = (n: number) => String(n).padStart(2, '0')
36  return `${day} ${t.getDate()} ${mon} ${pad(t.getHours())}:${pad(t.getMinutes())}`
37}
38
39export const register: Register = on => {
40  on('session.start', async ($, e, next) => {
41    await $.command.register({
42      name: 'usage-panel',
43      description: 'Show or hide the Claude usage bars above the prompt',
44    })
45    const { startedAt, context, rateLimits, cost } = await $.session.usage()
46    await update($, usage, () => ({ startedAt, context, rateLimits, cost }))
47
48    return next(e)
49  })
50
51  on('command.run', { command: 'usage-panel' }, async $ => {
52    const hidden = await update($, isHidden, v => !v)
53
54    return { text: hidden ? 'Usage bars hidden.' : 'Usage bars shown.' }
55  })
56
57  // ponytail: pushed after each turn, so bars refresh per turn, not per second
58  on('session.measure', async ($, e, next) => {
59    await update($, usage, prev => ({
60      startedAt: prev?.startedAt ?? 0,
61      context: e.context,
62      rateLimits: e.rateLimits,
63      cost: e.cost,
64    }))
65
66    return next(e)
67  })
68
69  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
70    const u = await read($, usage)
71    if (e.props.hasSurvey || !u || (await read($, isHidden))) return next(e)
72
73    const { Box, Text } = $.ui.resolve(e)
74    const cols = e.viewport?.columns ?? 80
75    const showReset = cols >= 100
76    const now = await $.clock.now()
77    const ctx = u.context.percent ?? 0
78
79    const meter = (label: string, pct: number, extra?: string) => (
80      <Box marginRight={3}>
81        <Text dimColor>{label} </Text>
82        <Text color={tone(pct)}>{'▰'.repeat(cells(pct))}</Text>
83        <Text dimColor>{'▱'.repeat(10 - cells(pct))}</Text>
84        <Text> {pct}%</Text>
85        {extra && <Text dimColor> ↻{extra}</Text>}
86      </Box>
87    )
88
89    return (
90      <Box flexDirection="row" paddingX={1} marginTop={1}>
91        {meter('ctx', ctx)}
92        {u.rateLimits.map(r =>
93          meter(
94            LABELS[r.kind] ?? r.kind,
95            r.percentUsed,
96            showReset && r.resetsAt
97              ? span(Date.parse(r.resetsAt) - now) +
98                  (r.kind === 'seven_day' ? ` (${when(r.resetsAt)})` : '')
99              : undefined,
100          ),
101        )}
102        {u.cost && <Text dimColor>~${u.cost.usd.toFixed(2)} API</Text>}
103      </Box>
104    )
105  })
106}
107
types/index.d.ts 15 lines
1export type RateLimit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Usage = {
4  startedAt: number
5  context: { tokens?: number; window: number; percent?: number }
6  rateLimits: RateLimit[]
7  cost?: { usd: number }
8}
9
10declare module 'claude-code' {
11  interface PluginState {
12    'usage-panel': { usage: Shaped<Usage | null>; isHidden: boolean }
13  }
14}
15