A speedometer above the prompt: how full the context window is, from 0 to MAX.

A speedometer above the prompt: how full Claude's context window is.
🏎️ SPEEDOMETER 0 ███ ½ █░░ MAX context 67% full
Read it like a car's dash: 0 on the left, MAX on the right. Green while there is room, yellow from 60%, red from 85%, with one toast when it redlines so you can /compact before Claude does it for you.
/speed or /speedometer prints the dial./speed hide and /speed show toggle the band.Gas Gauge's sibling on the same dash. The band draws in the terminal and in the Code tab of the Claude desktop app. It reads only the figures Claude Code already has, and sends nothing anywhere.
Nothing goes anywhere. Here is each thing, plainly.
What it sends, and where.
$.session.usage, and shows it on your screen.Which programs it runs, and why.
What it puts in the prompts it submits.
What each hook does.
session.start: adds the /speed and /speedometer commands and reads the context fill.session.measure: takes the new fill, and shows one toast when it crosses 85%.command.run for speed, and for speedometer: prints the dial, or hides or shows the band.ui.render for AbovePrompt: draws the dial above the prompt.Tested with Claude Code 2.1.287.
See every Rostech mod, live, at rostech.app.
hooks/register.tsx 93 lines1import { atom, read, update } from 'claude-code'
2import type { Register, StateDollar } from 'claude-code'
3
4// How full the context window is, 0 to 100, as the engine last measured it.
5// null until the first response reports a fill.
6const heat = atom({ plugin: 'speedometer', key: 'heat' } as const, null as number | null)
7const isHidden = atom({ plugin: 'speedometer', key: 'isHidden' } as const, false)
8
9// Gas Gauge's sibling, in a car's other dial: 0 .. ½ .. MAX.
10const CELLS = 6
11const HOT = 85
12
13function colour(pct: number): string {
14 return pct < 60 ? 'green' : pct < HOT ? 'yellow' : 'red'
15}
16
17function halves(pct: number): [string, string] {
18 const full = Math.round((pct / 100) * CELLS)
19 const cells = '█'.repeat(full) + '░'.repeat(CELLS - full)
20 return [cells.slice(0, CELLS / 2), cells.slice(CELLS / 2)]
21}
22
23function gauge(pct: number): string {
24 const [low, high] = halves(pct)
25 return `0 ${low} ½ ${high} MAX`
26}
27
28// /speed and /speedometer give the same answer. Only $.state is touched here.
29async function answer($: StateDollar, args: string) {
30 const arg = args.trim().toLowerCase()
31 if (arg === 'hide' || arg === 'show') {
32 await update($, isHidden, () => arg === 'hide')
33 return { text: arg === 'hide' ? 'Band hidden.' : 'Band showing.' }
34 }
35 const now = await read($, heat)
36 if (now === null) return { text: 'No reading yet. The gauge warms up after Claude answers once.' }
37 return { text: `${gauge(now)} context ${now}% full` }
38}
39
40export const register: Register = on => {
41 on('session.start', async ($, e, next) => {
42 await $.command.register({ name: 'speed', description: 'Show how full the context window is. /speed hide or /speed show for the band.' })
43 await $.command.register({ name: 'speedometer', description: 'Same as /speed.' })
44 const usage = await $.session.usage()
45 await update($, heat, () => usage.context.percent ?? null)
46 return next(e)
47 })
48
49 on('session.measure', async ($, e, next) => {
50 if (e.changed.includes('context') && e.context.percent !== undefined) {
51 const was = await read($, heat)
52 const now = Math.round(e.context.percent)
53 if (was !== null && was < HOT && now >= HOT) {
54 $.ui.toast(`🏎️ Redlining: context is ${now}% full. /compact soon.`)
55 }
56 await update($, heat, () => now)
57 }
58 return next(e)
59 })
60
61 on('command.run', { command: 'speed' }, async ($, e) => answer($, e.args))
62 on('command.run', { command: 'speedometer' }, async ($, e) => answer($, e.args))
63
64 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
65 if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
66 const now = await read($, heat)
67 if (now === null) return next(e)
68
69 const { Box, Text } = $.ui.resolve(e)
70 const [low, high] = halves(now)
71 // Half the band is ours; every other band above the prompt draws in the rest.
72 const half = Math.floor((e.props.bodyColumns ?? e.viewport?.columns ?? 80) / 2)
73 const below = await next(e)
74 return (
75 <Box flexDirection="row">
76 <Box width={half} flexDirection="column">
77 <Box flexDirection="row">
78 <Text bold>🏎️ </Text>
79 <Text bold>0 </Text>
80 <Text color={colour(now)}>{low}</Text>
81 <Text bold> ½ </Text>
82 <Text color={colour(now)}>{high}</Text>
83 <Text bold> MAX</Text>
84 <Text> context </Text>
85 <Text color={colour(now)} bold>{now}% full</Text>
86 </Box>
87 </Box>
88 {below}
89 </Box>
90 )
91 })
92}
93types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'speedometer': { heat: number | null; isHidden: boolean }
4 }
5}
6