Shows context fill and usage limits in the footer under the prompt in the desktop app, where no status line exists

Shows context and usage limits in the desktop app, where Claude Code has no status line. One short line sits in the footer under the prompt, beside the app's model and effort pickers:
C 10% · 5h 74% · W 98%
| Part | Meaning |
|---|---|
C | How full the context window is |
5h | Use of the five-hour rate-limit window |
W | Use of the weekly rate-limit window |
Each value is green below 60%, yellow from 60% and red from 85%. A window without a reading, as off a subscription, is left out. Mode labels the app shows in the same footer stay in front.
Mode, folder, model and effort are not repeated: the app's own controls already show them. In the terminal the mod draws nothing, so an existing status line is not duplicated.
After every tool call and turn, when the desktop app connects, and every 30 seconds.
hooks/register.tsx 96 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Usage } from '../types'
5import { spansOf } from './format'
6
7const REFRESH_MS = 30_000
8
9const usage = atom({ plugin: 'status-band', key: 'usage' } as const, null)
10
11async function isDrawnRemotely($: EngineInterface): Promise<boolean> {
12 const surfaces = await $.session.surfaces()
13
14 return surfaces.some(i => i !== 'terminal')
15}
16
17async function refresh($: EngineInterface): Promise<void> {
18 if (!(await isDrawnRemotely($))) {
19 return
20 }
21 const current = await $.session.usage()
22 const windowOf = (kind: string) => current.rateLimits.find(i => i.kind === kind)?.percentUsed ?? null
23 const next: Usage = {
24 contextPercent: current.context.percent ?? null,
25 fiveHourPercent: windowOf('five_hour'),
26 sevenDayPercent: windowOf('seven_day'),
27 }
28 await update($, usage, () => next)
29}
30
31export const register: Register = on => {
32 on('session.start', async ($, e, next) => {
33 const result = await next(e)
34 $.clock.every(REFRESH_MS, () => {
35 void refresh($)
36 })
37 await refresh($)
38
39 return result
40 })
41
42 on('session.attach', async ($, e, next) => {
43 const result = await next(e)
44 await refresh($)
45
46 return result
47 })
48
49 on('tool.call', async ($, e, next) => {
50 const result = await next(e)
51 if (e.agentId === undefined) {
52 await refresh($)
53 }
54
55 return result
56 })
57
58 on('turn.complete', async ($, e, next) => {
59 const result = await next(e)
60 await refresh($)
61
62 return result
63 })
64
65 // The desktop draws the SessionMode labels in the footer under the prompt,
66 // beside its model and effort pickers: the one slot with room for a short line.
67 // Other mods may draw there too; what they drew beneath stays.
68 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
69 if (e.surface === 'terminal') {
70 return next(e)
71 }
72 const current = await read($, usage)
73 const spans = current === null ? [] : spansOf(current)
74 const below = await next(e)
75 if (spans.length === 0) {
76 return below
77 }
78 const { Box, Text } = $.ui.resolve(e)
79 const modes = e.props.modes.length === 0 ? null : <Text dimColor>{`${e.props.modes.join(' & ')} · `}</Text>
80
81 return (
82 <Box flexDirection="row">
83 {below.type === 'engine' ? modes : below}
84 {below.type === 'engine' ? null : <Text dimColor> · </Text>}
85 <Text wrap="truncate-end">
86 {spans.map((span, index) => (
87 <Text key={`span-${index}`} color={span.color} dimColor={span.isDim}>
88 {span.text}
89 </Text>
90 ))}
91 </Text>
92 </Box>
93 )
94 })
95}
96hooks/format.ts 39 lines1import type { Span, Usage } from '../types'
2
3export const COLORS = {
4 green: '#22a355',
5 yellow: '#d4a017',
6 red: '#e5484d',
7}
8
9export function usageColor(percent: number): string {
10 if (percent >= 85) {
11 return COLORS.red
12 }
13 if (percent >= 60) {
14 return COLORS.yellow
15 }
16
17 return COLORS.green
18}
19
20export function spansOf(usage: Usage): Span[] {
21 const readings: [string, number | null][] = [
22 ['C', usage.contextPercent],
23 ['5h', usage.fiveHourPercent],
24 ['W', usage.sevenDayPercent],
25 ]
26 const spans: Span[] = []
27 for (const [label, percent] of readings) {
28 if (percent === null) {
29 continue
30 }
31 if (spans.length > 0) {
32 spans.push({ text: ' · ', isDim: true })
33 }
34 spans.push({ text: `${label} `, isDim: true }, { text: `${Math.round(percent)}%`, color: usageColor(percent) })
35 }
36
37 return spans
38}
39types/index.d.ts 20 lines1export type Usage = {
2 contextPercent: number | null
3 fiveHourPercent: number | null
4 sevenDayPercent: number | null
5}
6
7export type Span = {
8 text: string
9 color?: string
10 isDim?: boolean
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'status-band': {
16 usage: Usage | null
17 }
18 }
19}
20