SLOPSHOPPER

usage-meter

Band above the prompt showing context window fill, rate-limit windows, and session cost.

newbandtimer
v0.1.0no licenseupdated 2026-10-06dblanken-yale/usage-meter
A shopper browsing a rack in a slop shop
README

usage-meter

A Claude Code mod that keeps your context window, usage limits, and session cost visible in a row above the prompt, so you don't have to open the usage popup to check them.

usage-meter above the prompt in Claude Code Desktop

What it shows

SectionMeans
ctxHow full the context window is, then tokens used out of the window size
5hHow much of your 5-hour limit you've used, then when it resets
7dHow much of your weekly limit you've used, then when it resets
$What this session has cost so far

These are the same figures the status line and the usage popup show.

  • Colors: each percentage is green, turns yellow at 70%, and red at 90%. The dot at the start follows the context percentage.
  • Reset times are in your local time. A reset within the next 20 hours shows the time only; a later one adds the weekday.
  • Limits only show on a Claude subscription, after the first reply reports them. Any other limit Claude Code reports (a gateway spend limit, for example) gets its own section, labeled with its raw name.

Install

Run these in Claude Code:

/plugin marketplace add dblanken-yale/usage-meter
/plugin install usage-meter@usage-meter

Or from a terminal:

claude plugin marketplace add dblanken-yale/usage-meter
claude plugin install usage-meter@usage-meter

Start a new Claude Code session, in the terminal or the desktop app.

Update

Turn on auto-update for the usage-meter marketplace in /plugin (Marketplaces tab), and new versions install when Claude Code starts. To update by hand:

claude plugin marketplace update usage-meter

Desktop app sessions pick up the new version when you start a new one.

Develop

Clone the repo and load it from the folder, so edits reload as you save:

git clone git@github.com:dblanken-yale/usage-meter.git ~/code/usage-meter
claude --plugin-dir ~/code/usage-meter

Disable the marketplace install while you do this, or it loads twice.

Bump version in .claude-plugin/plugin.json with every release. Installed copies only update when the version changes.

Notes

  • It shares the row above the prompt with other mods. It calls next(e) and stacks whatever the mods beneath drew under its own row. A mod above it that returns only its own row hides it; cache-buster stacks the same way, so the two show together. In the desktop app there's a little space between the rows; the terminal gets none, since one step of padding there is a whole blank line.
  • To check it after editing: claude plugin validate ~/code/usage-meter and claude plugin test ~/code/usage-meter.
  • tsconfig.json points at .claude-plugin/types/, which Claude Code generates and git ignores, so type-checking a fresh clone needs those files regenerated first.
Source 2 files
hooks/register.tsx 84 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5
6const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
7
8const usage = atom({ plugin: 'usage-meter', key: 'usage' } as const, null)
9const now = atom({ plugin: 'usage-meter', key: 'now' } as const, 0)
10
11const colorFor = (pct: number) => (pct >= 90 ? 'red' : pct >= 70 ? 'yellow' : 'green')
12
13const timeFmt = new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' })
14const dayTimeFmt = new Intl.DateTimeFormat(undefined, { weekday: 'short', hour: 'numeric', minute: '2-digit' })
15
16// Clock time of the reset; weekday only when it is not within the next ~day.
17function resetAt(limit: Limit, t: number) {
18  if (!limit.resetsAt) return ''
19  const at = Date.parse(limit.resetsAt)
20  return (at - t > 20 * 3_600_000 ? dayTimeFmt : timeFmt).format(at)
21}
22
23export const register: Register = on => {
24  on('session.start', async ($, e, next) => {
25    const started = await next(e)
26    const [{ context, rateLimits, cost }, t0] = await Promise.all([$.session.usage(), $.clock.now()])
27    const snap: Snapshot = { context, rateLimits, cost }
28    await update($, usage, () => snap)
29    await update($, now, () => t0)
30    $.clock.every(60_000, async () => {
31      const t = await $.clock.now()
32      await update($, now, () => t)
33    })
34    return started
35  })
36
37  on('session.measure', async ($, e, next) => {
38    const snap: Snapshot = { context: e.context, rateLimits: e.rateLimits, cost: e.cost }
39    await update($, usage, () => snap)
40    return next(e)
41  })
42
43  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44    // Stack on whatever the next plugin (e.g. cache-buster) draws instead of hiding it.
45    const below = await next(e)
46    const u = await read($, usage)
47    if (e.props.hasSurvey || u === null) return below
48
49    const t = await read($, now)
50    const { Box, Text } = $.ui.resolve(e)
51
52    const meter = (key: string, label: string, pct: number, suffix = '') => {
53      const color = colorFor(pct)
54      return (
55        <Box key={key}>
56          <Text bold>{label} </Text>
57          <Text color={color}>{Math.round(pct)}%</Text>
58          {suffix ? <Text dimColor> {suffix}</Text> : null}
59        </Box>
60      )
61    }
62
63    const ctxPct = u.context.percent ?? 0
64    const ctxK = u.context.tokens ? `${Math.round(u.context.tokens / 1000)}k/${Math.round(u.context.window / 1000)}k` : ''
65
66    const sections = [
67      meter('ctx', 'ctx', ctxPct, ctxK),
68      ...u.rateLimits.map(l => meter(l.kind, LABELS[l.kind] ?? l.kind, l.percentUsed, resetAt(l, t))),
69      ...(u.cost ? [<Text key="cost" bold>${u.cost.usd.toFixed(2)}</Text>] : []),
70    ]
71
72    return (
73      // A little space under the band above (cache-buster); on the terminal a step is a whole blank row, so skip it there.
74      <Box flexDirection="column" paddingTop={e.surface === 'terminal' ? 0 : 1}>
75        <Box>
76          <Text color={colorFor(ctxPct)}>● </Text>
77          {sections.flatMap((section, i) => (i ? [<Text key={`sep${i}`} dimColor>{'  │  '}</Text>, section] : [section]))}
78        </Box>
79        {below}
80      </Box>
81    )
82  })
83}
84
types/index.d.ts 18 lines
1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Snapshot = {
3  context: { tokens?: number; window: number; percent?: number }
4  rateLimits: Limit[]
5  cost?: { usd: number }
6}
7
8declare module 'claude-code' {
9  interface PluginState {
10    'usage-meter': {
11      /** Latest usage reading; null before the first. */
12      usage: Snapshot | null
13      /** Epoch ms, refreshed every minute so the weekday rule stays current. */
14      now: number
15    }
16  }
17}
18