Usage & context progress bars above the prompt: 5-hour limit, 7-day limit, context window

Usage and context at a glance: three compact, colored progress bars in one line above the prompt.
5h ▰▰▱▱▱▱▱▱ 23% ↻14:30 7d ▰▰▰▱▱▱▱▱ 41% ↻Mo ctx ▰▱▱▱▱▱▱▱ 6%
| Bar | Shows |
|---|---|
5h | Current 5-hour rate-limit window, with the time it resets |
7d | 7-day rate-limit window, with the weekday it resets |
ctx | How full the context window is |
Colors follow your theme: green below 50 %, yellow from 50 %, red from 80 %.
5h and 7d need a Claude subscription login; with an API key only ctx is shown.ctx shows a placeholder (▱▱▱▱▱▱▱▱ –), since nothing has been measured yet./plugin install clausage --marketplace Flo0806/fh-claude-modshooks/register.tsx 98 lines1import { atom, read, update } from 'claude-code'
2import type { Register, SessionContextUsage, SessionRateLimit, SessionUsage } from 'claude-code'
3
4import type { Bar, Usage } from '../types'
5
6const usage = atom({ plugin: 'clausage', key: 'usage' } as const, null)
7
8const toBar = (limit?: SessionRateLimit): Bar | undefined =>
9 limit && { percent: limit.percentUsed, resetsAt: limit.resetsAt }
10
11// Fall back to tokens/window when the engine reports no percentage.
12const contextBar = ({ percent, tokens, window }: SessionContextUsage): Bar | undefined => {
13 if (percent !== undefined) return { percent }
14 if (tokens !== undefined && window > 0) return { percent: (tokens / window) * 100 }
15 return undefined
16}
17
18const toUsage = ({ rateLimits, context }: Pick<SessionUsage, 'rateLimits' | 'context'>): Usage => ({
19 fiveHour: toBar(rateLimits.find((r) => r.kind === 'five_hour')),
20 sevenDay: toBar(rateLimits.find((r) => r.kind === 'seven_day')),
21 context: contextBar(context),
22})
23
24const WIDTH = 8
25
26const meter = (percent: number) => {
27 const filled = Math.round((Math.min(percent, 100) / 100) * WIDTH)
28 return '▰'.repeat(filled) + '▱'.repeat(WIDTH - filled)
29}
30
31const DAY_MS = 24 * 60 * 60 * 1000
32
33const pad = (n: number) => String(n).padStart(2, '0')
34
35// Within a day the clock time is precise enough, further out the weekday is.
36const resetLabel = (iso: string, now: number) => {
37 const at = new Date(iso)
38 return at.getTime() - now < DAY_MS
39 ? `${pad(at.getHours())}:${pad(at.getMinutes())}`
40 : at.toLocaleDateString(undefined, { weekday: 'short' })
41}
42
43const tone = (percent: number) => (percent >= 80 ? 'error' : percent >= 50 ? 'warning' : 'success')
44
45export const register: Register = (on) => {
46 // Seed right away, so the band shows after a start or reload without waiting for a turn.
47 on('session.start', async ($, e, next) => {
48 const now = toUsage(await $.session.usage())
49 await update($, usage, () => now)
50 return next(e)
51 })
52
53 on('session.measure', async ($, e, next) => {
54 const measured = toUsage(e)
55 await update($, usage, () => measured)
56 return next(e)
57 })
58
59 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
60 const current = await read($, usage)
61 if (e.props.hasSurvey || current === null) {
62 return next(e)
63 }
64
65 const { Box, Text } = $.ui.resolve(e)
66 const now = await $.clock.now()
67
68 // A missing bar draws as an empty placeholder, so the band keeps its shape.
69 const segment = (label: string, bar?: Bar) => (
70 <Box gap={1}>
71 <Text dimColor>{label}</Text>
72 {bar ? (
73 <Text color={tone(bar.percent)}>
74 {meter(bar.percent)} {Math.round(bar.percent)}%
75 </Text>
76 ) : (
77 <Text dimColor>{meter(0)} –</Text>
78 )}
79 {bar?.resetsAt && <Text dimColor>↻{resetLabel(bar.resetsAt, now)}</Text>}
80 </Box>
81 )
82
83 // Stack below other plugins' bands instead of replacing them, so ours sits next to the prompt.
84 const beneath = await next(e)
85
86 return (
87 <Box flexDirection="column">
88 {beneath}
89 <Box gap={2}>
90 {current.fiveHour && segment('5h', current.fiveHour)}
91 {current.sevenDay && segment('7d', current.sevenDay)}
92 {segment('ctx', current.context)}
93 </Box>
94 </Box>
95 )
96 })
97}
98types/index.d.ts 15 lines1export type Bar = { percent: number; resetsAt?: string }
2
3// Each bar is optional: rate limits are absent off a subscription or before the first response.
4export type Usage = {
5 fiveHour?: Bar
6 sevenDay?: Bar
7 context?: Bar
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 clausage: { usage: Usage | null }
13 }
14}
15