A line under the prompt with 5h / 7d usage limits and context window as colored bars

A Claude Code mod that shows your 5-hour and 7-day usage limits and the context window as colored bars, right under the prompt input.
? for shortcuts
5h ▰▰▰▰▱▱▱▱▱▱▱▱ 34% ↻ 1h12m 7d ▰▱▱▱▱▱▱▱▱▱▱▱ 11% ↻ 1d5h context ▰▱▱▱▱▱▱▱▱▱▱▱ 6% 59.3k/1M
↻ is the time until the window resets; refreshes every minute.▲ fast appears when you are using a window faster than its time is passing.– until the first reply of a session.At the prompt of a Claude Code terminal session:
/plugin install usage-hud --marketplace qiuyue97/claude-usage-hud
Answer y to add the marketplace, then pick the user scope so every session loads it.
From a shell:
claude plugin install usage-hud --marketplace qiuyue97/claude-usage-hud
Update later with claude plugin update usage-hud.
hooks/register.tsx 171 lines1import { atom, read, update } from 'claude-code'
2import type { Register, SessionUsage, SessionMeasureInput } from 'claude-code'
3
4import type { Limit, Snapshot } from '../types'
5
6const snapshot = atom({ plugin: 'usage-hud', key: 'snapshot' } as const, null)
7const now = atom({ plugin: 'usage-hud', key: 'now' } as const, 0)
8
9const WINDOW_MS: Record<string, number> = {
10 five_hour: 5 * 3600_000,
11 seven_day: 7 * 86400_000,
12}
13
14const LABEL: Record<string, string> = {
15 five_hour: '5h',
16 seven_day: '7d',
17 spend_limit: '$',
18}
19
20const toSnapshot = (u: SessionUsage | SessionMeasureInput): Snapshot => ({
21 context: {
22 tokens: u.context.tokens,
23 window: u.context.window,
24 percent: u.context.percent,
25 },
26 limits: u.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt })),
27})
28
29export const tone = (pct: number) => (pct < 50 ? 'success' : pct < 80 ? 'warning' : 'error')
30
31export const tokens = (n: number) =>
32 n >= 1_000_000
33 ? `${+(n / 1_000_000).toFixed(1)}M`
34 : n >= 1000
35 ? `${+(n / 1000).toFixed(1)}k`
36 : `${n}`
37
38export const countdown = (ms: number) => {
39 const m = Math.max(0, Math.round(ms / 60_000))
40 const d = Math.floor(m / 1440)
41 const h = Math.floor((m % 1440) / 60)
42 const mm = m % 60
43
44 if (d > 0) return `${d}d${h}h`
45 if (h > 0) return `${h}h${String(mm).padStart(2, '0')}m`
46
47 return `${mm}m`
48}
49
50/** Fraction of the window already elapsed, or undefined when unknown. */
51const pace = (limit: Limit, at: number) => {
52 const span = WINDOW_MS[limit.kind]
53 if (!span || !limit.resetsAt || !at) return undefined
54 const left = Date.parse(limit.resetsAt) - at
55
56 return Math.min(1, Math.max(0, 1 - left / span))
57}
58
59export const register: Register = on => {
60 on('session.start', async ($, e, next) => {
61 const result = await next(e)
62 const usage = await $.session.usage()
63 await update($, snapshot, () => toSnapshot(usage))
64 const at = await $.clock.now()
65 await update($, now, () => at)
66 // keep reset countdowns fresh between turns, and pick up whatever the engine last heard
67 $.clock.every(60_000, async () => {
68 const [t, u] = await Promise.all([$.clock.now(), $.session.usage()])
69 await update($, snapshot, () => toSnapshot(u))
70 await update($, now, () => t)
71 })
72
73 return result
74 })
75
76 on('session.measure', async ($, e, next) => {
77 await update($, snapshot, () => toSnapshot(e))
78 const at = await $.clock.now()
79 await update($, now, () => at)
80
81 return next(e)
82 })
83
84 // drawn under the prompt: the engine's hint line, then the HUD beneath it
85 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
86 const hint = await next(e)
87
88 // drawing is pure: before the first measurement, read the figures directly
89 const snap = (await read($, snapshot)) ?? toSnapshot(await $.session.usage())
90 let at = await read($, now)
91 if (!at) at = await $.clock.now()
92
93 const { Box, Text } = $.ui.resolve(e)
94 const isWide = (e.viewport?.columns ?? 80) >= 100
95 const width = isWide ? 12 : 10
96
97 const bar = (pct: number) => {
98 const p = Math.min(100, Math.max(0, pct))
99 // at least one filled cell once anything is used, so 1–8% still shows
100 const filled = p === 0 ? 0 : Math.max(1, Math.round((p / 100) * width))
101
102 return (
103 <Text>
104 <Text color={tone(pct)}>{'▰'.repeat(filled)}</Text>
105 <Text color="inactive" dimColor>
106 {'▱'.repeat(width - filled)}
107 </Text>
108 </Text>
109 )
110 }
111
112 const metric = (key: string, label: string, pct: number | undefined, tail: string, elapsed?: number) => (
113 <Box key={key} flexDirection="row" gap={1}>
114 <Text color="claude" weight="bold">
115 {label.padEnd(3)}
116 </Text>
117 {pct === undefined ? (
118 <Text color="inactive" dimColor>
119 {'▱'.repeat(width)}
120 </Text>
121 ) : (
122 bar(pct)
123 )}
124 <Text color={pct === undefined ? 'inactive' : tone(pct)} weight="bold">
125 {pct === undefined ? ' –' : `${String(Math.round(pct)).padStart(3)}%`}
126 </Text>
127 {tail ? <Text dimColor>{tail}</Text> : null}
128 {/* used faster than the window's time is passing */}
129 {pct !== undefined && elapsed !== undefined && pct > elapsed * 100 + 5 ? (
130 <Text color="warning">▲ fast</Text>
131 ) : null}
132 </Box>
133 )
134
135 const ctx = snap.context
136 const ctxTail = ctx.tokens === undefined ? `of ${tokens(ctx.window)}` : `${tokens(ctx.tokens)}/${tokens(ctx.window)}`
137 const rows = []
138
139 for (const kind of ['five_hour', 'seven_day']) {
140 const limit = snap.limits.find(l => l.kind === kind)
141 // past its reset time the window has rolled over; new figures arrive with the next reply
142 const expired = !!limit?.resetsAt && Date.parse(limit.resetsAt) <= at
143 const reset = !limit?.resetsAt ? '' : expired ? '↻ reset' : `↻ ${countdown(Date.parse(limit.resetsAt) - at)}`
144 rows.push(
145 metric(
146 kind,
147 LABEL[kind] ?? kind,
148 expired ? 0 : limit?.percentUsed,
149 reset,
150 expired ? undefined : limit && pace(limit, at),
151 ),
152 )
153 }
154
155 for (const limit of snap.limits.filter(l => !WINDOW_MS[l.kind])) {
156 rows.push(metric(limit.kind, LABEL[limit.kind] ?? limit.kind, limit.percentUsed, ''))
157 }
158
159 rows.push(metric('context', 'context', ctx.percent, ctxTail))
160
161 return (
162 <Box flexDirection="column">
163 {hint}
164 <Box flexDirection={isWide ? 'row' : 'column'} columnGap={3}>
165 {rows}
166 </Box>
167 </Box>
168 )
169 })
170}
171types/index.d.ts 13 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Snapshot = {
4 context: { tokens?: number; window: number; percent?: number }
5 limits: Limit[]
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'usage-hud': { snapshot: Snapshot | null; now: number }
11 }
12}
13