SLOPSHOPPER

usage-band

Always-visible plan usage limits in the prompt footer

newspinnertimer
v0.1.0no licenseupdated 2026-10-15Lingfar/claude-code-mods/usage-band
A shopper browsing a rack in a slop shop
README

claude-code-mods

A small Claude Code mod, made for the desktop app's Code tab (it also works in the terminal).

ModWhat it shows
usage-bandYour plan limits in the prompt footer: 5h ◔ 23% ↻2h13 │ 7j ◕ 91% ↻3j (orange from 75 %, red from 90 %).

Install

Run this in a terminal claude session (the desktop Code tab does not accept /plugin, but mods installed at the user scope load there too):

/plugin install usage-band --marketplace Lingfar/claude-code-mods

Answer y to add the marketplace, then pick the user scope. New sessions, desktop ones included, load the mod.

To update later:

/plugin marketplace update claude-code-mods

Run from a clone

To edit a mod and see the change, load it from its folder instead of installing it.

git clone git@github.com:Lingfar/claude-code-mods.git ~/.claude/mods

Then list the mod folders in ~/.claude/settings.json (: separates paths on macOS and Linux, ; on Windows):

{
  "env": {
    "CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/mods/usage-band"
  }
}

For a single terminal session, claude --plugin-dir ~/.claude/mods/usage-band does the same.

Before committing a change:

claude plugin validate usage-band
claude plugin test usage-band

Notes

  • Built against Claude Code 2.1.295. The mods API is early access and may change between releases.
Source 3 files
hooks/register.tsx 82 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { UsageSnapshot } from '../types'
5
6import { gaugesOf, ringGlyph, snapshotOf } from './format'
7
8const usage = atom({ plugin: 'usage-band', key: 'usage' } as const, null)
9const now = atom({ plugin: 'usage-band', key: 'now' } as const, 0)
10
11const PERCENT_COLORS = { normal: undefined, warning: 'warning', error: 'error' } as const
12const RING_COLORS = { normal: 'success', warning: 'warning', error: 'error' } as const
13const SEPARATOR = ' │ '
14
15async function refreshNow($: EngineInterface): Promise<void> {
16  const nowMs = await $.clock.now()
17
18  await update($, now, () => nowMs)
19}
20
21async function storeUsage($: EngineInterface, latest: UsageSnapshot | null): Promise<void> {
22  if (latest === null || JSON.stringify(await read($, usage)) === JSON.stringify(latest)) {
23    return
24  }
25
26  await update($, usage, () => latest)
27}
28
29export const register: Register = on => {
30  on('session.start', async ($, e, next) => {
31    await storeUsage($, snapshotOf(await $.session.usage()))
32    await refreshNow($)
33
34    $.clock.every(10_000, () => {
35      $.session
36        .usage()
37        .then(latest => storeUsage($, snapshotOf(latest)))
38        .catch(() => undefined)
39    })
40    $.clock.every(60_000, () => {
41      refreshNow($).catch(() => undefined)
42    })
43
44    return next(e)
45  })
46
47  on('session.measure', async ($, e, next) => {
48    await storeUsage($, snapshotOf(e))
49    await refreshNow($)
50
51    return next(e)
52  })
53
54  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
55    const snapshot = await read($, usage)
56
57    if (snapshot === null) {
58      return next(e)
59    }
60
61    const gauges = gaugesOf(snapshot, await read($, now))
62    const { Box, Text } = $.ui.resolve(e)
63
64    const segments = gauges.map(gauge => [
65      <Text>{`${gauge.label} `}</Text>,
66      <Text color={RING_COLORS[gauge.level]}>{`${ringGlyph(gauge.percent)} `}</Text>,
67      <Text bold color={PERCENT_COLORS[gauge.level]}>{`${gauge.percent}%`}</Text>,
68      ...(gauge.detail === undefined ? [] : [<Text dimColor>{` ${gauge.detail}`}</Text>]),
69    ])
70
71    if (e.props.modes.length > 0) {
72      segments.push([<Text dimColor>{e.props.modes.join(' & ')}</Text>])
73    }
74
75    return (
76      <Box key="usage" flexDirection="row" flexShrink={0}>
77        <Text wrap="end">{segments.flatMap((segment, index) => (index === 0 ? segment : [<Text dimColor>{SEPARATOR}</Text>, ...segment]))}</Text>
78      </Box>
79    )
80  })
81}
82
hooks/format.ts 72 lines
1import type { SessionRateLimit } from 'claude-code'
2
3import type { UsageSnapshot } from '../types'
4
5const LIMIT_LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7j', spend_limit: 'Budget' }
6
7const RING_GLYPHS = ['○', '◔', '◑', '◕', '●']
8
9export type Level = 'normal' | 'warning' | 'error'
10
11export type Gauge = { label: string; percent: number; detail?: string; level: Level }
12
13export function snapshotOf(measure: { rateLimits: SessionRateLimit[] }): UsageSnapshot | null {
14  if (measure.rateLimits.length === 0) {
15    return null
16  }
17
18  return { limits: measure.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt })) }
19}
20
21export function formatCountdown(resetsAt: string | undefined, nowMs: number): string | undefined {
22  const resetMs = resetsAt === undefined ? NaN : Date.parse(resetsAt)
23
24  if (Number.isNaN(resetMs) || nowMs === 0) {
25    return undefined
26  }
27
28  const minutes = Math.max(0, Math.round((resetMs - nowMs) / 60_000))
29  const days = Math.floor(minutes / 1440)
30  const hours = Math.floor((minutes % 1440) / 60)
31  const mins = minutes % 60
32
33  if (days > 0) {
34    return `${days}j`
35  }
36
37  if (hours > 0) {
38    return `${hours}h${String(mins).padStart(2, '0')}`
39  }
40
41  return `${mins}min`
42}
43
44function levelOf(percent: number): Level {
45  if (percent >= 90) {
46    return 'error'
47  }
48
49  return percent >= 75 ? 'warning' : 'normal'
50}
51
52export function gaugesOf(usage: UsageSnapshot, nowMs: number): Gauge[] {
53  return usage.limits.map(limit => {
54    const countdown = formatCountdown(limit.resetsAt, nowMs)
55
56    return {
57      label: LIMIT_LABELS[limit.kind] ?? limit.kind,
58      percent: Math.round(limit.percentUsed),
59      detail: countdown === undefined ? undefined : `↻${countdown}`,
60      level: levelOf(limit.percentUsed),
61    }
62  })
63}
64
65export function ringGlyph(percent: number): string {
66  if (percent <= 0 || percent >= 100) {
67    return percent <= 0 ? '○' : '●'
68  }
69
70  return RING_GLYPHS[Math.min(3, Math.max(1, Math.round(percent / 25)))] ?? '◑'
71}
72
types/index.d.ts 10 lines
1export type UsageLimit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type UsageSnapshot = { limits: UsageLimit[] }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'usage-band': { usage: UsageSnapshot | null; now: number }
8  }
9}
10