The context window's fill (percent and used/total) always shown in the prompt footer, beside the model name

Two small Claude Code mods for keeping an eye on how much room you have left: one for the context window, one for the 5-hour and 7-day usage limits. They work in the terminal and in the desktop app's Code tab. What changed in each version: CHANGELOG.md.

Above the prompt, limit-band (right) beside Cache Maxxer's band (left); in the footer, context-label beside the model name.
Cache Maxxer is not part of this repository. The prompt-cache countdown on the left of the screenshot is Cache Maxxer by Vayaan Labs, a separate mod. Install it from there if you want it too; limit-band shares the row with it when it is installed and takes the row by itself when it is not. These mods work without it.
Copy this into your coding agent (Claude Code, for example); the copy button is in the block's corner:
Install the Claude Code mods from https://github.com/pdrive/prompt-gauges. Read the "Install with an AI agent" section of its README and follow those steps, asking me before you install anything.
The steps below are written for that agent.
For the agent installing these mods. Ask the user before each step that installs anything.
- Ask which mods they want: context-label (context fill in the prompt footer), limit-band (the 5-hour and 7-day usage limits above the prompt; it shows only on a subscription with usage limits), or both.
- Ask whether they also want Cache Maxxer (the prompt-cache countdown, a separate mod by Vayaan Labs, not maintained here). Say plainly that it is someone else's mod; skip it if they say no.
- Install from a shell, in this order (limit-band before Cache Maxxer, so the two share one row): ``
claude plugin install context-label --marketplace pdrive/prompt-gauges claude plugin install limit-band --marketplace pdrive/prompt-gauges claude plugin install cache-maxxer --marketplace vayaan-labs/plugins`` Leave out the lines for what they declined.- If Cache Maxxer was already installed before limit-band, the two land on separate lines: in
~/.claude/settings.json, move"limit-band@prompt-gauges"above"cache-maxxer@vayaan-labs"inenabledPlugins, with the user's OK.- Tell the user to run
/reload-plugins(or start a new session), and that the settings are in/config(context-label.*,limit-band.*; see the sections below).- In the desktop app, where a shell is not at hand, the same is: + next to the prompt box → Plugins → Add marketplace
pdrive/prompt-gauges→ Add plugin.
The context window's fill, always visible in the prompt footer beside the model name, instead of behind a click:
■☐☐☐ ctx 23% · 228k/1M Opus 5.5 Medium
| Color | From (defaults) | Why |
|---|---|---|
| green | 0 | |
| yellow | half the window (500k of 1M), or 100k on Haiku | each turn resends the whole context; Haiku 5.5's price per token goes up 5x for prompts over 100k; the other Claude 4.6-and-later models price the full 1M window the same (pricing) |
| orange | 72% of the way to auto-compact (about 700k) | getting close |
| red | 93% of the way to auto-compact (about 900k) | auto-compact is near, which can break a chat |
Orange and red follow your session's auto-compact point (read from Claude Code, as /context shows it); with auto-compact off they follow the whole window.
Settings, with /config. The thresholds take any whole number from 0 to 100, and 0 turns a level off:
/config context-label.yellow_at=50 # % of the window
/config context-label.orange_at=72 # % of the way to auto-compact
/config context-label.red_at=93 # % of the way to auto-compact
/config context-label.haiku_price_step=true # yellow at 100k on Haiku 5.5
/config context-label.show=bar and text|bar|text
The levels are checked from red down and the first that is reached wins, so they never overlap: a lower level set above a higher one simply never shows.
A thin line above the prompt for the 5-hour usage window, with its percent and time to reset, and the 7-day window beside it:
5h ━━━━━━━━┃━━────────── 49% (2h 56m) ↑119% · 7d 31% (4d 18h)
The bar is colored by pace, not by how full it is. 49% used sounds fine, but if only 41% of the window has gone by, you are on course to hit the limit before it resets. So:
↑ by its window's pace; the numbers after it, the times and the 7-day percent stay dim;↑119% shows the projection once it reaches the orange pace setting (100% by default), for the 7-day window too; its arrow is orange, or red past the red pace setting;The percents come from the last API response; the countdowns tick on their own. It shows only on a subscription with usage limits.
Settings, with /config (the defaults are the values above). Any whole number, 0 is off; the levels are checked from red down, so they never overlap:
/config limit-band.orange_used=60 # % used, 0 to 100
/config limit-band.red_used=90 # % used, 0 to 100
/config limit-band.orange_pace=100 # % projected, 0 to 500
/config limit-band.red_pace=0 # % projected, 0 to 500 (off by default)
Pace coloring is an idea from PCritchfield/claude-gauges (MIT), whose status line and session pane color plan limits the same way; this is its own implementation for the band above the prompt.
It shares its row with Cache Maxxer's band when that mod is installed (and moves under it while Cache Maxxer's detail is open); without it, it takes the row by itself. It also gives that band fixed-width digits, so its ticking seconds don't nudge the row. The row works when limit-band is drawn around Cache Maxxer's band, which follows the order of enabledPlugins in ~/.claude/settings.json: if the two land on separate lines, list limit-band@prompt-gauges before cache-maxxer@vayaan-labs there and run /reload-plugins.
At the prompt of a Claude Code terminal session:
/plugin install context-label --marketplace pdrive/prompt-gauges
/plugin install limit-band --marketplace pdrive/prompt-gauges
Answer y to add the marketplace, then pick a scope. From a shell:
claude plugin install context-label --marketplace pdrive/prompt-gauges
claude plugin install limit-band --marketplace pdrive/prompt-gauges
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with pdrive/prompt-gauges, then Add plugin.
Each mod is a Claude Code hooks module under plugins/<mod>/: .claude-plugin/plugin.json (the manifest and its /config settings), hooks/register.tsx (the module), types/index.d.ts (its state contract and shared types) and tests/*.test.ts. .claude-plugin/marketplace.json lists them.
To run your changes, add a local clone as a marketplace and install from it; Claude Code then reads the mods from the clone, and an edit plus /reload-plugins reloads them:
claude plugin marketplace add <path to your clone>
claude plugin install context-label@prompt-gauges
claude plugin install limit-band@prompt-gauges
Before a commit:
claude plugin validate . and claude plugin validate plugins/<mod>.claude plugin test plugins/<mod> for each mod changed, with the tests updated for the new behaviour.version in its plugin.json for a change people should get, and add it to its heading here and to CHANGELOG.md.Limits of the desktop app worth knowing:
Svg is dropped, a Button loses the colors, sibling Texts are spaced apart (nest them in one Text), and the slot is cut at a fixed width. Keep it short.enabledPlugins (a reinstall appends it at the end and splits the row).isInteractive on the bar's Svg. The interactive frame a <title> tooltip needs reloads on every redraw, so the bar flashes with every countdown tick.tabular on its Text, so it doesn't change width.Keep docs, comments and commit messages about the mods themselves, and credit ideas taken from other projects here.
MIT
hooks/register.tsx 158 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register, SessionContextUsage } from 'claude-code'
3
4import type { Config, Ctx, Level } from '../types'
5
6// The context window's fill in the prompt footer's mode slot, beside the model name: a four-cell bar
7// ("■■☐☐", the filled squares colored by level, the empty ones dim), then "ctx 35% · 353k/1M".
8// Kept short: the desktop slot is cut at a fixed width. Figures come from the last API response.
9// The slot is drawn as a tree of our own: the desktop app ignores a rewritten `modes` list but draws a tree.
10//
11// Levels: green; yellow from half the window, where each turn (which resends it all) gets dear, or from
12// 100k on Haiku 5.5, whose price per token goes up 5x past it (the other Claude 4.6-and-later models price
13// the full window the same); orange and red as a share of the way to auto-compaction (72% and 93%),
14// which is what can break a chat. Each threshold is a setting.
15
16const ctxAtom = atom({ plugin: 'context-label', key: 'ctx' } as const, null)
17const thresholdAtom = atom({ plugin: 'context-label', key: 'threshold' } as const, null)
18const modelAtom = atom({ plugin: 'context-label', key: 'model' } as const, null)
19
20const BAR_CELLS = 4
21const HAIKU_YELLOW = 100_000
22
23const COLORS: Record<Level, string> = { green: '#3cc43c', yellow: '#e0c020', orange: '#f08c1e', red: '#e8453c' }
24
25const pick = <T extends string>(v: unknown, allowed: readonly T[], fallback: T): T =>
26 typeof v === 'string' && (allowed as readonly string[]).includes(v) ? (v as T) : fallback
27
28// A whole-percent setting as a share: 72 -> 0.72, 0 or "off" -> null (off); a value outside 0 to 100,
29// or no number at all, falls back to the default.
30export function toShare(v: unknown, fallback: number): number | null {
31 const n = typeof v === 'number' ? v : typeof v === 'string' ? (v.trim() === 'off' ? 0 : parseInt(v, 10)) : NaN
32 const pct = Number.isFinite(n) && n >= 0 && n <= 100 ? Math.round(n) : fallback
33 return pct === 0 ? null : pct / 100
34}
35
36export function toConfig(options: PluginOptions): Config {
37 const show = pick(options.show, ['bar and text', 'bar', 'text'], 'bar and text')
38 return {
39 yellowAt: toShare(options.yellow_at, 50),
40 haikuStep: options.haiku_price_step !== false,
41 orangeAt: toShare(options.orange_at, 72),
42 redAt: toShare(options.red_at, 93),
43 show: show === 'bar and text' ? 'both' : show,
44 }
45}
46
47// null until a response of the live window reported a fill (a fresh or just-compacted session).
48export const toCtx = (c: SessionContextUsage): Ctx | null =>
49 c.tokens === undefined ? null : { tokens: c.tokens, window: c.window, percent: c.percent ?? Math.round((100 * c.tokens) / c.window) }
50
51// 353200 -> "353k", 45300 -> "45.3k", 1000000 -> "1M": no decimal from 100k up, to stay short
52export function fmtTokens(n: number): string {
53 if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`
54 if (n >= 100_000) return `${Math.round(n / 1_000)}k`
55 if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`
56 return `${n}`
57}
58
59export const label = (c: Ctx) => `ctx ${c.percent}% · ${fmtTokens(c.tokens)}/${fmtTokens(c.window)}`
60
61// The engine's own mode labels ("focus & memory paused") first, then ours.
62export const footerText = (modes: readonly string[], c: Ctx) => [modes.join(' & '), label(c)].filter(Boolean).join(' · ')
63
64const clamp01 = (x: number) => Math.min(Math.max(x, 0), 1)
65
66// Orange and red measure the way to auto-compaction, or to the window's end with it off; yellow, the
67// window. Checked from red down, the first that holds wins: a lower level set above a higher one never
68// shows, so the levels cannot overlap.
69export function levelOf(c: Ctx, threshold: number | null, model: string | null, cfg: Config): Level {
70 const end = threshold ?? c.window
71 if (cfg.redAt !== null && c.tokens >= cfg.redAt * end) return 'red'
72 if (cfg.orangeAt !== null && c.tokens >= cfg.orangeAt * end) return 'orange'
73 const isHaiku = cfg.haikuStep && model !== null && /haiku/i.test(model)
74 const yellowAt = isHaiku ? HAIKU_YELLOW : cfg.yellowAt === null ? null : cfg.yellowAt * c.window
75 if (yellowAt !== null && c.tokens >= yellowAt) return 'yellow'
76 return 'green'
77}
78
79// A cell fills once the fill is within a fifth of a cell of its end: with four, at 20%, 45%, 70%, 95%.
80export const filledCells = (share: number) => Math.min(BAR_CELLS, Math.floor(BAR_CELLS * clamp01(share) + 0.2))
81
82// The bar spans the whole window in cells, filled ones told apart by shape as well as color.
83export function barText(c: Ctx): string {
84 const filled = filledCells(c.tokens / c.window)
85 return '■'.repeat(filled) + '☐'.repeat(BAR_CELLS - filled)
86}
87
88// The threshold moves only with the window (a model switch), so it is read then, not every response.
89let thresholdFor: number | null = null
90
91async function readThreshold($: EngineInterface, window: number) {
92 if (thresholdFor === window) return
93 thresholdFor = window
94 try {
95 const usage = await $.session.usage({ breakdown: 'summary' })
96 const b = usage.context.breakdown
97 await update($, thresholdAtom, () => (b?.isAutoCompactEnabled ? (b.autoCompactThreshold ?? null) : null))
98 } catch {
99 thresholdFor = null
100 }
101}
102
103async function readModel($: EngineInterface) {
104 try {
105 const model = await $.session.model()
106 await update($, modelAtom, () => model)
107 } catch {
108 // Keep the last one
109 }
110}
111
112export const register: Register = (on, options) => {
113 const cfg = toConfig(options)
114
115 on('session.start', async ($, e, next) => {
116 const started = await next(e)
117 try {
118 const usage = await $.session.usage()
119 await update($, ctxAtom, () => toCtx(usage.context))
120 await readThreshold($, usage.context.window)
121 await readModel($)
122 } catch {
123 // No reading yet: the label waits for the first response
124 }
125 return started
126 })
127
128 on('session.measure', async ($, e, next) => {
129 if (e.changed.includes('context')) {
130 await update($, ctxAtom, () => toCtx(e.context))
131 await readThreshold($, e.context.window)
132 await readModel($)
133 }
134 return next(e)
135 })
136
137 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
138 const c = await read($, ctxAtom)
139 if (c === null) return next(e)
140 const color = COLORS[levelOf(c, await read($, thresholdAtom), await read($, modelAtom), cfg)]
141 const bar = barText(c)
142 const { Box, Text } = $.ui.resolve(e)
143 // Text only, and short: the desktop footer drops an Svg, and cuts the slot at a fixed width
144 return (
145 <Box flexDirection="row">
146 {cfg.show !== 'text' && (
147 // One Text with the two runs nested: as sibling Texts the desktop spaces them apart
148 <Text key="bar">
149 <Text color={color}>{bar.replace(/☐/g, '')}</Text>
150 <Text dimColor>{bar.replace(/■/g, '')}</Text>
151 </Text>
152 )}
153 {cfg.show !== 'bar' && <Text key="text" dimColor mono>{`${cfg.show === 'both' ? ' ' : ''}${footerText(e.props.modes, c)}`}</Text>}
154 </Box>
155 )
156 })
157}
158types/index.d.ts 13 lines1export type Ctx = { tokens: number; window: number; percent: number }
2export type Level = 'green' | 'yellow' | 'orange' | 'red'
3// The userConfig values, read once per load
4// Shares are 0 to 1, null is off; haikuStep puts yellow at the 100k price step of Haiku 5.5
5export type Config = { yellowAt: number | null; haikuStep: boolean; orangeAt: number | null; redAt: number | null; show: 'both' | 'bar' | 'text' }
6
7declare module 'claude-code' {
8 interface PluginState {
9 // threshold: the token count auto-compaction runs at; null when it is off or not read yet
10 'context-label': { ctx: Ctx | null; threshold: number | null; model: string | null }
11 }
12}
13