Context usage bar against a 150k-token budget (can exceed 100%): status line in the terminal, band above the prompt on desktop

Claude Code mod: a horizontal bar showing context usage against a 150k-token budget (can exceed 100%). In the terminal it sits on the status line; in the desktop app, which does not draw mod status lines, it is a band above the prompt.
At a Claude Code terminal prompt:
/plugin install context-meter --marketplace lrdub/claude-code-context-meter-mod
Answer y to add the marketplace, then pick the user scope.
hooks/register.tsx 51 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Tokens } from '../types'
5
6// 100% means this many tokens, whatever the model's real window; usage past it reads above 100%.
7export const BUDGET = 150_000
8
9// Cells in the bar; it fills at 100% and stays full beyond.
10export const BAR_WIDTH = 10
11
12export const formatBar = (fraction: number): string => {
13 const filled = Math.min(BAR_WIDTH, Math.max(0, Math.round(fraction * BAR_WIDTH)))
14 return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled)
15}
16
17export const formatContext = (tokens: number | undefined): string => {
18 const used = tokens ?? 0
19 const fraction = used / BUDGET
20 return `ctx: ${formatBar(fraction)} ${Math.round(fraction * 100)}% (${used}/${BUDGET} tokens)`
21}
22
23// The desktop app does not draw a mod's status line, so there the meter is a band above the prompt.
24const tokens = atom({ plugin: 'context-meter', key: 'tokens' } as const, null as Tokens)
25
26export const register: Register = on => {
27 on('session.start', ($, e, next) => {
28 $.ui.status(formatContext(undefined))
29 return next(e)
30 })
31
32 on('session.measure', async ($, e, next) => {
33 if (e.changed.includes('context')) {
34 $.ui.status(formatContext(e.context.tokens))
35 await update($, tokens, () => e.context.tokens ?? null)
36 }
37 return next(e)
38 })
39
40 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
41 if (e.surface !== 'desktop' || e.props.hasSurvey) return next(e)
42
43 const { Box, Text } = $.ui.resolve(e)
44 return (
45 <Box>
46 <Text dimColor>{formatContext((await read($, tokens)) ?? undefined)}</Text>
47 </Box>
48 )
49 })
50}
51types/index.d.ts 8 lines1export type Tokens = number | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'context-meter': { tokens: Tokens }
6 }
7}
8