Status band above the prompt: directory, git, model, effort, cost, rate limits and context.

My dotfiles, I don't think they'll work for ya but who knows. This is for Macs (./setup). I have a runbook in Obsidian with instructions.
This now also supports Linux "remote devboxes", for the age of agents. Devboxes can run ./setup --profile devbox, either via its Ansible playbook or via dotfiles-sync from the Mac, which pulls the latest pushed commits on the box and re-runs setup there.
hooks/register.tsx 354 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Git, Limit, Usage, Where } from '../types'
5
6const usage = atom({ plugin: 'status-band', key: 'usage' } as const, null)
7const git = atom({ plugin: 'status-band', key: 'git' } as const, null)
8const where = atom({ plugin: 'status-band', key: 'where' } as const, null)
9const effort = atom({ plugin: 'status-band', key: 'effort' } as const, null)
10
11// `claude-opus-4-5-20251101` reads as `Opus 4.5`; an alias (`opus`) as `Opus`.
12const modelName = (id: string) => {
13 const [, family = id, version] = /^(?:claude-)?([a-z]+)(?:-([\d-]+?))?(?:-\d{8})?(\[.*\])?$/.exec(id) ?? []
14 const suffix = /\[.*\]$/.exec(id)?.[0] ?? ''
15 const name = family.charAt(0).toUpperCase() + family.slice(1)
16
17 return `${name}${version === undefined ? '' : ` ${version.replaceAll('-', '.')}`}${suffix}`
18}
19
20const refreshWhere = async ($: EngineInterface) => {
21 const [cwd, home, model] = await Promise.all([
22 $.session.cwd(),
23 $.env.get('HOME'),
24 $.session.model(),
25 ])
26 const short = home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd
27 const found: Where = { cwd: short, model: modelName(model) }
28 const shown = await read($, where)
29
30 if (shown?.cwd !== found.cwd || shown.model !== found.model) {
31 await update($, where, () => found)
32 }
33}
34
35const refreshGit = async ($: EngineInterface) => {
36 let found: Git | null = null
37
38 try {
39 const head = await $.process.run(['git', 'symbolic-ref', '--short', '-q', 'HEAD'])
40 const sha =
41 head.exitCode === 0 ? head : await $.process.run(['git', 'rev-parse', '--short', 'HEAD'])
42
43 if (sha.exitCode === 0) {
44 const stat = await $.process.run(['git', 'diff', '--shortstat', 'HEAD'])
45 const added = /(\d+) insertion/.exec(stat.stdout)?.[1] ?? '0'
46 const deleted = /(\d+) deletion/.exec(stat.stdout)?.[1] ?? '0'
47 found = { branch: sha.stdout.trim(), added: Number(added), deleted: Number(deleted) }
48 }
49 } catch {
50 found = null
51 }
52
53 await update($, git, () => found)
54}
55
56const refreshUsage = async ($: EngineInterface) => {
57 const { context, rateLimits, cost } = await $.session.usage()
58 const found: Usage = { ...context, limits: rateLimits, usd: cost?.usd }
59 await update($, usage, () => found)
60}
61
62// The engine draws the band's `[-]` collapse control over its top right.
63const COLLAPSE_CELLS = 4
64
65const GREEN = '#4ade80'
66const AMBER = '#fbbf24'
67const RED = '#f87171'
68const TRACK = '#88888855'
69
70const levelColor = (percent: number) => (percent >= 85 ? RED : percent >= 60 ? AMBER : GREEN)
71
72const clamp = (percent: number) => Math.min(100, Math.max(0, percent))
73
74const timeLeft = (limit: Limit | undefined, now: number) => {
75 const ms = limit?.resetsAt === undefined ? NaN : Date.parse(limit.resetsAt) - now
76
77 if (Number.isNaN(ms) || ms <= 0) {
78 return undefined
79 }
80
81 const hours = Math.floor(ms / 3_600_000)
82 const minutes = Math.floor((ms % 3_600_000) / 60_000)
83
84 return hours >= 24 ? `${Math.floor(hours / 24)}d${hours % 24}h` : `${hours}h${minutes}m`
85}
86
87const tokens = (count: number) =>
88 count >= 1_000_000 ? `${(count / 1_000_000).toFixed(1)}M` : `${Math.round(count / 1000)}k`
89
90const ring = (percent: number) => {
91 const around = 2 * Math.PI * 7
92 const arc = (clamp(percent) / 100) * around
93
94 return (
95 '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">' +
96 `<circle cx="9" cy="9" r="7" fill="none" stroke="${TRACK}" stroke-width="2.5"/>` +
97 `<circle cx="9" cy="9" r="7" fill="none" stroke="${levelColor(percent)}" stroke-width="2.5" ` +
98 `stroke-linecap="round" stroke-dasharray="${arc.toFixed(2)} ${around.toFixed(2)}" ` +
99 'transform="rotate(-90 9 9)"/></svg>'
100 )
101}
102
103const METER_WIDTH = 132
104
105const meterStops = (percent: number) =>
106 percent >= 85 ? [RED, '#fca5a5'] : percent >= 60 ? [AMBER, '#fcd34d'] : [GREEN, '#86efac']
107
108// The context meter: a pill in the level color, a little lighter at its tip.
109const meter = (percent: number) => {
110 const inner = METER_WIDTH - 2
111 const fill = percent <= 0 ? 0 : Math.max(8, (clamp(percent) / 100) * inner)
112 const [from, to] = meterStops(percent)
113
114 return (
115 `<svg xmlns="http://www.w3.org/2000/svg" width="${METER_WIDTH}" height="18" viewBox="0 0 ${METER_WIDTH} 18">` +
116 `<defs><linearGradient id="fill" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="${from}"/>` +
117 `<stop offset="1" stop-color="${to}"/></linearGradient></defs>` +
118 `<rect x="1" y="5" width="${inner}" height="8" rx="4" fill="${TRACK}"/>` +
119 `<rect x="1" y="5" width="${fill.toFixed(1)}" height="8" rx="4" fill="url(#fill)"/>` +
120 '</svg>'
121 )
122}
123
124const COIN =
125 '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">' +
126 `<circle cx="9" cy="9" r="7.5" fill="${GREEN}" fill-opacity="0.16" stroke="${GREEN}" stroke-width="1.5"/>` +
127 `<text x="9" y="12.6" text-anchor="middle" font-family="system-ui, sans-serif" font-size="10.5" ` +
128 `font-weight="700" fill="${GREEN}">$</text></svg>`
129
130type Gauge = { label: string; percent: number; note?: string }
131
132const gaugesOf = (used: Usage | null, now: number): Gauge[] => {
133 const fiveHour = used?.limits.find(limit => limit.kind === 'five_hour')
134 const sevenDay = used?.limits.find(limit => limit.kind === 'seven_day')
135 const found: Gauge[] = []
136
137 if (fiveHour !== undefined) {
138 found.push({ label: '5h', percent: fiveHour.percentUsed, note: timeLeft(fiveHour, now) })
139 }
140
141 if (sevenDay !== undefined) {
142 found.push({ label: '7d', percent: sevenDay.percentUsed, note: timeLeft(sevenDay, now) })
143 }
144
145 if (used?.percent !== undefined) {
146 const note = used.tokens === undefined ? undefined : `${tokens(used.tokens)}/${tokens(used.window)}`
147 found.push({ label: 'ctx', percent: used.percent, note })
148 }
149
150 return found
151}
152
153export const register: Register = on => {
154 on('session.start', async ($, e, next) => {
155 await Promise.all([refreshWhere($), refreshGit($), refreshUsage($)])
156
157 return next(e)
158 })
159
160 // `/model`, `/cd` and the config menu change what the band shows with no
161 // turn in between, and a picker may answer after its command returned.
162 on('command.run', async ($, e, next) => {
163 const ran = await next(e)
164 await Promise.all([refreshWhere($), refreshGit($)])
165 $.clock.after(2000, () => void refreshWhere($))
166
167 return ran
168 })
169
170 on('config.set', async ($, e, next) => {
171 const set = await next(e)
172 await refreshWhere($)
173
174 return set
175 })
176
177 on('prompt.submit', async ($, e, next) => {
178 await refreshWhere($)
179
180 return next(e)
181 })
182
183 on('turn.step', async function* ($, e, next) {
184 await update($, effort, () => (e.effort === undefined ? null : String(e.effort)))
185
186 return yield* next(e)
187 })
188
189 on('turn.complete', async ($, e, next) => {
190 const done = await next(e)
191 await Promise.all([refreshWhere($), refreshGit($)])
192
193 return done
194 })
195
196 on('session.measure', async ($, e, next) => {
197 const found: Usage = { ...e.context, limits: e.rateLimits, usd: e.cost?.usd }
198 await update($, usage, () => found)
199
200 return next(e)
201 })
202
203 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
204 const place = await read($, where)
205
206 if (e.props.hasSurvey || place === null) {
207 return next(e)
208 }
209
210 const repo = await read($, git)
211 const used = await read($, usage)
212 const level = await read($, effort)
213 const gauges = gaugesOf(used, await $.clock.now())
214 const cost = used?.usd === undefined ? undefined : `$${used.usd.toFixed(2)}`
215 const isDirty = repo !== null && repo.added + repo.deleted > 0
216 const columns = e.props.bodyColumns
217
218 // Desktop: a card of ring gauges. The app already shows folder and model.
219 if (e.surface === 'desktop') {
220 const { Box, Text, Svg } = $.ui.resolve(e)
221 const context = gauges.find(gauge => gauge.label === 'ctx')
222
223 return (
224 <Box
225 flexDirection="row"
226 justifyContent="space-between"
227 alignItems="center"
228 borderStyle="round"
229 borderDimColor
230 paddingX={1}
231 >
232 <Box flexDirection="row" alignItems="center" gap={3}>
233 {gauges
234 .filter(gauge => gauge.label !== 'ctx')
235 .map(gauge => (
236 <Box flexDirection="row" alignItems="center" gap={1}>
237 <Svg
238 source={ring(gauge.percent)}
239 alt={`${gauge.label} ${Math.round(gauge.percent)}%`}
240 width={18}
241 height={18}
242 />
243 <Text bold>{Math.round(gauge.percent)}%</Text>
244 <Text dimColor>
245 {gauge.label}
246 {gauge.note === undefined ? '' : ` · ${gauge.note}`}
247 </Text>
248 </Box>
249 ))}
250 {context !== undefined && (
251 <Box flexDirection="row" alignItems="center" gap={1}>
252 <Svg
253 source={meter(context.percent)}
254 alt={`context ${context.percent}%`}
255 width={METER_WIDTH}
256 height={18}
257 />
258 <Text bold>{context.percent}%</Text>
259 <Text dimColor>ctx{context.note === undefined ? '' : ` · ${context.note}`}</Text>
260 </Box>
261 )}
262 </Box>
263 <Box flexDirection="row" alignItems="center" gap={2}>
264 {repo !== null && <Text dimColor>⎇ {repo.branch}</Text>}
265 {repo !== null && isDirty && <Text color={GREEN}>+{repo.added}</Text>}
266 {repo !== null && isDirty && <Text color={RED}>−{repo.deleted}</Text>}
267 {used?.usd !== undefined && (
268 <Box flexDirection="row" alignItems="center" gap={1}>
269 <Svg source={COIN} alt="cost" width={18} height={18} />
270 <Box flexDirection="row">
271 <Text bold color={GREEN}>
272 {Math.floor(used.usd)}
273 </Text>
274 <Text color={GREEN} dimColor>
275 .{used.usd.toFixed(2).split('.')[1]}
276 </Text>
277 </Box>
278 </Box>
279 )}
280 </Box>
281 </Box>
282 )
283 }
284
285 const { Box, Text } = $.ui.resolve(e)
286 const cells = columns >= 110 ? 8 : 5
287 const isNarrow = columns < 70
288
289 const meters = (
290 <Box flexDirection="row" gap={isNarrow ? 2 : 3}>
291 {gauges.map(gauge => {
292 const filled = Math.round((clamp(gauge.percent) * cells) / 100)
293
294 return (
295 <Box flexDirection="row" gap={1}>
296 <Text dimColor>{gauge.label}</Text>
297 {!isNarrow && (
298 <Box flexDirection="row">
299 <Text color={levelColor(gauge.percent)}>{'━'.repeat(filled)}</Text>
300 <Text dimColor>{'━'.repeat(cells - filled)}</Text>
301 </Box>
302 )}
303 <Text bold color={levelColor(gauge.percent)}>
304 {Math.round(gauge.percent)}%
305 </Text>
306 {!isNarrow && gauge.note !== undefined && <Text dimColor>{gauge.note}</Text>}
307 </Box>
308 )
309 })}
310 </Box>
311 )
312
313 if (isNarrow) {
314 return (
315 <Box flexDirection="row" justifyContent="space-between" width={columns}>
316 {meters}
317 {cost !== undefined && <Text color={GREEN}>{cost}</Text>}
318 </Box>
319 )
320 }
321
322 return (
323 <Box flexDirection="column" width={columns}>
324 <Box flexDirection="row" justifyContent="space-between" paddingRight={COLLAPSE_CELLS}>
325 <Box flexDirection="row" gap={2}>
326 <Text bold color="cyan">
327 {place.cwd}
328 </Text>
329 {repo !== null && <Text color="magenta">⎇ {repo.branch}</Text>}
330 {repo !== null && isDirty && (
331 <Box flexDirection="row" gap={1}>
332 <Text color={GREEN}>+{repo.added}</Text>
333 <Text color={RED}>−{repo.deleted}</Text>
334 </Box>
335 )}
336 </Box>
337 <Box flexDirection="row" gap={1}>
338 <Text>{place.model}</Text>
339 {level !== null && <Text dimColor>· {level}</Text>}
340 </Box>
341 </Box>
342 <Box flexDirection="row" justifyContent="space-between">
343 {meters}
344 {cost !== undefined && (
345 <Text bold color={GREEN}>
346 {cost}
347 </Text>
348 )}
349 </Box>
350 </Box>
351 )
352 })
353}
354types/index.d.ts 25 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Usage = {
4 tokens?: number
5 window: number
6 percent?: number
7 limits: Limit[]
8 usd?: number
9}
10
11export type Git = { branch: string; added: number; deleted: number }
12
13export type Where = { cwd: string; model: string }
14
15declare module 'claude-code' {
16 interface PluginState {
17 'status-band': {
18 usage: Usage | null
19 git: Git | null
20 where: Where | null
21 effort: string | null
22 }
23 }
24}
25