SLOPSHOPPER

context-label

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

newspinner
A shopper browsing a rack in a slop shop
README

prompt-gauges

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.

limit-band above the prompt and context-label in the footer, in the desktop app

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.

Install with an AI agent

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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" in enabledPlugins, with the user's OK.
  5. 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).
  6. 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.

context-label · v0.6.0

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
  • The bar is four squares over the whole window. A square fills once you are within 5 points of it: at 20%, 45%, 70% and 95%.
  • The color of the filled squares says how much you should care:
ColorFrom (defaults)Why
green0
yellowhalf the window (500k of 1M), or 100k on Haikueach 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)
orange72% of the way to auto-compact (about 700k)getting close
red93% 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.

  • The text is the same figures as the app's own context popover, updated after every response. A fresh or just-compacted session shows nothing until its first response.

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.

limit-band · v0.7.0

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:

  • projected = used % ÷ the share of the window gone by;
  • green while on pace, orange from a projected 100%;
  • near the top the used share wins whatever the pace: orange from 60% used, red from 90% (red is kept for that; red by pace is a setting, off by default);
  • in the text, the 5-hour percent is colored by how full it is (green, orange from 60%, red from 90%) and each hint's ↑ 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 tick on the bar marks where the window's clock is: a fill past the tick is ahead of pace;
  • no projection in the first tenth of a window, when it would be noise; only the used floors apply then.

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.

Install

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.

Working on the mods

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:

  1. claude plugin validate . and claude plugin validate plugins/<mod>.
  2. claude plugin test plugins/<mod> for each mod changed, with the tests updated for the new behaviour.
  3. Bump the mod's version in its plugin.json for a change people should get, and add it to its heading here and to CHANGELOG.md.
  4. Keep this README in step: what each mod shows, its colors and thresholds, its settings.

Limits of the desktop app worth knowing:

  • The prompt footer (context-label) draws text only: an 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.
  • The band above the prompt (limit-band) shares one row with Cache Maxxer's band, with little room to spare: anything added to its text can push Cache Maxxer onto two lines. The shared row needs limit-band listed before Cache Maxxer in enabledPlugins (a reinstall appends it at the end and splits the row).
  • No 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.
  • A number that ticks (a countdown, a percentage) wants 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.

License

MIT

Source 2 files
hooks/register.tsx 158 lines
1import { 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}
158
types/index.d.ts 13 lines
1export 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