SLOPSHOPPER

context-meter

Context-window usage at the bottom right of Claude Code, next to the sub-meter bar: tokens used, a bar, and a color that says when to /compact.

newspinnertoasttimer
A shopper browsing a rack in a slop shop
README

context-mod

A Claude Code mod (plugin) that shows how much of the context window the session has used, right next to the sub-meter usage bar in the prompt footer.

Status: early version. Needs Claude Code 2.1.306 or later. The plugin lives in context-meter/; this repository is also its marketplace.

context-meter at 14%, 56% and 78% of a 1M window: the bar is orange, then yellow, then red

Rendered with the plugin's own formatting code, at the default thresholds and a 1M window; not a screenshot of a session. Colors approximate the terminal theme.

What it shows

ctx ━────────┃ 142k/1M 14%
  • A ten-cell bar, filled in proportion to the context window, then the tokens used against the window and the percent.
  • The bar is yellow from 500K tokens and red from 700K (both configurable). On a model whose window is no larger than the red threshold, the same share of the window is used instead, so the color can still change.
  • ┃ marks the cell where auto-compact will run. It is hidden when auto-compact is off, and it follows /autocompact.
  • A toast suggests /compact the first time the context enters red. It is raised once per stretch in red, and again after the context drops below yellow, after a compaction, or after /clear. The toast text is in Spanish by default, and can be switched to English with toastLanguage.
  • ctx – is shown while there is no figure yet, for example in a new session and right after a compaction, until the next response.
  • Where the footer is short, the meter gives up its bar, then the window, then the percent, and finally hides, so it never pushes the mode labels or other plugins out. With sub-meter enabled in your settings, the meter also keeps free the room for the widest sub-meter bar, so ctx is the one that shrinks first on a narrow terminal, whichever of the two plugins is drawn first.

context-meter is a separate plugin and does not modify sub-meter.

Install

In a Claude Code session in the terminal:

/plugin install context-meter --marketplace pablosalasd2004/context-mod

From a shell:

claude plugin install context-meter --marketplace pablosalasd2004/context-mod

For the desktop app, run the shell command above once. It installs the plugin at user scope, and the desktop app's local sessions read the same settings as the terminal, so the plugin is there too. To manage it from the app, click + next to the prompt box and open the plugin browser (Add plugins, or Plugins then Browse plugins). Plugins are not available in WSL sessions of the desktop app.

Install from a local copy

To run the plugin from a folder on your machine (for development), register the folder as a marketplace from its parent directory, then install from it:

git clone https://github.com/pablosalasd2004/context-mod
claude plugin marketplace add ./context-mod
claude plugin install context-meter@context-mod

Run these from the folder that holds the clone, and use the name of your own folder after ./ if it is not context-mod.

claude plugin marketplace add . is rejected: the source must be owner/repo, a URL, or a path that starts with ./. The marketplace then reads the plugin from the folder itself, so edits take effect after /reload-plugins, without a new version. Do not load the same folder with --plugin-dir at the same time: the hooks would run twice.

Configure

Set the options with /plugin configure context-meter@context-mod in a session. The plugin reads them when it loads, so restart Claude Code after changing one.

OptionDefaultWhat it does
warnTokensK500Thousands of tokens at which the bar turns yellow.
criticalTokensK700Thousands of tokens at which the bar turns red and the toast is raised.
showToastonRaise the toast when the context enters red.
toastLanguageesLanguage of the toast: es (Spanish) or en (English).
showMarkeronMark the auto-compact cell on the bar.

A value that is not a number of at least 1 takes its default, and a red threshold at or below the yellow one sends both thresholds back to the defaults.

Things to know

  • Where it works. The meter is drawn in the prompt footer of the terminal and of the desktop app. The VS Code extension has no such footer, so only the toast shows there, and plugins do not load in WSL sessions of the desktop app. The desktop app carries its own copy of Claude Code: check its version with /status.
  • Where the figures come from. The meter reads the engine's own context figures ($.session.usage() and session.measure). It never counts tokens itself, so its numbers are the ones the engine reports.
  • Refresh. While a main-conversation turn runs, the meter asks the engine every two seconds and redraws only when the figure changes. Subagent turns do not trigger a refresh, and nothing is polled while the session is idle.
  • Auto-compact point. Read from the engine's breakdown when the session starts, after a window change, and after /autocompact or the auto-compact switch in /config. A changed threshold made some other way shows up after the next /clear, /resume or /reload-plugins.
  • After a compaction. The engine keeps reporting the figure from before the compaction until the next response. The meter ignores that figure and shows ctx – instead, so the number never goes back up by itself.
  • sub-meter detection. The meter reads enabledPlugins from your settings when the session starts and after /clear, /resume and a fork. A plugin that you enable or disable in the middle of a session is picked up after /reload-plugins. The detection looks for the name sub-meter in enabledPlugins, so it is not sure to work with a copy loaded from a folder rather than installed.
  • Narrow terminals. The room reserved for sub-meter is a fixed 29 columns, its widest form. If sub-meter changes its layout, that number needs updating.
  • Hint width. The meter also reserves room for the hint line after the mode label. That width is held in the module, so a reload starts it at zero until the hint is drawn again.
  • Trust. A mod is code that runs with your permissions and can read your files and settings. Read the source before you install it. This one reads the engine's context figures and the enabledPlugins setting, draws in the footer and raises a toast; it starts no process and uses no network or file access. claude plugin validate ./context-meter lists every call it makes on its calls: line.

context-meter is verified in a terminal session and through the test kit on the desktop surface. It has not been looked at in the desktop app.

Development

The plugin has no build step and no runtime dependencies. Commands run from the root of the repository; the package manager is pnpm.

claude plugin validate ./context-meter --strict
claude plugin validate .
claude plugin test ./context-meter
pnpm dlx --package typescript tsc -p ./context-meter

tsc -p needs the types the engine writes into context-meter/.claude-plugin/types/ when it loads the plugin, so load it once first. To load the folder for a session with hot reload, from a terminal:

claude --plugin-dir ./context-meter

The maintainer's own notes (verification order, conventions and the third-party development skills) are in CLAUDE.md.

License

MIT. Parts of the footer measurement are adapted from sub-meter (MIT, Miguel Ángel Durán); its notice is in context-meter/LICENSE, which also ships with the plugin.

Source 3 files
hooks/register.tsx 233 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register } from 'claude-code'
3
4import type { Reading } from '../types'
5import {
6  AROUND, SEPARATOR, SUB_METER_WIDTH, barRuns, detailFor, filledCells, formatTokens, holdsPlugin, isBlank, isPluginEnabled, levelFor, markerCell,
7  roomOf, thresholdsFor, toastText, treeWidth, widthOf
8} from './meter'
9
10const reading = atom({ plugin: 'context-meter', key: 'reading' } as const, null as Reading | null)
11const compactAt = atom({ plugin: 'context-meter', key: 'compactAt' } as const, null as number | null)
12const stale = atom({ plugin: 'context-meter', key: 'stale' } as const, null as number | null)
13const isWorking = atom({ plugin: 'context-meter', key: 'isWorking' } as const, false)
14const isAlerted = atom({ plugin: 'context-meter', key: 'isAlerted' } as const, false)
15const isSubMeterOn = atom({ plugin: 'context-meter', key: 'isSubMeterOn' } as const, false)
16
17// While a turn runs the engine's figure moves with each response, but it is only announced when the turn ends.
18const REFRESH_MS = 2000
19
20// Columns of `ctx –`, which the meter shows while it has no figure.
21const DASH = 5
22
23// The hint line as last drawn on the terminal, in columns. A render hook cannot write the session's
24// state, so this one value is kept here; a reload loses it until the hint is drawn again.
25let hintWidth = 0
26
27/**
28 * Raises the toast the first time the context enters red. It stays quiet while the context stays red or
29 * between yellow and red, and is armed again below yellow and when there is no figure (a compaction, /clear).
30 */
31async function judge ($: EngineInterface, options: PluginOptions, tokens: number | null, window: number) {
32  const alerted = await read($, isAlerted)
33  const { warn, critical } = thresholdsFor(window, options)
34  if (tokens === null || tokens < warn) {
35    if (alerted) await update($, isAlerted, () => false)
36    return
37  }
38  if (tokens < critical || alerted) return
39  await update($, isAlerted, () => true)
40  if (options.showToast === false) return
41  const percent = Math.round((tokens / window) * 100)
42  await $.ui.toast(toastText(options, tokens, percent))
43}
44
45/**
46 * Keeps a context figure from the engine for the footer. After a compaction the engine goes on
47 * reporting the figure from before it, so that one is not kept; the first different figure is.
48 * A window that is not a positive size gives nothing to measure against, so it is no figure at all.
49 */
50async function take ($: EngineInterface, options: PluginOptions, context: { tokens?: number; window: number }) {
51  if (!(context.window > 0)) return
52  const old = await read($, stale)
53  if (old !== null) {
54    if (context.tokens === old) return
55    await update($, stale, () => null)
56  }
57  const tokens = context.tokens ?? null
58  const current = await read($, reading)
59  if (current !== null && current.tokens === tokens && current.window === context.window) return
60  await update($, reading, () => ({ tokens, window: context.window }))
61  await judge($, options, tokens, context.window)
62}
63
64/**
65 * Reads the context and the auto-compact point from the engine; where the engine cannot say, the meter
66 * keeps what it had. The breakdown is the costly part, so this runs at the start and when the point may
67 * have moved, not at each measurement.
68 */
69async function seed ($: EngineInterface, options: PluginOptions) {
70  try {
71    const { context } = await $.session.usage({ breakdown: 'summary' })
72    await take($, options, context)
73    await update($, compactAt, () => context.breakdown?.autoCompactThreshold ?? null)
74  } catch {
75    // no reading this time
76  }
77}
78
79/** Notes whether sub-meter is enabled; where the settings cannot be read, the meter keeps what it knew. */
80async function noteSubMeter ($: EngineInterface) {
81  try {
82    const enabled = await $.settings.read({ key: 'enabledPlugins' })
83    await update($, isSubMeterOn, () => isPluginEnabled(enabled, 'sub-meter'))
84  } catch {
85    // the footer is shared as before
86  }
87}
88
89/** Asks the engine for the figure again, but only while a turn of the main conversation runs. */
90async function tick ($: EngineInterface, options: PluginOptions) {
91  try {
92    if (!(await read($, isWorking))) return
93    const { context } = await $.session.usage()
94    await take($, options, context)
95  } catch {
96    // the next tick tries again
97  }
98}
99
100/** A compaction emptied the context: show no figure until a response reports the new one. */
101async function afterCompaction ($: EngineInterface, options: PluginOptions) {
102  try {
103    const { context } = await $.session.usage()
104    await update($, stale, () => context.tokens ?? null)
105    await update($, reading, current => (current === null ? null : { ...current, tokens: null }))
106    await update($, isAlerted, () => false)
107  } catch {
108    // the next measurement sets it right
109  }
110  await seed($, options)
111}
112
113export const register: Register = (on, options) => {
114  // Timers stop when the module reloads, so this is where one starts.
115  on('session.start', async ($, e, next) => {
116    await noteSubMeter($)
117    await seed($, options)
118    $.clock.every(REFRESH_MS, () => {
119      void tick($, options)
120    })
121    return next(e)
122  })
123
124  on('turn.start', async ($, e, next) => {
125    if (e.agentId === undefined) await update($, isWorking, () => true)
126    return next(e)
127  })
128
129  on('turn.complete', async ($, e, next) => {
130    if (e.agentId === undefined) await update($, isWorking, () => false)
131    return next(e)
132  })
133
134  // /clear, /resume and a fork reset the state without a new session.start. A compaction also raises this
135  // with source `compact`, while the engine still reports the figure from before it.
136  on('classic.SessionStart', async ($, e, next) => {
137    if (e.source !== 'compact') {
138      await noteSubMeter($)
139      await seed($, options)
140    }
141    return next(e)
142  })
143
144  on('session.compact', async ($, e, next) => {
145    const result = await next(e)
146    if (e.trigger !== 'precompute' && e.agentId === undefined && result.skip === undefined) await afterCompaction($, options)
147    return result
148  }).catch(($, e, next) => next(e))
149
150  on('session.measure', async ($, e, next) => {
151    const before = await read($, reading)
152    await take($, options, e.context)
153    if (before === null || before.window !== e.context.window) await seed($, options)
154    return next(e)
155  })
156
157  // The auto-compact point moves with /autocompact and with the /config toggle.
158  on('command.run', { command: 'autocompact' }, async ($, e, next) => {
159    const result = await next(e)
160    await seed($, options)
161    return result
162  }).catch(($, e, next) => next(e))
163
164  on('config.set', { key: 'autoCompact' }, async ($, e, next) => {
165    const result = await next(e)
166    await seed($, options)
167    return result
168  }).catch(($, e, next) => next(e))
169
170  // The hint after the mode label shares the row with the meter, so its width tells how much room is left.
171  // Adapted from sub-meter, MIT, Copyright (c) 2026 Miguel Ángel Durán (midudev); the notice is in ../LICENSE.
172  on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
173    if (e.surface === 'terminal' && widthOf(e.props.hint) !== hintWidth) {
174      hintWidth = widthOf(e.props.hint)
175      $.ui.invalidate('ui.render')
176    }
177    return next(e)
178  })
179
180  // The meter, then what is beneath it (the engine's mode labels, another plugin's switch).
181  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
182    const current = await read($, reading)
183    const point = await read($, compactAt)
184    const beneath = await next(e)
185    const { Box, Text } = $.ui.resolve(e)
186
187    // What is beneath is drawn after the meter, and the row has only so much room for both.
188    const hasModes = e.props.modes.length > 0
189    const hasBeneath = hasModes || !isBlank(beneath)
190    const below = (hasModes ? widthOf(e.props.modes.join(' & ')) : 0) + treeWidth(beneath)
191    // A plugin beneath is measured here and gives up room itself; with none, one above may wrap the meter.
192    const above = (await read($, isSubMeterOn)) ? SUB_METER_WIDTH + SEPARATOR.length : AROUND
193    const room =
194      roomOf(e.surface, e.viewport?.columns, hintWidth) - (hasBeneath ? below + SEPARATOR.length : 0) - (holdsPlugin(beneath) ? 0 : above)
195    const separator = hasBeneath ? <Text dimColor>{SEPARATOR}</Text> : null
196
197    if (current === null || current.tokens === null) {
198      if (room < DASH) return beneath
199      return (
200        <Box flexDirection="row">
201          <Text dimColor>ctx –</Text>
202          {separator}
203          {beneath}
204        </Box>
205      )
206    }
207
208    const percent = Math.round((current.tokens / current.window) * 100)
209    const words = { tokens: formatTokens(current.tokens), window: formatTokens(current.window), percent: `${percent}%` }
210    const detail = detailFor(room, words)
211    if (detail === null) return beneath
212
213    const level = levelFor(current.tokens, thresholdsFor(current.window, options))
214    const marker = options.showMarker === false ? null : markerCell(point ?? undefined, current.window)
215    const runs = barRuns(filledCells(current.tokens, current.window), marker)
216
217    return (
218      <Box flexDirection="row">
219        <Text dimColor>ctx</Text>
220        {detail.hasBar
221          ? runs.map((run, i) => {
222              const cells = (i === 0 ? ' ' : '') + (run.isMarker ? '┃' : run.isFilled ? '━' : '─').repeat(run.cells)
223              return run.isFilled ? <Text color={level}>{cells}</Text> : <Text dimColor>{cells}</Text>
224            })
225          : null}
226        <Text>{` ${words.tokens}${detail.hasWindow ? '/' + words.window : ''}${detail.hasPercent ? ' ' + words.percent : ''}`}</Text>
227        {separator}
228        {beneath}
229      </Box>
230    )
231  })
232}
233
hooks/meter.ts 168 lines
1import type { RenderNode } from 'claude-code'
2
3import type { Level } from '../types'
4
5/** Token counts at which the meter turns yellow and red. */
6export type Thresholds = { warn: number; critical: number }
7
8/** A token count as the meter writes it: `999`, `142k`, `1M`. */
9export function formatTokens (tokens: number): string {
10  if (tokens < 1_000) return String(tokens)
11  const thousands = Math.round(tokens / 1_000)
12  if (thousands < 1_000) return `${thousands}k`
13  return `${Math.round(tokens / 100_000) / 10}M`
14}
15
16const DEFAULT_WARN_K = 500
17const DEFAULT_CRITICAL_K = 700
18// The window the thresholds are written for: where one does not fit the window, its share of this one is used.
19const REFERENCE_WINDOW = 1_000_000
20
21const isThousands = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 1
22
23/**
24 * The thresholds for a window, from the config's `warnTokensK` and `criticalTokensK`.
25 * A value that is not a number of at least 1 takes its default; a red threshold at or below the
26 * yellow one sends both to the defaults. Where the red threshold reaches the window, both keep
27 * their share of the window instead, so the color can still change.
28 */
29export function thresholdsFor (window: number, options: Readonly<Record<string, unknown>>): Thresholds {
30  const warnK = isThousands(options.warnTokensK) ? options.warnTokensK : DEFAULT_WARN_K
31  const criticalK = isThousands(options.criticalTokensK) ? options.criticalTokensK : DEFAULT_CRITICAL_K
32  const isOrdered = warnK < criticalK
33  const warn = (isOrdered ? warnK : DEFAULT_WARN_K) * 1_000
34  const critical = (isOrdered ? criticalK : DEFAULT_CRITICAL_K) * 1_000
35  if (critical < window) return { warn, critical }
36  return { warn: (warn * window) / REFERENCE_WINDOW, critical: (critical * window) / REFERENCE_WINDOW }
37}
38
39/**
40 * The toast that suggests /compact, in the language of the config's `toastLanguage`: English for `en`,
41 * Spanish for anything else, which is the default.
42 */
43export function toastText (options: Readonly<Record<string, unknown>>, tokens: number, percent: number): string {
44  const figure = `${formatTokens(tokens)} (${percent}%)`
45  return options.toastLanguage === 'en' ? `Context at ${figure}: consider running /compact` : `Contexto en ${figure}: conviene hacer /compact`
46}
47
48/** How many cells wide the bar is. */
49export const CELLS = 10
50
51/** How many cells of the bar the context fills. */
52export function filledCells (tokens: number, window: number): number {
53  if (!(window > 0)) return 0
54  return Math.round(Math.min(Math.max(tokens / window, 0), 1) * CELLS)
55}
56
57/** The cell (1 to CELLS) where auto-compact will run, or null with auto-compact off. */
58export function markerCell (threshold: number | undefined, window: number): number | null {
59  if (threshold === undefined || !(window > 0)) return null
60  return Math.min(Math.max(Math.round((threshold / window) * CELLS), 1), CELLS)
61}
62
63/** A stretch of the bar drawn alike: `isMarker` runs are the single cell auto-compact is marked in. */
64export type BarRun = { cells: number; isFilled: boolean; isMarker: boolean }
65
66/** The bar as runs of alike cells, left to right; `marker` is the 1-based cell to mark, or null. */
67export function barRuns (filled: number, marker: number | null): BarRun[] {
68  const runs: BarRun[] = []
69  for (let cell = 1; cell <= CELLS; cell++) {
70    const isFilled = cell <= filled
71    const isMarker = cell === marker
72    const last = runs.at(-1)
73    if (last !== undefined && !isMarker && !last.isMarker && last.isFilled === isFilled) last.cells += 1
74    else runs.push({ cells: 1, isFilled, isMarker })
75  }
76  return runs
77}
78
79// The footer measurements below (and `isBlank`, `widthOf`, `holdsPlugin`, `roomOf`) follow the ones in
80// sub-meter, MIT, Copyright (c) 2026 Miguel Ángel Durán (midudev); the notice is in ../LICENSE.
81// The terminal footer's row: two columns of padding at each end, the permission mode label at the left
82// (20 columns for the longest) and at least two columns between that and what is drawn at the right.
83const FOOTER_PADDING = 4
84const FOOTER_GAP = 2
85const MODE_LABEL = 20
86
87/** Separates the meter from what is drawn after it. */
88export const SEPARATOR = ' · '
89
90// Kept free, when no other plugin is beneath, for one that may wrap the meter and draw after it.
91export const AROUND = 1 + SEPARATOR.length
92
93// The most columns sub-meter draws: `week`, its ten-cell bar, ` 100%` and ` ↻ 6d 23h`. When sub-meter is
94// enabled and nothing is beneath the meter, sub-meter may be wrapping it, and it measures the meter and
95// shrinks itself to fit, so the meter keeps this much (and a separator) free for it instead.
96export const SUB_METER_WIDTH = 29
97
98/** Whether `enabledPlugins` (settings.json's map of `name@marketplace`) has `name` on in some marketplace. */
99export function isPluginEnabled (enabledPlugins: unknown, name: string): boolean {
100  if (typeof enabledPlugins !== 'object' || enabledPlugins === null || Array.isArray(enabledPlugins)) return false
101  return Object.entries(enabledPlugins).some(([id, isOn]) => isOn === true && id.startsWith(`${name}@`))
102}
103
104/**
105 * Columns a text takes on one row: one per character, none for a variation selector (U+FE0E, U+FE0F).
106 * Wide characters (CJK, most emoji) are not counted double, which suits the footer's own labels.
107 */
108export function widthOf (text: string): number {
109  return [...text.replace(/[\uFE0E\uFE0F]/g, '')].length
110}
111
112/**
113 * Whether a drawing shows nothing: no text and no control. The engine's own node is no drawing of
114 * its own here, it stands for the mode labels, which are told apart by the footer's `modes`.
115 */
116export function isBlank (node: RenderNode | undefined): boolean {
117  if (node === undefined || node === null) return true
118  if (typeof node === 'string') return node.trim() === ''
119  if (node.type === 'engine') return true
120  if (node.type !== 'Box' && node.type !== 'Text') return false
121  return (node.children ?? []).every(isBlank)
122}
123
124/** Columns the text of a drawing takes on one row, not counting the engine's own node. */
125export function treeWidth (node: RenderNode | undefined): number {
126  if (node === undefined || node === null || (typeof node !== 'string' && node.type === 'engine')) return 0
127  if (typeof node === 'string') return widthOf(node)
128  return (node.children ?? []).reduce((sum, child) => sum + treeWidth(child), 0)
129}
130
131/** Whether a drawing holds another plugin's: the engine marks the root of each hook's tree with its plugin. */
132export function holdsPlugin (node: RenderNode | undefined): boolean {
133  if (node === undefined || node === null || typeof node === 'string' || node.type === 'engine') return false
134  return 'part' in node || (node.children ?? []).some(holdsPlugin)
135}
136
137/**
138 * Columns the footer leaves at its right end, beside the mode label and the hint after it: all of
139 * them off the terminal or where no width was measured.
140 */
141export function roomOf (surface: string, columns: number | undefined, hintWidth: number): number {
142  if (surface !== 'terminal' || columns === undefined) return Infinity
143  return columns - FOOTER_PADDING - FOOTER_GAP - MODE_LABEL - (hintWidth > 0 ? 1 + hintWidth : 0)
144}
145
146/** What the meter still draws: the bar, the window beside the tokens, the percent. */
147export type Detail = { hasBar: boolean; hasWindow: boolean; hasPercent: boolean }
148
149/** The most the meter can draw in `room` columns, or null when not even the tokens fit. */
150export function detailFor (room: number, words: { tokens: string; window: string; percent: string }): Detail | null {
151  const label = 'ctx'.length
152  const tokens = 1 + words.tokens.length
153  const window = 1 + words.window.length
154  const percent = 1 + words.percent.length
155  const fit = [
156    { hasBar: true, hasWindow: true, hasPercent: true, width: label + 1 + CELLS + tokens + window + percent },
157    { hasBar: false, hasWindow: true, hasPercent: true, width: label + tokens + window + percent },
158    { hasBar: false, hasWindow: false, hasPercent: true, width: label + tokens + percent },
159    { hasBar: false, hasWindow: false, hasPercent: false, width: label + tokens }
160  ].find(({ width }) => width <= room)
161  return fit === undefined ? null : { hasBar: fit.hasBar, hasWindow: fit.hasWindow, hasPercent: fit.hasPercent }
162}
163
164/** The color for a token count: `claude` is the normal one. */
165export function levelFor (tokens: number, thresholds: Thresholds): Level {
166  return tokens >= thresholds.critical ? 'error' : tokens >= thresholds.warn ? 'warning' : 'claude'
167}
168
types/index.d.ts 24 lines
1/** The meter's color, as the theme names it: normal, past the yellow threshold, past the red one. */
2export type Level = 'claude' | 'warning' | 'error'
3
4/** The last context figure: input tokens of the last response (null until one reports) against the window. */
5export type Reading = { tokens: number | null; window: number }
6
7declare module 'claude-code' {
8  interface PluginState {
9    'context-meter': {
10      reading: Reading | null
11      /** Tokens at which auto-compact runs; null when it is off or not known yet. */
12      compactAt: number | null
13      /** The figure the engine keeps reporting after a compaction, until a response reports a new one. */
14      stale: number | null
15      /** Whether a turn of the main conversation is running: the meter polls the engine only then. */
16      isWorking: boolean
17      /** Whether the toast already went off for this stretch in red. */
18      isAlerted: boolean
19      /** Whether sub-meter is enabled in the settings, and so may share the footer row with the meter. */
20      isSubMeterOn: boolean
21    }
22  }
23}
24