SLOPSHOPPER

usage-band

Band above the prompt showing 5h/7d usage limits, context fill and session cost

newband
v0.1.1MITupdated 2026-10-25HeidariMilad/claude-usage-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band
› 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 ⟨Claude Code's own drawing⟩ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │Session31% Context49% Cost$0.42 │ │resets 09:53 AM97k of 200kthis session │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │Session31% Context49% Cost$0.42 │ │resets 09:53 AM97k of 200kthis session │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
README

usage-band

A Claude Code mod that keeps your plan usage in view: a card in the band above the prompt with your 5-hour session limit, weekly limit, context window fill and session cost. Each meter has a small pill bar (blue, amber from 75%, red from 90%) and shows when its window resets. It updates after every reply and whenever a limit moves by a point. Works in the Claude desktop app and the terminal.

Limit windows appear on Pro and Max plans; with an API key you see context and cost only.

Install

At the prompt of a terminal session:

/plugin install usage-band --marketplace HeidariMilad/claude-usage-band

Answer y to add the marketplace, then pick a scope (user scope loads it in every session).

From a shell:

claude plugin install usage-band --marketplace HeidariMilad/claude-usage-band

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

Develop

claude plugin validate .
claude plugin test .

License

MIT

Source 2 files
hooks/register.tsx 98 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-band', key: 'usage' } as const, null)
7
8const LABELS: Record<string, string> = {
9  five_hour: 'Session',
10  seven_day: 'Weekly',
11  spend_limit: 'Spend',
12}
13
14const clamp = (percent: number) => Math.min(100, Math.max(0, percent))
15
16const tint = (percent: number) => (percent >= 90 ? '#d9534f' : percent >= 75 ? '#e0a030' : '#3b82f6')
17
18// A pill like the desktop's own Context meter; the translucent track reads on light and dark.
19const pill = (percent: number) => {
20  const fill = Math.max(10, (clamp(percent) / 100) * 48)
21  return `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="10" viewBox="0 0 48 10"><rect width="48" height="10" rx="5" fill="#8884" /><rect width="${fill}" height="10" rx="5" fill="${tint(percent)}" /></svg>`
22}
23
24// An absolute local time, like claude.ai's: a countdown would go stale, since the band redraws only when a figure moves.
25const resetAt = (iso: string | undefined, now: number) => {
26  if (!iso) return undefined
27  const at = new Date(iso)
28  const time = at.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
29  const isToday = at.toDateString() === new Date(now).toDateString()
30  return `resets ${isToday ? time : `${at.toLocaleDateString([], { weekday: 'short' })} ${time}`}`
31}
32
33const thousands = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`)
34
35export const register: Register = on => {
36  on('session.start', async ($, e, next) => {
37    const { context, rateLimits, cost } = await $.session.usage()
38    await update($, usage, () => ({ context, rateLimits, cost }))
39
40    return next(e)
41  })
42
43  // Pushed after each turn and whenever a limit window moves a whole point.
44  on('session.measure', async ($, e, next) => {
45    const latest: Usage = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
46    await update($, usage, () => latest)
47
48    return next(e)
49  })
50
51  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
52    const u = await read($, usage)
53    if (e.props.hasSurvey || !u) return next(e)
54    // Other mods (ponytail) share this slot: keep theirs and add ours below.
55    // next(e) rejects only when nothing answers beneath (the test harness): the band still draws.
56    const others = await next(e).catch(() => undefined)
57
58    const ui = $.ui.resolve(e)
59    const { Box, Text } = ui
60    const Svg = 'Svg' in ui ? ui.Svg : undefined
61    const now = await $.clock.now()
62
63    // `detail` is the caption under the label; the terminal has no Svg, so it gets the number alone.
64    const meter = (label: string, percent: number, detail?: string) => (
65      <Box flexDirection="column">
66        <Box flexDirection="row" alignItems="center" gap="sm">
67          <Text>{label}</Text>
68          <Text dimColor tabular>{Math.round(percent)}%</Text>
69          {Svg && <Svg source={pill(percent)} alt={`${label} ${Math.round(percent)}%`} width={48} height={10} />}
70        </Box>
71        {detail && <Text size="caption" dimColor>{detail}</Text>}
72      </Box>
73    )
74
75    const ctx = u.context
76
77    return (
78      <Box flexDirection="column" gap="sm">
79        {others}
80        <Box flexDirection="row" flexWrap="wrap" columnGap="lg" rowGap="sm" borderStyle="round" borderColor="subtle" paddingX="md" paddingY="sm">
81          {u.rateLimits.map(w => meter(LABELS[w.kind] ?? w.kind, w.percentUsed, resetAt(w.resetsAt, now)))}
82          {ctx.percent !== undefined &&
83            meter('Context', ctx.percent, ctx.tokens === undefined ? undefined : `${thousands(ctx.tokens)} of ${thousands(ctx.window)}`)}
84          {u.cost && (
85            <Box flexDirection="column">
86              <Box flexDirection="row" gap="sm">
87                <Text>Cost</Text>
88                <Text dimColor tabular>${u.cost.usd.toFixed(2)}</Text>
89              </Box>
90              <Text size="caption" dimColor>this session</Text>
91            </Box>
92          )}
93        </Box>
94      </Box>
95    )
96  })
97}
98
types/index.d.ts 12 lines
1export type Usage = {
2  context: { tokens?: number; window: number; percent?: number }
3  rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
4  cost?: { usd: number }
5}
6
7declare module 'claude-code' {
8  interface PluginState {
9    'usage-band': { usage: Usage | null }
10  }
11}
12