SLOPSHOPPER

usage-band-for-claude

Shows model, effort, ultracode, context use, rate limits and their reset times above the prompt

newbandprompttimer
v0.1.0MITupdated 2026-10-15Norrkhan/usage-band-for-claude
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-band-for-claude
› 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 ◆ opus-5-5 ultracode off ctx █████░░░░░ 49% 97k/200k 5h ▰▰▱▱▱ 31% ⏱ 30m ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
◆ opus-5-5 ultracode off ctx █████░░░░░ 49% 97k/200k 5h ▰▰▱▱▱ 31% ⏱ 30m
README

usage-band-for-claude

A Claude Code mod that shows a live, colour-coded usage line right above the prompt:

◆ opus-5-5 ⚡high ultracode off   ctx █████░░░░░░░░░░░░░░░ 23% 45k/200k   5h ▰▰▰▰▱▱▱▱▱▱ 42% ↻ 2h13m   7d ▰▱▱▱▱▱▱▱▱▱ 12% ↻ 3d4h   ⏱ 25m
  • Model in use.
  • ⚡ Effort of the latest request, coloured by level (low → max).
  • Ultracode: a highlighted badge when it's on for the session or for one prompt.
  • ctx: a progress bar of how full the context window is, with tokens used.
  • 5h / 7d: how much of your usage limits you've used, and when each resets (↻).
  • ⏱ how long the session has been running.

Bars and percentages turn green → yellow (60%) → red (85%). Colours follow your Claude Code theme, and the bars shrink on narrow terminals. The 5h/7d limits show on a Claude subscription after the first reply of a session.

Install

At the prompt of a Claude Code terminal session:

/plugin install usage-band-for-claude --marketplace Norrkhan/usage-band-for-claude

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

From a shell:

claude plugin install usage-band-for-claude --marketplace Norrkhan/usage-band-for-claude

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

Collapse the line with [-] or ctrl+x ctrl+a.

License

MIT

Source 2 files
hooks/register.tsx 195 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Limit, Segment, Snapshot } from '../types'
5
6const snapshot = atom({ plugin: 'usage-band-for-claude', key: 'snapshot' } as const, null)
7// The effort the last main-loop request actually used, and whether the
8// current prompt opted into ultracode with the keyword.
9const stepEffort = atom({ plugin: 'usage-band-for-claude', key: 'stepEffort' } as const, null)
10const ultracodeTurn = atom({ plugin: 'usage-band-for-claude', key: 'ultracodeTurn' } as const, false)
11
12const EFFORT_COLORS: Record<string, string> = {
13  low: 'inactive',
14  medium: 'suggestion',
15  high: 'success',
16  xhigh: 'warning',
17  max: 'error',
18}
19
20const LIMIT_NAMES: Record<string, string> = {
21  five_hour: '5h',
22  seven_day: '7d',
23  spend_limit: 'spend',
24}
25
26const GAP: Segment = { text: '   ' }
27
28export function formatTokens(n: number): string {
29  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
30  if (n >= 1_000) return `${Math.round(n / 1_000)}k`
31  return String(n)
32}
33
34export function formatSpan(ms: number): string {
35  const minutes = Math.max(0, Math.round(ms / 60_000))
36  const days = Math.floor(minutes / 1440)
37  const hours = Math.floor((minutes % 1440) / 60)
38  const mins = minutes % 60
39  if (days > 0) return `${days}d${hours}h`
40  if (hours > 0) return `${hours}h${String(mins).padStart(2, '0')}m`
41  return `${mins}m`
42}
43
44// Green while there's room, yellow when getting close, red near the limit.
45export function levelColor(percent: number): string {
46  if (percent >= 85) return 'error'
47  if (percent >= 60) return 'warning'
48  return 'success'
49}
50
51export function bar(percent: number, width: number, full: string, empty: string): Segment[] {
52  const clamped = Math.min(100, Math.max(0, percent))
53  const filled = Math.round((clamped / 100) * width)
54  return [
55    { text: full.repeat(filled), color: levelColor(percent) },
56    { text: empty.repeat(width - filled), color: 'inactive' },
57  ]
58}
59
60export function limitSegments(limit: Limit, now: number, width: number): Segment[] {
61  const name = LIMIT_NAMES[limit.kind] ?? limit.kind
62  const segments: Segment[] = [
63    { text: `${name} `, dim: true },
64    ...bar(limit.percentUsed, width, '▰', '▱'),
65    { text: ` ${Math.round(limit.percentUsed)}%`, color: levelColor(limit.percentUsed), bold: true },
66  ]
67  const resetsAt = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN
68  if (!Number.isNaN(resetsAt)) {
69    segments.push({ text: ` ↻ ${formatSpan(resetsAt - now)}`, color: 'suggestion' })
70  }
71  return segments
72}
73
74export function lineSegments(s: Snapshot, columns: number): Segment[] {
75  // Narrow terminals get shorter bars so everything still fits on one row.
76  const isNarrow = columns < 110
77  const ctxWidth = isNarrow ? 10 : 20
78  const limitWidth = isNarrow ? 5 : 10
79
80  const percent = s.contextPercent ?? 0
81  const used = s.contextTokens ?? 0
82  const segments: Segment[] = [
83    { text: `◆ ${s.model.replace(/^claude-/, '')}`, color: 'claude', bold: true },
84  ]
85  if (s.effort !== undefined) {
86    segments.push(
87      { text: ' ' },
88      { text: `⚡${s.effort}`, color: EFFORT_COLORS[s.effort] ?? 'suggestion', bold: true },
89    )
90  }
91  if (s.ultracode === 'off') {
92    segments.push({ text: ' ' }, { text: 'ultracode off', dim: true })
93  } else {
94    const label = s.ultracode === 'turn' ? 'ULTRACODE (this prompt)' : 'ULTRACODE'
95    segments.push({ text: ' ' }, { text: ` ${label} `, color: 'inverseText', backgroundColor: 'planMode', bold: true })
96  }
97  segments.push(
98    GAP,
99    { text: 'ctx ', dim: true },
100    ...bar(percent, ctxWidth, '█', '░'),
101    { text: ` ${percent}%`, color: levelColor(percent), bold: true },
102    { text: ` ${formatTokens(used)}/${formatTokens(s.contextWindow)}`, dim: true },
103  )
104
105  for (const limit of s.limits) {
106    segments.push(GAP, ...limitSegments(limit, s.now, limitWidth))
107  }
108
109  segments.push(GAP, { text: `⏱ ${formatSpan(s.now - s.startedAt)}`, dim: true })
110  return segments.filter(seg => seg.text !== '')
111}
112
113async function refresh($: EngineInterface): Promise<void> {
114  const [usage, model, now, settings, lastEffort, isUltracodeTurn] = await Promise.all([
115    $.session.usage(),
116    $.session.model(),
117    $.clock.now(),
118    $.settings.read(),
119    read($, stepEffort),
120    read($, ultracodeTurn),
121  ])
122  const configuredEffort = typeof settings.effortLevel === 'string' ? settings.effortLevel : undefined
123  const next: Snapshot = {
124    model,
125    effort: lastEffort ?? configuredEffort,
126    ultracode: settings.ultracode === true ? 'on' : isUltracodeTurn ? 'turn' : 'off',
127    contextTokens: usage.context.tokens,
128    contextWindow: usage.context.window,
129    contextPercent: usage.context.percent,
130    limits: usage.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
131    startedAt: usage.startedAt,
132    now,
133  }
134  await update($, snapshot, () => next)
135}
136
137export const register: Register = on => {
138  let tick: { cancel: () => void } | undefined
139
140  on('session.start', async ($, e, next) => {
141    const result = await next(e)
142    await refresh($)
143    tick?.cancel()
144    // Keep the reset countdowns current while idle.
145    tick = $.clock.every(60_000, () => refresh($))
146    return result
147  })
148
149  on('prompt.submit', async ($, e, next) => {
150    // Never let the band get in the way of sending a prompt.
151    await update($, ultracodeTurn, () => /\bultracode\b/i.test(e.text))
152      .then(() => refresh($))
153      .catch(() => undefined)
154    return next(e)
155  })
156
157  on('turn.step', async function* ($, e, next) {
158    if (e.agentId === undefined && e.effort !== undefined) {
159      const effort = String(e.effort)
160      await read($, stepEffort)
161        .then(async last => {
162          if (last === effort) return
163          await update($, stepEffort, () => effort)
164          await refresh($)
165        })
166        .catch(() => undefined)
167    }
168    return yield* next(e)
169  })
170
171  on('session.measure', async ($, e, next) => {
172    await refresh($)
173    return next(e)
174  })
175
176  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
177    const s = await read($, snapshot)
178    if (e.props.hasSurvey || s === null) {
179      return next(e)
180    }
181
182    const { Text } = $.ui.resolve(e)
183
184    return (
185      <Text wrap="truncate-end">
186        {lineSegments(s, e.props.bodyColumns).map(seg => (
187          <Text color={seg.color} backgroundColor={seg.backgroundColor} dimColor={seg.dim} weight={seg.bold ? 'bold' : undefined}>
188            {seg.text}
189          </Text>
190        ))}
191      </Text>
192    )
193  })
194}
195
types/index.d.ts 22 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Segment = { text: string; color?: string; backgroundColor?: string; dim?: boolean; bold?: boolean }
4
5export type Snapshot = {
6  model: string
7  effort?: string
8  ultracode: 'on' | 'turn' | 'off'
9  contextTokens?: number
10  contextWindow: number
11  contextPercent?: number
12  limits: Limit[]
13  startedAt: number
14  now: number
15}
16
17declare module 'claude-code' {
18  interface PluginState {
19    'usage-band-for-claude': { snapshot: Snapshot | null; stepEffort: string | null; ultracodeTurn: boolean }
20  }
21}
22