Context-window fill and plan rate limits (5h / 7d) on the line under the prompt

A Claude Code mod that shows how much of your context window and plan rate limits you've used, on the line under the prompt:
⏵⏵ auto mode on (shift+tab to cycle) ctx ▰▰▰▰▱▱▱▱▱▱ 42% 84k/200k │ 5h ▰▰▱▱▱▱▱▱▱▱ 23% ↻2h14m │ 7d ▰▱▱▱▱▱▱▱▱▱ 9% ↻4d
ctx 42% │ 5h 23% │ 7d 9%.Figures update after each turn and whenever a rate-limit window moves by a full point.
/plugin marketplace add PinkOctopusLabs/claude-simple-usage
/plugin install simple-usage@claude-simple-usage
Or from a terminal:
claude plugin marketplace add PinkOctopusLabs/claude-simple-usage
claude plugin install simple-usage@claude-simple-usage
/usage-bar shows or hides it. While it's hidden, the normal hint line comes back.
session.start: registers /usage-bar and reads the session's current usage figures.session.measure: stores the latest context and rate-limit figures, and shows a one-time notice when a limit passes 90%. It only observes; the figures pass on unchanged.command.run for /usage-bar only: toggles the line on or off. It answers that one command and doesn't see or change any other command.ui.render for the hint line under the prompt: draws the hint text followed by the usage figures. While hidden, it leaves the engine's own hint line in place.It makes no network requests, reads or writes no files, and keeps its figures only for the current session.
claude --plugin-dir . # run a session with the mod loaded
claude plugin validate . # check the manifest and hooks
claude plugin test . # run tests/
simple-usage is free and provided as-is, without support.
It collects nothing and makes no network requests. See PRIVACY.md.
MIT. See LICENSE.
hooks/register.tsx 163 lines1import type { Register, SessionContextUsage, SessionCost, SessionRateLimit } from 'claude-code'
2
3import type { Usage } from '../types'
4
5const USAGE = { plugin: 'simple-usage', key: 'usage' } as const
6const IS_HIDDEN = { plugin: 'simple-usage', key: 'isHidden' } as const
7
8const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
9const WARN_AT = 90
10
11type Figures = { context: SessionContextUsage; rateLimits: SessionRateLimit[]; cost?: SessionCost }
12
13const toUsage = ({ context, rateLimits, cost }: Figures): Usage => ({
14 contextTokens: context.tokens,
15 contextWindow: context.window,
16 contextPercent: context.percent,
17 limits: rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt })),
18 costUsd: cost?.usd,
19})
20
21const colorFor = (percent: number) => (percent >= 85 ? 'error' : percent >= 60 ? 'warning' : 'success')
22
23const bar = (percent: number, width: number) => {
24 const filled = Math.round((Math.min(100, Math.max(0, percent)) / 100) * width)
25 return '▰'.repeat(filled) + '▱'.repeat(width - filled)
26}
27
28const compact = (tokens: number) =>
29 tokens >= 1_000_000 ? `${(tokens / 1_000_000).toFixed(1)}M` : `${Math.round(tokens / 1000)}k`
30
31const until = (resetsAt: string | undefined, now: number) => {
32 if (!resetsAt) return ''
33 const minutes = Math.max(0, Math.round((Date.parse(resetsAt) - now) / 60_000))
34 if (minutes >= 48 * 60) return `${Math.round(minutes / 1440)}d`
35 if (minutes >= 60) return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`
36 return `${minutes}m`
37}
38
39export const register: Register = on => {
40 // Kinds already warned about, so a toast fires once per crossing.
41 const warned = new Set<string>()
42
43 on('session.start', async ($, e, next) => {
44 await $.command.register({ name: 'usage-bar', description: 'Show or hide the usage bar under the prompt' })
45 const figures = await $.session.usage()
46 await $.state.set(USAGE, toUsage(figures))
47
48 return next(e)
49 })
50
51 on('session.measure', async ($, e, next) => {
52 await $.state.set(USAGE, toUsage(e))
53
54 for (const limit of e.rateLimits) {
55 if (limit.percentUsed >= WARN_AT && !warned.has(limit.kind)) {
56 warned.add(limit.kind)
57 $.ui.toast(`${LABELS[limit.kind] ?? limit.kind} limit at ${limit.percentUsed}%`)
58 } else if (limit.percentUsed < WARN_AT) {
59 warned.delete(limit.kind)
60 }
61 }
62
63 return next(e)
64 })
65
66 on('command.run', { command: 'usage-bar' }, async ($) => {
67 const held = await $.state.get(IS_HIDDEN)
68 const hidden = !held.value
69 await $.state.set(IS_HIDDEN, hidden)
70
71 return { text: hidden ? 'Usage bar hidden.' : 'Usage bar shown.' }
72 })
73
74 // Drawn in place of the hint line under the prompt: the engine's hint, then usage padded out
75 // to the right edge. The engine sizes this site to its content, so the padding is counted here.
76 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
77 const { value: hidden } = await $.state.get(IS_HIDDEN)
78 if (hidden) {
79 return next(e)
80 }
81
82 // Nothing stored yet (drawn before session.start, or after /reload-plugins, which skips it):
83 // read the figures directly rather than waiting for the next measurement.
84 const { value: stored } = await $.state.get(USAGE)
85 const u = stored ?? toUsage(await $.session.usage())
86
87 const { Box, Text } = $.ui.resolve(e)
88 const now = await $.clock.now()
89 const columns = e.viewport?.columns ?? 120
90 const room = columns - MODE_LABEL_RESERVE - [...e.props.hint].length - MIN_GAP
91
92 // The fullest layout that fits: bars and detail, then detail alone, then percentages alone.
93 const layouts = [
94 { width: BAR_WIDTH, isDetailed: true },
95 { width: 0, isDetailed: true },
96 { width: 0, isDetailed: false },
97 ]
98 let segments: Segment[] = []
99 for (const layout of layouts) {
100 segments = usageSegments(u, layout.width, layout.isDetailed, now)
101 if (lengthOf(segments) <= room) break
102 }
103 const gap = Math.max(MIN_GAP, room + MIN_GAP - lengthOf(segments))
104
105 return (
106 <Box flexDirection="row">
107 <Text key="hint" dimColor>
108 {e.props.hint}
109 {' '.repeat(gap)}
110 </Text>
111 {segments.map((seg, i) => (
112 <Text key={`seg-${i}`} dimColor={seg.dim} color={seg.color}>
113 {seg.text}
114 </Text>
115 ))}
116 </Box>
117 )
118 })
119}
120
121type Segment = { text: string; dim?: boolean; color?: string }
122
123const SEPARATOR = ' │ '
124const MIN_GAP = 3
125// Room for the label the engine draws left of the hint (`⏵⏵ auto mode on · `) and a right margin.
126const MODE_LABEL_RESERVE = 22
127
128const BAR_WIDTH = 10
129
130const lengthOf = (segments: Segment[]) => segments.reduce((n, seg) => n + [...seg.text].length, 0)
131
132const gauge = (percent: number, width: number): Segment => ({
133 text: `${width ? `${bar(percent, width)} ` : ''}${percent}%`,
134 color: colorFor(percent),
135})
136
137const contextSegments = (u: Usage, width: number, isDetailed: boolean): Segment[] => {
138 const percent = u.contextPercent
139 if (percent === undefined) {
140 return [{ text: `ctx —${isDetailed ? ` /${compact(u.contextWindow)}` : ''}`, dim: true }]
141 }
142 const detail =
143 isDetailed && u.contextTokens !== undefined ? ` ${compact(u.contextTokens)}/${compact(u.contextWindow)}` : ''
144 return [{ text: 'ctx ', dim: true }, gauge(percent, width), ...(detail ? [{ text: detail, dim: true }] : [])]
145}
146
147const usageSegments = (u: Usage, width: number, isDetailed: boolean, now: number): Segment[] => {
148 const items: Segment[][] = [contextSegments(u, width, isDetailed)]
149 for (const limit of u.limits) {
150 const reset = isDetailed ? until(limit.resetsAt, now) : ''
151 items.push([
152 { text: `${LABELS[limit.kind] ?? limit.kind} `, dim: true },
153 gauge(limit.percentUsed, width),
154 ...(reset ? [{ text: ` ↻${reset}`, dim: true }] : []),
155 ])
156 }
157 if (u.limits.length === 0 && u.costUsd !== undefined) {
158 items.push([{ text: `$${u.costUsd.toFixed(2)}`, dim: true }])
159 }
160 const separator = isDetailed ? SEPARATOR : ' │ '
161 return items.flatMap((item, i) => (i === 0 ? item : [{ text: separator, dim: true }, ...item]))
162}
163types/index.d.ts 16 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Usage = {
4 contextTokens?: number
5 contextWindow: number
6 contextPercent?: number
7 limits: Limit[]
8 costUsd?: number
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'simple-usage': { usage: Usage | null; isHidden: boolean }
14 }
15}
16