SLOPSHOPPER

usage-band

Mod: a slim retro line above the prompt showing your plan's 5-hour and weekly usage limits.

newbandcommandtimer
★ 1v0.1.0MITupdated 2026-10-03iamkhalid2/usage-band/plugins/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 › /usage-band ⎿ usage-band: Usage line hidden. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

usage-band

A Claude Code mod: a slim, retro-styled line above the prompt that shows how much of your plan's usage limits you've used.

5H ▰▰▰▰▰▰▱▱▱▱▱▱ 62% · 2h 14m  ◆  WK ▰▰▱▱▱▱▱▱▱▱▱▱ 31% · 3d 4h
  • 5H: the rolling 5-hour window. WK: the weekly window.
  • Bars run through a muted 70s sunset gradient (teal, mustard, burnt orange, brick), so a fuller bar reads warmer.
  • Shows time until each window resets (on windows 70+ columns wide).
  • One line, no animation, no border. It stays out of the way.
  • Shown only on a subscription plan, once Claude Code has a reading from your first response. Without rate-limit data there is nothing to draw.
  • Works in the terminal and the desktop app (the AbovePrompt slot).

Install

claude plugin marketplace add iamkhalid2/usage-band
claude plugin install usage-band@usage-band

Start a new session (or reload plugins in a running one).

Use

It shows up on its own. /usage-band hides or shows the line.

Uninstall

claude plugin uninstall usage-band@usage-band

How it works

A hooks module (plugins/usage-band/hooks/register.tsx) listens to session.measure for the rate-limit windows and draws them in a ui.render hook on the AbovePrompt component. It reads usage figures only; it makes no network requests of its own and stores nothing outside the session.

License

MIT

Contact

contact@iamkhalid.xyz

Source 2 files
hooks/register.tsx 171 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit } from '../types'
5
6const limits = atom({ plugin: 'usage-band', key: 'limits' } as const, [])
7const nowMs = atom({ plugin: 'usage-band', key: 'nowMs' } as const, 0)
8const isHidden = atom({ plugin: 'usage-band', key: 'isHidden' } as const, false)
9
10// 70s sunset palette: teal -> mustard -> burnt orange -> brick
11const STOPS = ['#3fa7a0', '#e5b53b', '#e0752f', '#c8453f']
12const CREAM = '#f2e3c6'
13const ROSE = '#b5656b'
14const TRACK = '#6b5b4b'
15
16const LABELS: Record<string, string> = {
17  five_hour: '5H',
18  seven_day: 'WK',
19  spend_limit: '$$',
20}
21const ORDER = ['five_hour', 'seven_day']
22
23const hex = (n: number) => Math.round(n).toString(16).padStart(2, '0')
24
25// the colour at t (0..1) along the sunset gradient
26const ramp = (t: number) => {
27  const x = Math.max(0, Math.min(1, t)) * (STOPS.length - 1)
28  const i = Math.min(STOPS.length - 2, Math.floor(x))
29  const k = x - i
30  const a = STOPS[i]
31  const b = STOPS[i + 1]
32  const ch = (s: string, o: number) => parseInt(s.slice(o, o + 2), 16)
33  const mix = (o: number) => ch(a, o) + (ch(b, o) - ch(a, o)) * k
34
35  return `#${hex(mix(1))}${hex(mix(3))}${hex(mix(5))}`
36}
37
38const countdown = (iso: string | undefined, now: number) => {
39  if (!iso || !now) return ''
40  const ms = Date.parse(iso) - now
41  if (Number.isNaN(ms)) return ''
42  if (ms <= 0) return 'now'
43  const mins = Math.floor(ms / 60000)
44  const days = Math.floor(mins / 1440)
45  const hours = Math.floor((mins % 1440) / 60)
46  if (days > 0) return `${days}d ${hours}h`
47  if (hours > 0) return `${hours}h ${String(mins % 60).padStart(2, '0')}m`
48  return `${Math.max(mins, 1)}m`
49}
50
51const sorted = (list: Limit[]) =>
52  [...list].sort((a, b) => {
53    const ai = ORDER.indexOf(a.kind)
54    const bi = ORDER.indexOf(b.kind)
55    return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi)
56  })
57
58export const register: Register = on => {
59  on('session.start', async ($, e, next) => {
60    await $.command.register({
61      name: 'usage-band',
62      description: 'Show or hide the plan usage line above the prompt',
63    })
64
65    try {
66      const now = await $.clock.now()
67      await update($, nowMs, () => now)
68      const usage = await $.session.usage()
69      if (usage.rateLimits.length) await update($, limits, () => usage.rateLimits)
70    } catch {
71      // the band waits for the first measurement
72    }
73
74    // keeps the countdowns honest between measurements
75    $.clock.every(30000, async () => {
76      const now = await $.clock.now()
77      await update($, nowMs, () => now)
78    })
79
80    return next(e)
81  })
82
83  on('session.measure', async ($, e, next) => {
84    if (e.changed.includes('rateLimits')) {
85      await update($, limits, () => e.rateLimits)
86    }
87    const now = await $.clock.now()
88    await update($, nowMs, () => now)
89
90    return next(e)
91  })
92
93  on('command.run', { command: 'usage-band' }, async $ => {
94    const hidden = !(await read($, isHidden))
95    await update($, isHidden, () => hidden)
96
97    return { text: hidden ? 'Usage line hidden.' : 'Usage line shown.' }
98  })
99
100  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
101    const list = sorted(await read($, limits))
102
103    if (e.props.hasSurvey || list.length === 0 || (await read($, isHidden))) {
104      return next(e)
105    }
106
107    const now = (await read($, nowMs)) || (await $.clock.now())
108    const { Box, Text } = $.ui.resolve(e)
109
110    const cols = e.props.bodyColumns
111    const isWide = cols >= 70
112    const barWidth = Math.max(6, Math.min(14, Math.floor((cols - (isWide ? 40 : 24)) / list.length)))
113
114    const parts: unknown[] = []
115
116    list.forEach((limit, n) => {
117      const pct = Math.max(0, Math.min(100, limit.percentUsed))
118      const filled = Math.round((pct / 100) * barWidth)
119      const reset = countdown(limit.resetsAt, now)
120
121      if (n > 0) {
122        parts.push(
123          <Text key={`${limit.kind}-d`} color={ROSE}>
124            {'  ◆  '}
125          </Text>,
126        )
127      }
128
129      parts.push(
130        <Text key={`${limit.kind}-l`} bold color={CREAM}>
131          {`${LABELS[limit.kind] ?? limit.kind} `}
132        </Text>,
133      )
134
135      for (let i = 0; i < barWidth; i++) {
136        parts.push(
137          i < filled ? (
138            <Text key={`${limit.kind}-${i}`} color={ramp(i / (barWidth - 1))}>
139              ▰
140            </Text>
141          ) : (
142            <Text key={`${limit.kind}-${i}`} color={TRACK}>
143              ▱
144            </Text>
145          ),
146        )
147      }
148
149      parts.push(
150        <Text key={`${limit.kind}-p`} bold color={ramp(pct / 100)}>
151          {` ${Math.round(pct)}%`}
152        </Text>,
153      )
154
155      if (isWide && reset) {
156        parts.push(
157          <Text key={`${limit.kind}-r`} color={TRACK}>
158            {` · ${reset}`}
159          </Text>,
160        )
161      }
162    })
163
164    return (
165      <Box justifyContent="center" width="100%">
166        <Text wrap="truncate">{parts}</Text>
167      </Box>
168    )
169  })
170}
171
types/index.d.ts 8 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-band': { limits: Limit[]; nowMs: number; isHidden: boolean }
6  }
7}
8