Gauge in the prompt footer for the 5-hour session and weekly usage limits, then context window fill

The start of a dotty journey.
Feel welcome to copy!
make stow to populate symlinks.
stow defaultsWithout a .stow-local-ignore file, stow will use defaults, but if this file exists then the defaults will be ignored.
tmux plugins setupgit clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpmprefix + I see$ sudo apt-get install xselatuin better history searchbash <(curl --proto '=https' --tlsv1.2 -sSf https://setup.atuin.sh)atuin import auto (local syncing only, no cloud)neovim requirementssudo apt install ripgrepsudo apt install fd-find LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | \grep -Po '"tag_name": *"v\K[^"]*') curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" tar xf lazygit.tar.gz lazygit sudo install lazygit -D -t /usr/local/bin/ ``claude usage-gauge modStatus line mod in .claude/mods/usage-gauge/. It shows:
ctx 84k/1M (8%)Needs a Nerd Font for the icons.
hooks/register.tsx 178 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextUsage, ThemeKey } from 'claude-code'
3
4import type { Fill, Limit } from '../types'
5
6const limits = atom({ plugin: 'usage-gauge', key: 'limits' } as const, [] as Limit[])
7const fill = atom({ plugin: 'usage-gauge', key: 'fill' } as const, null as Fill | null)
8const model = atom({ plugin: 'usage-gauge', key: 'model' } as const, null as string | null)
9const compactions = atom({ plugin: 'usage-gauge', key: 'compactions' } as const, 0)
10
11const WIDTH = 10
12const NARROW = 100
13const LABELS: Record<string, string> = { five_hour: 'Session', seven_day: 'Week' }
14
15const ICONS: Record<string, string> = {
16 fable: '\u{F14F7}',
17 opus: '\u{F075A}',
18 sonnet: '\u{F03EB}',
19 haiku: '\u{F024A}',
20}
21
22export const modelLabel = (name: string) => {
23 const short = name.replace(/^claude-/, '')
24 const icon = ICONS[Object.keys(ICONS).find(k => short.toLowerCase().includes(k)) ?? '']
25
26 return icon ? `${icon} ${short}` : short
27}
28
29const COMPACT_ICON = '\u{F0615}'
30
31export const compactColor = (count: number): ThemeKey =>
32 count >= 10 ? 'error' : count >= 4 ? 'warning' : 'subtle'
33
34export const bar = (percent: number) => {
35 const filled = Math.round((Math.min(percent, 100) / 100) * WIDTH)
36
37 return '█'.repeat(filled) + '░'.repeat(WIDTH - filled)
38}
39
40export const colorFor = (percent: number): ThemeKey =>
41 percent >= 80 ? 'error' : percent >= 50 ? 'warning' : 'subtle'
42
43export const until = (resetsAt: string | undefined, now: number) => {
44 if (!resetsAt) {
45 return ''
46 }
47
48 const minutes = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60000))
49 const days = Math.floor(minutes / 1440)
50 const hours = Math.floor((minutes % 1440) / 60)
51
52 return days > 0 ? `${days}d${hours}h` : hours > 0 ? `${hours}h${minutes % 60}m` : `${minutes}m`
53}
54
55const short = (n: number) =>
56 n >= 1_000_000 ? `${+(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`
57
58const toFill = (c: SessionContextUsage): Fill => {
59 const b = c.breakdown
60
61 return {
62 tokens: c.tokens ?? null,
63 window: c.window,
64 percent: c.percent ?? null,
65 compact: b?.isAutoCompactEnabled && b.rawMaxTokens < c.window ? b.rawMaxTokens : null,
66 }
67}
68
69export const contextPercent = (f: Fill) =>
70 f.tokens === null
71 ? null
72 : f.compact === null
73 ? (f.percent ?? 0)
74 : Math.round((f.tokens / f.compact) * 100)
75
76export const contextLabel = (f: Fill) => {
77 const limit = f.compact ?? f.window
78 const percent = contextPercent(f)
79
80 return f.tokens === null
81 ? `ctx –/${short(limit)}`
82 : `ctx ${short(f.tokens)}/${short(limit)} (${percent}%)`
83}
84
85const refresh = async ($: EngineInterface) => {
86 const { context } = await $.session.usage({ breakdown: 'summary' })
87 await update($, fill, () => toFill(context))
88 const name = await $.session.model()
89 await update($, model, () => name)
90}
91
92export const gauges = (all: Limit[]) =>
93 Object.keys(LABELS).flatMap(kind => {
94 const limit = all.find(l => l.kind === kind)
95
96 return limit ? [{ ...limit, label: LABELS[kind] ?? kind }] : []
97 })
98
99export const register: Register = on => {
100 on('session.start', async ($, e, next) => {
101 const result = await next(e)
102 const { rateLimits } = await $.session.usage()
103 await update($, limits, () => rateLimits)
104 await refresh($)
105
106 return result
107 })
108
109 on('session.compact', async ($, e, next) => {
110 const result = await next(e)
111
112 if (!('skip' in result && result.skip) && e.trigger !== 'precompute' && !e.agentId) {
113 await update($, compactions, n => n + 1)
114 }
115
116 return result
117 })
118
119 on('session.measure', async ($, e, next) => {
120 await update($, limits, () => e.rateLimits)
121
122 if (e.changed.includes('context')) {
123 await refresh($)
124 }
125
126 return next(e)
127 })
128
129 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
130 const shown = gauges(await read($, limits))
131 const context = await read($, fill)
132 const name = await read($, model)
133 const compacted = await read($, compactions)
134
135 if (shown.length === 0 && context === null && name === null) {
136 return next(e)
137 }
138
139 const { Box, Text } = $.ui.resolve(e)
140 const now = await $.clock.now()
141 const isNarrow = (e.viewport?.columns ?? NARROW) < NARROW
142 const hasModes = e.props.modes.length > 0
143
144 return (
145 <Box flexDirection="row" flexShrink={0}>
146 {hasModes ? <Box marginRight={3}>{await next(e)}</Box> : ''}
147 {name === null ? '' : <Text>{modelLabel(name)} </Text>}
148 {compacted === 0 ? '' : (
149 <Text color={compactColor(compacted)} dimColor={compacted <= 3}>
150 {COMPACT_ICON} {compacted}{' '}
151 </Text>
152 )}
153 {shown.map((g, i) => {
154 const left = until(g.resetsAt, now)
155
156 return (
157 <Text key={g.kind}>
158 {i > 0 ? ' ' : ''}
159 <Text dimColor>{g.label} </Text>
160 {isNarrow ? '' : <Text color={colorFor(g.percentUsed)}>{bar(g.percentUsed)} </Text>}
161 <Text color={colorFor(g.percentUsed)}>{Math.round(g.percentUsed)}%</Text>
162 {left ? <Text dimColor> · {left}</Text> : ''}
163 </Text>
164 )
165 })}
166 {context === null ? '' : (
167 <Text>
168 {shown.length > 0 ? ' ' : ''}
169 {(contextPercent(context) ?? 0) < 50
170 ? <Text dimColor>{contextLabel(context)}</Text>
171 : <Text color={colorFor(contextPercent(context) ?? 0)}>{contextLabel(context)}</Text>}
172 </Text>
173 )}
174 </Box>
175 )
176 })
177}
178types/index.d.ts 9 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Fill = { tokens: number | null; window: number; percent: number | null; compact: number | null }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'usage-gauge': { limits: Limit[]; fill: Fill | null; model: string | null; compactions: number }
7 }
8}
9