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

<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>
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.
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‑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‑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‑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‑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‑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-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.
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.
| Option | Values | Default | Effect |
|---|---|---|---|
display | gauge, text | gauge | gauge draws a five-segment gauge before each percentage; text leaves it out. |
details | true, false | true | Shows the figures in parentheses. |
usagePosition | below, above | below | Puts 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.
| Element | What it shows |
|---|---|
| Context | Fill 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 %. |
| Auto | The 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. |
| Compact | Runs /compact now. |
| Clear | Runs /clear. |
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.
| Option | Values | Default | Effect |
|---|---|---|---|
display | gauge, text | gauge | gauge draws a five-segment gauge before the percentage; text leaves it out. |
details | true, false | true | Shows 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:
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.
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 %.
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.
| Option | Values | Default | Effect |
|---|---|---|---|
display | gauge, text | gauge | gauge draws a five-segment gauge before each percentage; text leaves it out. |
details | true, false | true | Shows 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.
| Element | What it shows |
|---|---|
| Cache | Share 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 / cold | How 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 warm | Off 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. |
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.
| Option | Values | Default | Effect |
|---|---|---|---|
display | gauge, text | gauge | gauge draws a five-segment gauge before the percentage; text leaves it out. |
details | true, false | true | Shows the figures in parentheses. |
keepWarmMinutes | 15, 30, 60, 120 | 60 | How 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.
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.
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.
A pane with one tab per category: Skills, Commands, Agents, Mods, Plugins, Hooks (from the settings files), MCP servers and built-in Tools.
/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:
| Sort | Order |
|---|---|
| Most used, Recently used, Name | By usage, or alphabetically. |
| Clean-up | Costly and unused entries first. |
| GitHub stars, Trending, GitHub forks, Latest GitHub push | By the plugin's repository; Trending is stars gained per month. |
| Installs, Newest installed | By 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.
| Option | Values | Default | Effect |
|---|---|---|---|
language | any language name | English | The language Explain with Haiku answers in. |
githubStats | true, false | true | Fetches 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.
githubStats.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.
hooks/register.tsx 267 lines1import { 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}
267types/index.d.ts 11 lines1export 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