SLOPSHOPPER

context-meter

Context fill in one row above the prompt, with a switchable auto-compact threshold and Compact and Clear buttons.

bandtoasttimer
★ 3v0.2.4MITupdated 2026-10-24T-AI-Labs/claude-code-mods/context-meter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-meter
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM Context ▮▮▯▯▯ ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Context ▮▮▯▯▯ ⟨Claude Code's own drawing⟩
README

<h1 align="center">Claude Code Mods</h1>

<a href="https://github.com/T-AI-Labs/claude-code-mods/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/T-AI-Labs/claude-code-mods/ci.yml?branch=main&style=flat-square&label=ci" alt="CI"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fcontext-usage%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=context-usage&style=flat-square" alt="context-usage version"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fcache-meter%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=cache-meter&style=flat-square" alt="cache-meter version"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fcatalog-pane%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=catalog-pane&style=flat-square" alt="catalog-pane version"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fcontext-meter%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=context-meter&style=flat-square" alt="context-meter version"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fusage-limits%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=usage-limits&style=flat-square" alt="usage-limits version"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/T-AI-Labs/claude-code-mods?style=flat-square" alt="License"></a>

Installation

Add the marketplace once, then install the mods you want with the first command in their row of the Mods table. To update, run the second lines: the marketplace first, then each mod. The commands work in any shell, or inside Claude Code with /plugin in place of claude plugin. New sessions load the changes.

claude plugin marketplace add T-AI-Labs/claude-code-mods
claude plugin marketplace update T-AI-Labs

The mods use Claude Code's function-hook plugin API, which is in early access and may change between releases. They are built and tested against Claude Code 2.1.295, the engine in the Claude desktop app 2.31226.1.

Mods

context-usage already contains context-meter and usage-limits: install either it or those two, not both.

<table> <tr><th>Mod</th><th>Description</th><th width="35%">Preview</th></tr> <tr><td><a href="#context-usage">context&#8209;usage</a></td><td>

Start here. The context fill with the auto-compact threshold, Compact and Clear, and the 5-hour and weekly limits beneath it.

claude plugin install context-usage@T-AI-Labs
claude plugin update context-usage@T-AI-Labs

</td><td><img src=".docs/context-usage.svg" alt="The context-usage rows" width="320"></td></tr> <tr><td><a href="#context-meter">context&#8209;meter</a></td><td>

The context row of context-usage on its own.

claude plugin install context-meter@T-AI-Labs
claude plugin update context-meter@T-AI-Labs

</td><td><img src=".docs/context-meter.svg" alt="The context-meter row" width="320"></td></tr> <tr><td><a href="#usage-limits">usage&#8209;limits</a></td><td>

The usage row of context-usage on its own.

claude plugin install usage-limits@T-AI-Labs
claude plugin update usage-limits@T-AI-Labs

</td><td><img src=".docs/usage-limits.svg" alt="The usage-limits row" width="320"></td></tr> <tr><td><a href="#cache-meter">cache&#8209;meter</a></td><td>

How much of each turn the prompt cache served and how long it stays warm, with a switch that keeps it warm while you are away.

claude plugin install cache-meter@T-AI-Labs
claude plugin update cache-meter@T-AI-Labs

</td><td><img src=".docs/cache-meter.svg" alt="The cache-meter row" width="320"></td></tr> <tr><td><a href="#catalog-pane">catalog&#8209;pane</a></td><td>

A pane listing every skill, command, agent, mod, plugin, settings hook, MCP server and tool, with usage, token cost and buttons to use or manage each one.

claude plugin install catalog-pane@T-AI-Labs
claude plugin update catalog-pane@T-AI-Labs

</td><td><img src=".docs/catalog-pane.svg" alt="The catalog pane" width="320"></td></tr> </table>

context-usage

context-meter and usage-limits in one mod: the context row and the usage row beneath it. Both work as described under context-meter and usage-limits. To switch from the two separate mods, install context-usage, uninstall context-meter and usage-limits, and pick the threshold again.

Usage

The rows sit in the band above the prompt and cost no tokens: they only read figures Claude Code already has. In the terminal, ctrl+x tab moves into the band: Tab walks the controls, Enter presses a button, the arrow keys pick a threshold. ctrl+x ctrl+a or the band's [-] mark collapses it.

Options

OptionValuesDefaultEffect
displaygauge, textgaugegauge draws a five-segment gauge before each percentage; text leaves it out.
detailstrue, falsetrueShows the figures in parentheses.
usagePositionbelow, abovebelowPuts the usage row under or on top of the context row.
'{"display":"text","usagePosition":"above"}' | claude plugin configure context-usage@T-AI-Labs --values-stdin

That is PowerShell; in cmd, use echo {"display":"text"} | …. Options you leave out keep their value, the command without --values-stdin shows what is set, and a new session picks up the change. --config display=text sets an option while installing, and in a terminal session the options are rows of the /config menu.

context-meter

ElementWhat it shows
ContextFill of the context window, with tokens used and window size. Yellow within 15 points of the threshold, red at it; with Auto off, the same against 100 %.
AutoThe fill at which the mod compacts on its own: Off, 97, 90, 70, 60 or 50 %. Off leaves it to you and to Claude Code's built-in auto-compaction. Kept across sessions.
CompactRuns /compact now.
ClearRuns /clear.

Usage

The row sits in the band above the prompt and costs no tokens: it only reads figures Claude Code already has. In the desktop app it is two lines high so the app's taller buttons fit. In the terminal, ctrl+x tab moves into the band: Tab walks the controls, Enter presses a button, the arrow keys pick a threshold. ctrl+x ctrl+a or the band's [-] mark collapses it.

Until the first response of a session, and again after a clear, the fill reads 0%: the engine counts the system prompt and tools only once a response arrives.

Options

OptionValuesDefaultEffect
displaygauge, textgaugegauge draws a five-segment gauge before the percentage; text leaves it out.
detailstrue, falsetrueShows the figures in parentheses.
'{"display":"text","details":"false"}' | claude plugin configure context-meter@T-AI-Labs --values-stdin

Set them as for context-usage. The row needs about 62 columns with the defaults; on a narrower window the buttons wrap to a second line. details off saves about 11 columns, display set to text about 6:

How auto-compact works

After each turn the mod compares the fill with the threshold and runs /compact once it is reached. It fires again only after the fill has dropped below the threshold, so a compaction that lands above it does not loop. Claude Code's built-in auto-compaction still runs at its own limit, so a threshold above that limit never fires.

Every compaction costs tokens, whether you press Compact or the threshold triggers it: /compact is a model request that summarizes the conversation, and the next turn starts on a cold cache. To keep usage down, leave the threshold high and compact by hand when it suits you.

usage-limits

A gauge for each rate-limit window, with the percentage used and the time until it resets: 5h for the 5-hour limit, Week for the weekly one. Yellow from 75 %, red from 90 %.

Usage

The row sits in the band above the prompt and costs no tokens. ctrl+x ctrl+a or the band's [-] mark collapses it in the terminal.

Claude Code reports the limits only on a subscription plan, with each response. Until the first one, and right after a /clear, the row shows the last reading, with a window that has reset since at 0 %. A window that never had a reading shows –.

Sessions running side by side share their readings through a file on your machine, once a minute and at no token cost, so an idle session still shows what the others used. context-usage and usage-limits keep separate stores and do not share with each other.

Options

OptionValuesDefaultEffect
displaygauge, textgaugegauge draws a five-segment gauge before each percentage; text leaves it out.
detailstrue, falsetrueShows the reset times in parentheses.
'{"display":"text"}' | claude plugin configure usage-limits@T-AI-Labs --values-stdin

Set them as for context-usage. Give the row mods the same display and details to keep their columns aligned.

cache-meter

ElementWhat it shows
CacheShare of the last turn's input tokens served from the prompt cache, with tokens read and total input. Yellow below 40 %; – until the first response and after a clear.
warm / coldHow long the cache stays valid: five minutes after the last request starts, or an hour once the session has used the hour-long cache. cold means the next turn writes the whole context again at full price, the cheapest moment to compact or clear.
Keep warmOff by default. Refreshes the cache shortly before it lapses, for up to keepWarmMinutes after the last turn, and counts the refreshes. Stops when a refresh finds the cache already gone.

Usage

The row sits in the band above the prompt and costs no tokens; only Keep warm's refreshes are model requests. Keep warm is a text link in the desktop app and a button elsewhere. In the terminal, ctrl+x tab moves into the band, Tab walks the controls and Enter presses one. ctrl+x ctrl+a or the band's [-] mark collapses it.

Before 0.4.0 cache-meter also showed the context fill, threshold, Compact and Clear. They moved to context-meter, which an update does not install: install it and pick the threshold again.

Options

OptionValuesDefaultEffect
displaygauge, textgaugegauge draws a five-segment gauge before the percentage; text leaves it out.
detailstrue, falsetrueShows the figures in parentheses.
keepWarmMinutes15, 30, 60, 12060How long after the last turn Keep warm goes on refreshing.
'{"keepWarmMinutes":"120"}' | claude plugin configure cache-meter@T-AI-Labs --values-stdin

Set them as for context-usage.

Why the hit rate matters

Every turn sends the whole conversation again. Cached tokens cost about a tenth of the input price, a twentieth on Claude Opus 5.5; writing them to the cache costs 1.25 times the input price. The cache lapses after a pause, on a model switch, and when something early in the context changes, such as CLAUDE.md or the tools.

What Keep warm costs

Each refresh reads the cached context once at the cache price, plus a one-word reply. With 200k tokens on Claude Opus 5.5 that is about $0.04, against about $1.00 for writing the context again after it went cold. At one refresh every four and a half minutes, an hour costs about half a rewrite: worth it if you come back, lost if you don't. On a subscription the refreshes count against the usage limits.

catalog-pane

A pane with one tab per category: Skills, Commands, Agents, Mods, Plugins, Hooks (from the settings files), MCP servers and built-in Tools.

Usage

/catalog opens the pane, in the desktop app and in the terminal; /catalog mods opens a category, /catalog <text> a search. Each row shows the entry's origin, how often it was used in total and in the last 30 days, and for plugin parts the tokens it adds to every session. Usage is counted from the install of the mod on. On the desktop you can filter by text and origin and sort:

SortOrder
Most used, Recently used, NameBy usage, or alphabetically.
Clean-upCostly and unused entries first.
GitHub stars, Trending, GitHub forks, Latest GitHub pushBy the plugin's repository; Trending is stars gained per month.
Installs, Newest installedBy the official directory's install count, or by install date.

Pressing an entry opens its detail: usage over 7 and 30 days, token cost, version and install dates, what a mod hooks and calls, the repository and the full description. Explain with Haiku sums up in three sentences what the entry is for. The buttons depend on the kind: insert or run a skill or command, insert an agent, MCP server or tool into the prompt, update, disable or uninstall a plugin or mod, remove a local MCP server. Uninstalling and removing ask first.

Options

OptionValuesDefaultEffect
languageany language nameEnglishThe language Explain with Haiku answers in.
githubStatstrue, falsetrueFetches the GitHub stats of each plugin's repository once a day.
'{"language":"German"}' | claude plugin configure catalog-pane@T-AI-Labs --values-stdin

Set them as for context-usage.

Network and model requests

  • GitHub: once a day, the public API for each plugin repository's stars, forks, open issues and last push. Off with githubStats.
  • Haiku: one request of at most 400 output tokens per Explain with Haiku. The answer is kept, so each entry is explained once.
  • Claude Code CLI: claude plugin details once per plugin version for its token cost, claude plugin validate once per mod version, and update, disable, enable or uninstall when you press those buttons.

Everything else comes from Claude Code and the configuration files, and is kept in the mod's own store.

License

MIT

Source 2 files
hooks/register.tsx 267 lines
1import { atom, read, update } from 'claude-code'
2import type {
3  Color,
4  Elements,
5  EngineInterface,
6  Register,
7  SessionContextUsage,
8} from 'claude-code'
9
10import type { ContextFill } from '../types'
11
12const OFF = 0
13const STEPS = [OFF, 97, 90, 70, 60, 50]
14const stepLabel = (step: number) => (step === OFF ? 'Off' : `${step}%`)
15const OPTIONS = STEPS.map(step => ({ value: String(step), label: stepLabel(step) }))
16const DEFAULT_THRESHOLD = 97
17const STORE_KEY = 'threshold'
18
19// Shared with cache-meter, context-meter and usage-limits so the gauges of all rows line up.
20const LABEL_WIDTH = 7
21const SEGMENTS = 5
22const PERCENT_WIDTH = 4
23const DETAIL_WIDTH = 10
24
25type Tags = Pick<Elements['terminal'], 'Box' | 'Text'>
26type Layout = { hasGauge: boolean; hasDetails: boolean }
27
28const DESKTOP_BUTTON_ROWS = 2
29
30const context = atom({ plugin: 'context-meter', key: 'context' } as const, null)
31const threshold = atom(
32  { plugin: 'context-meter', key: 'threshold' } as const,
33  DEFAULT_THRESHOLD,
34)
35
36const toFill = (live: SessionContextUsage): ContextFill | null =>
37  live.percent === undefined || live.tokens === undefined
38    ? null
39    : { percent: live.percent, tokens: live.tokens, window: live.window }
40
41const restore = async ($: EngineInterface) => {
42  const stored = await $.store.get(STORE_KEY)
43
44  if (typeof stored === 'number' && STEPS.includes(stored)) {
45    await update($, threshold, () => stored)
46  }
47
48  // The engine has no reading before the first response of a session, so an empty one counts as nothing sent yet.
49  const usage = await $.session.usage()
50  await update(
51    $,
52    context,
53    () => toFill(usage.context) ?? { percent: 0, tokens: 0, window: usage.context.window },
54  )
55}
56
57const tokens = (count: number) => {
58  const thousands = Math.round(count / 1000)
59
60  if (count < 1000) {
61    return String(count)
62  }
63
64  return thousands < 1000
65    ? `${thousands}k`
66    : `${Number((count / 1_000_000).toFixed(1))}M`
67}
68
69const contextTone = (percent: number, threshold: number): Color | undefined => {
70  const limit = threshold === OFF ? 100 : threshold
71
72  return percent >= limit ? 'error' : percent >= limit - 15 ? 'warning' : undefined
73}
74
75const meter = (
76  { Box, Text }: Tags,
77  layout: Layout,
78  fill: ContextFill | null,
79  limit: number,
80) => {
81  const percent = fill === null ? null : fill.percent
82  const tone = contextTone(percent ?? 0, limit)
83  const filled =
84    percent === null
85      ? 0
86      : Math.max(0, Math.min(SEGMENTS, Math.round((percent / 100) * SEGMENTS)))
87
88  return (
89    <Box flexDirection="row" flexShrink={0}>
90      <Box minWidth={LABEL_WIDTH} marginRight={1} flexShrink={0}>
91        <Text dimColor>Context</Text>
92      </Box>
93      {layout.hasGauge && (
94        <Box minWidth={SEGMENTS} marginRight={1} flexShrink={0}>
95          <Text color={tone}>{'▮'.repeat(filled)}</Text>
96          <Text dimColor>{'▯'.repeat(SEGMENTS - filled)}</Text>
97        </Box>
98      )}
99      <Box
100        minWidth={PERCENT_WIDTH}
101        marginRight={1}
102        flexShrink={0}
103        justifyContent="flex-end"
104      >
105        <Text color={percent === null ? 'inactive' : tone}>
106          {percent === null ? '–' : `${Math.round(percent)}%`}
107        </Text>
108      </Box>
109      {layout.hasDetails && (
110        <Box minWidth={DETAIL_WIDTH} marginRight={1} flexShrink={0}>
111          <Text dimColor>
112            {fill === null ? '' : `(${tokens(fill.tokens)}/${tokens(fill.window)})`}
113          </Text>
114        </Box>
115      )}
116    </Box>
117  )
118}
119
120const runCommand = ($: EngineInterface, command: string) => {
121  $.command
122    .run({ command })
123    .catch(() => $.ui.toast(`context-meter: /${command} could not run`))
124}
125
126const cycleThreshold = async ($: EngineInterface) => {
127  const value = await update(
128    $,
129    threshold,
130    held => STEPS[(STEPS.indexOf(held) + 1) % STEPS.length] ?? DEFAULT_THRESHOLD,
131  )
132  await $.store.set(STORE_KEY, value)
133}
134
135const setThreshold = async ($: EngineInterface, value: number) => {
136  if (STEPS.includes(value)) {
137    await update($, threshold, () => value)
138    await $.store.set(STORE_KEY, value)
139  }
140}
141
142export const register: Register = (on, options) => {
143  const layout: Layout = {
144    hasGauge: options.display !== 'text',
145    hasDetails: options.details !== false,
146  }
147  // Re-armed only once the fill drops below the threshold, so a compaction that lands above it does not loop.
148  let isArmed = true
149
150  on('session.start', async ($, e, next) => {
151    await restore($)
152
153    return next(e)
154  })
155
156  // A /clear wipes every plugin's state after session.end and raises no session.start, only this.
157  on('classic.SessionStart', async ($, e, next) => {
158    if (e.source === 'clear') {
159      await restore($).catch(() => undefined)
160    }
161
162    return next(e)
163  })
164
165  on('session.end', async ($, e, next) => {
166    if (e.reason === 'clear') {
167      isArmed = true
168      await update($, context, () => null)
169    }
170
171    return next(e)
172  })
173
174  on('session.measure', async ($, e, next) => {
175    if (e.changed.includes('context')) {
176      const fill = toFill(e.context)
177      const limit = await read($, threshold)
178      await update($, context, () => fill)
179
180      if (fill !== null && (limit === OFF || fill.percent < limit)) {
181        isArmed = true
182      } else if (fill !== null && isArmed) {
183        isArmed = false
184        // A compaction is refused while the turn that raised this measurement still runs.
185        $.clock.after(250, () => runCommand($, 'compact'))
186      }
187    }
188
189    return next(e)
190  })
191
192  on('session.compact', async ($, e, next) => {
193    const compacted = await next(e)
194    const isLive = e.agentId === undefined && e.trigger !== 'precompute'
195
196    if (isLive && compacted.skip === undefined) {
197      const after = compacted.tokensAfter
198      await update($, context, held =>
199        held === null || after === undefined
200          ? null
201          : {
202              tokens: after,
203              window: held.window,
204              percent: Math.round((after / held.window) * 100),
205            },
206      )
207    }
208
209    return compacted
210  }).catch(($, e, next) => next(e))
211
212  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
213    const below = await next(e)
214
215    if (e.props.hasSurvey) {
216      return below
217    }
218
219    const { Box, Button, Text } = $.ui.resolve(e)
220    const fill = await read($, context)
221    const limit = await read($, threshold)
222    let picker
223
224    // The mobile surface draws no Select.
225    if (e.surface === 'mobile') {
226      picker = (
227        <Button
228          key="threshold"
229          label={`Auto ${stepLabel(limit)}`}
230          onPress={() => void cycleThreshold($)}
231        />
232      )
233    } else {
234      const { Select } = $.ui.resolve(e)
235      picker = (
236        <Select
237          key="threshold"
238          label="Auto"
239          options={OPTIONS}
240          value={String(limit)}
241          onSelect={value => void setThreshold($, Number(value))}
242        />
243      )
244    }
245
246    return (
247      <Box flexDirection="column">
248        <Box
249          flexDirection="row"
250          flexWrap="wrap"
251          alignItems="center"
252          // The desktop's native Button is taller than one text row and spills onto the row below.
253          minHeight={e.surface === 'desktop' ? DESKTOP_BUTTON_ROWS : undefined}
254        >
255          {meter({ Box, Text }, layout, fill, limit)}
256          <Box flexDirection="row" gap={1} flexShrink={0}>
257            {picker}
258            <Button key="compact" label="Compact" onPress={() => runCommand($, 'compact')} />
259            <Button key="clear" label="Clear" onPress={() => runCommand($, 'clear')} />
260          </Box>
261        </Box>
262        {below}
263      </Box>
264    )
265  })
266}
267
types/index.d.ts 11 lines
1export type ContextFill = { percent: number; tokens: number; window: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-meter': {
6      context: ContextFill | null
7      threshold: number
8    }
9  }
10}
11