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

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
AbovePrompt slot).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).
It shows up on its own. /usage-band hides or shows the line.
claude plugin uninstall usage-band@usage-band
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.
MIT
contact@iamkhalid.xyz
hooks/register.tsx 171 lines1import { 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}
171types/index.d.ts 8 lines1export 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