SLOPSHOPPER

context-meter

Side-pane widget showing Claude Code context size, a fill bar, status and session cost, with a nudge to /compact or /clear

newpanecommandtoast
v0.1.0MITupdated 2026-10-10kk5190/context-meter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-meter
│ ┃ Context ✕ › fix the failing auth test and add an audit log call │ ┃ 49% │ ┃ ███████████████████████████░░░░░░░░░░░░░░░░ ⏺ Read(src/auth.ts) │ ┃ 97k of 200k tokens ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ Status Healthy ⎿ Added 2 lines, removed 1 line │ ┃ Re-sent each turn 97k ⏺ Bash(bun test) │ ┃ Session cost $0.42 ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /context-meter │ ⎿ context-meter: Context meter opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Context
49% ███████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░ 97k of 200k tokens Status Healthy Re-sent each turn 97k Session cost $0.42
README

context-meter has moved

This mod now lives in kk5190/claude-code-mods, alongside the other mods.

Install it from there:

/plugin install context-meter --marketplace kk5190/claude-code-mods

This repository is archived and no longer updated.

Source 2 files
hooks/register.tsx 99 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Reading } from '../types'
5
6// Every turn re-sends the whole context, so absolute size drives cost more than percent of the window.
7const WARN_TOKENS = 100_000
8const ALERT_TOKENS = 200_000
9
10const PANE = 'context-meter'
11const TITLE = 'Context'
12
13const reading = atom({ plugin: 'context-meter', key: 'reading' } as const, null)
14const hasWarned = atom({ plugin: 'context-meter', key: 'hasWarned' } as const, false)
15
16const k = (n: number) => (n >= 1_000_000 ? `${+(n / 1_000_000).toFixed(1)}M` : `${Math.round(n / 1000)}k`)
17
18const level = (tokens: number) =>
19  tokens >= ALERT_TOKENS
20    ? ({ color: 'error', status: 'Compact now' } as const)
21    : tokens >= WARN_TOKENS
22      ? ({ color: 'warning', status: 'Growing' } as const)
23      : ({ color: 'success', status: 'Healthy' } as const)
24
25export const register: Register = on => {
26  on('session.start', async ($, e, next) => {
27    await $.command.register({ name: 'context-meter', description: 'Show the context meter pane' })
28    void $.ui.open({ id: PANE, title: TITLE })
29
30    return next(e)
31  })
32
33  on('command.run', { command: 'context-meter' }, async $ => {
34    await $.ui.open({ id: PANE, title: TITLE })
35
36    return { text: 'Context meter opened.' }
37  })
38
39  on('session.measure', async ($, e, next) => {
40    const { tokens, window, percent } = e.context
41    const latest: Reading | null =
42      tokens === undefined ? null : { tokens, window, percent: percent ?? 0, usd: e.cost?.usd }
43    await update($, reading, () => latest)
44
45    // Toast once per crossing; compacting or clearing below the line re-arms it.
46    const isOver = latest !== null && latest.tokens >= ALERT_TOKENS
47    if (isOver && !(await read($, hasWarned))) {
48      $.ui.toast(`Context is ${k(latest.tokens)} and re-sent every turn: consider /compact or /clear`)
49    }
50    await update($, hasWarned, () => isOver)
51
52    return next(e)
53  })
54
55  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
56    const { Box, Text } = $.ui.resolve(e)
57    const r = await read($, reading)
58
59    if (r === null) {
60      return (
61        <Box paddingX={1}>
62          <Text dimColor>Waiting for the first response…</Text>
63        </Box>
64      )
65    }
66
67    const { color, status } = level(r.tokens)
68    const cells = Math.max(10, e.props.bodyColumns - 2)
69    const filled = Math.min(cells, Math.ceil((r.tokens / r.window) * cells))
70    const row = (label: string, value: string, valueColor?: typeof color) => (
71      <Box flexDirection="row" justifyContent="space-between">
72        <Text dimColor>{label}</Text>
73        <Text color={valueColor} bold={valueColor !== undefined}>{value}</Text>
74      </Box>
75    )
76
77    return (
78      <Box flexDirection="column" paddingX={1}>
79        <Text bold color={color}>{`${r.percent}%`}</Text>
80        <Box flexDirection="row">
81          <Text color={color}>{'█'.repeat(filled)}</Text>
82          <Text dimColor>{'░'.repeat(cells - filled)}</Text>
83        </Box>
84        <Text dimColor>{`${k(r.tokens)} of ${k(r.window)} tokens`}</Text>
85        <Box marginTop={1} flexDirection="column">
86          {row('Status', status, color)}
87          {row('Re-sent each turn', k(r.tokens))}
88          {r.usd !== undefined && row('Session cost', `$${r.usd.toFixed(2)}`)}
89        </Box>
90        {r.tokens >= ALERT_TOKENS && (
91          <Box marginTop={1}>
92            <Text color="error">Run /compact or /clear</Text>
93          </Box>
94        )}
95      </Box>
96    )
97  })
98}
99
types/index.d.ts 8 lines
1export type Reading = { tokens: number; window: number; percent: number; usd?: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-meter': { reading: Reading | null; hasWarned: boolean }
6  }
7}
8