Right-docked pane: context used, session spend, remaining credit and clickable pending tasks that fill a ready-made prompt

A Claude Code mod: a right-docked pane showing
Inside Claude Code:
/plugin marketplace add santisanudo13/claude-hud
/plugin install hud@claude-hud
Then restart Claude Code (or run /reload-plugins).
If you already have a local copy of hud loaded (e.g. from ~/.claude/my-mods/hud), disable it first so the two don't clash.
git clone https://github.com/santisanudo13/claude-hud ~/.claude/my-mods/hud
Then load it as a local mod from that folder (/plugin) and restart Claude Code.
Type definitions in .claude-plugin/types/ are generated by Claude Code and not committed. The mod lives in hooks/register.tsx.
MIT
hooks/register.tsx 207 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Credit, Snapshot } from '../types'
5
6const PANE = 'hud'
7const snapshot = atom({ plugin: 'hud', key: 'snapshot' } as const, null)
8
9const WARN = 80
10const BAR = 16
11const REFRESH_MS = 60_000
12
13let credit: Credit | null = null
14let fetchedAt = 0
15
16const kilo = (n: number) => {
17 const k = Math.round(n / 100) / 10
18 if (n >= 999_950) {
19 return `${Math.round(n / 100_000) / 10}M`
20 }
21 return n >= 1000 ? `${k}k` : String(n)
22}
23const bar = (p: number) => {
24 const f = Math.max(0, Math.min(BAR, Math.round((p / 100) * BAR)))
25 return { on: '█'.repeat(f), off: '░'.repeat(BAR - f) }
26}
27const tone = (p: number) => (p >= 90 ? 'red' : p >= WARN ? 'yellow' : 'green')
28const money = (n: number) => `$${n.toFixed(2)}`
29const label = (k: string) =>
30 k === 'five_hour' ? '5h window' : k === 'seven_day' ? '7d week' : k === 'spend_limit' ? 'Credit' : k
31
32const take = (
33 e: {
34 context: { tokens?: number; window: number; percent?: number }
35 rateLimits: { kind: string; percentUsed: number }[]
36 cost?: { usd: number }
37 },
38 extra: Credit | null,
39 prev?: Snapshot | null,
40): Snapshot => ({
41 credit: extra,
42 tokens: e.context.tokens ?? null,
43 window: e.context.window,
44 percent: e.context.percent ?? null,
45 limits: e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed })),
46 // usage of each window when this session first saw it; a window that renewed (value dropped) restarts from 0
47 base: e.rateLimits.map(l => {
48 const start = prev?.base?.find(b => b.kind === l.kind)?.percentUsed ?? l.percentUsed
49 return { kind: l.kind, percentUsed: l.percentUsed < start ? 0 : start }
50 }),
51 usd: e.cost?.usd ?? null,
52})
53
54const safe = async (fn: () => Promise<unknown>) => {
55 try {
56 await fn()
57 } catch {
58 // tracking must never break the tool call
59 }
60}
61
62// Same figures /usage shows; the credential never leaves the host.
63async function loadCredit($) {
64 const auth = await $.session.authorize()
65 if (!auth) {
66 return
67 }
68 const res = await $.http.fetch('https://api.anthropic.com/api/oauth/usage', {
69 auth: auth.handle,
70 headers: { 'anthropic-beta': 'oauth-2025-04-20' },
71 })
72 if (!res.ok) {
73 return
74 }
75 const spend = JSON.parse(res.text)?.spend
76 if (spend?.used && spend?.limit) {
77 const div = 10 ** (spend.limit.exponent ?? 2)
78 credit = { used: spend.used.amount_minor / div, limit: spend.limit.amount_minor / div, currency: spend.limit.currency }
79 }
80}
81
82async function refresh($) {
83 const now = Date.now()
84 if (now - fetchedAt >= REFRESH_MS) {
85 fetchedAt = now
86 await safe(() => loadCredit($))
87 }
88 const usage = await $.session.usage()
89 await update($, snapshot, prev => take(usage, credit, prev))
90}
91
92export const register: Register = on => {
93 on('session.start', async ($, e, next) => {
94 await $.command.register({ name: 'hud', description: 'Open the usage and context panel' })
95 await safe(() => refresh($))
96 void $.ui.open({ id: PANE, title: 'HUD' })
97
98 return next(e)
99 })
100
101 on('command.run', { command: 'hud' }, async $ => {
102 await $.ui.open({ id: PANE, title: 'HUD' })
103
104 return { text: 'HUD opened.' }
105 })
106
107 on('session.measure', async ($, e, next) => {
108 await update($, snapshot, prev => take(e, credit, prev))
109 void safe(() => refresh($))
110
111 return next(e)
112 })
113
114 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
115 const { Box, Text } = $.ui.resolve(e)
116 const s = await read($, snapshot)
117
118 const Meter = ({ percent }: { percent: number }) => {
119 const b = bar(percent)
120 return (
121 <Text>
122 <Text color={tone(percent)}>{b.on}</Text>
123 <Text dimColor>{b.off}</Text>
124 <Text bold color={tone(percent)}> {Math.round(percent)}%</Text>
125 </Text>
126 )
127 }
128
129 const Card = ({ title, color, right, children }: { title: string; color: string; right?: string; children: unknown }) => (
130 <Box flexDirection="column" borderStyle="round" borderColor={color} paddingX={1} marginBottom={1}>
131 <Box width="100%">
132 <Box flexGrow={1}>
133 <Text bold color={color}>
134 {title}
135 </Text>
136 </Box>
137 {right && <Text dimColor>{right}</Text>}
138 </Box>
139 {children}
140 </Box>
141 )
142
143 return (
144 <Box flexDirection="column">
145 <Card title="◉ CONTEXT" color="cyan">
146 {s?.percent != null ? (
147 <Box flexDirection="column">
148 <Meter percent={s.percent} />
149 <Text dimColor>
150 {s.tokens != null ? kilo(s.tokens) : '?'} of {kilo(s.window)} tokens
151 </Text>
152 </Box>
153 ) : (
154 <Text dimColor>waiting for first measurement…</Text>
155 )}
156 </Card>
157
158 <Card title="◆ SESSION" color="magenta">
159 <Text bold color="green">
160 {s?.usd != null ? money(s.usd) : '—'}
161 </Text>
162 <Text dimColor>spent this session</Text>
163 {(s?.limits ?? []).map(l => {
164 const used = Math.max(0, l.percentUsed - (s?.base?.find(b => b.kind === l.kind)?.percentUsed ?? l.percentUsed))
165 return (
166 <Text key={l.kind}>
167 <Text dimColor>{label(l.kind)}: </Text>
168 <Text bold color={tone(used * 4)}>
169 +{used.toFixed(used < 10 ? 1 : 0)}%
170 </Text>
171 </Text>
172 )
173 })}
174 </Card>
175
176 <Card
177 title="◈ CREDIT"
178 color="yellow"
179 >
180 {s?.credit != null && (
181 <Box flexDirection="column">
182 <Meter percent={(s.credit.used / s.credit.limit) * 100} />
183 <Text>
184 <Text bold color={tone((s.credit.used / s.credit.limit) * 100)}>
185 {money(s.credit.limit - s.credit.used)}
186 </Text>
187 <Text dimColor> left of {money(s.credit.limit)}</Text>
188 </Text>
189 <Text dimColor>month: {money(s.credit.used)} used</Text>
190 </Box>
191 )}
192 {(s?.limits ?? []).map(l => (
193 <Box key={l.kind} flexDirection="column">
194 <Box width="100%">
195 <Text dimColor>{label(l.kind)}</Text>
196 </Box>
197 <Meter percent={l.percentUsed} />
198 </Box>
199 ))}
200 {s?.credit == null && (s?.limits ?? []).length === 0 && <Text dimColor>no limit data</Text>}
201 </Card>
202
203 </Box>
204 )
205 })
206}
207types/index.d.ts 20 lines1export type Limit = { kind: string; percentUsed: number }
2export type Credit = { used: number; limit: number; currency: string }
3export type Snapshot = {
4 credit: Credit | null
5 tokens: number | null
6 window: number
7 percent: number | null
8 limits: Limit[]
9 base: Limit[]
10 usd: number | null
11}
12export type Status = 'pending' | 'in_progress' | 'completed'
13export type Item = { id: string; text: string; status: Status }
14
15declare module 'claude-code' {
16 interface PluginState {
17 hud: { snapshot: Snapshot | null }
18 }
19}
20