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

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.
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.
claude plugin validate .
claude plugin test .
MIT
hooks/register.tsx 98 lines1import { 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}
98types/index.d.ts 12 lines1export 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