SLOPSHOPPER

Speedometer

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

bandcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · speedometer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /speed ⎿ speedometer: 0 ███ ½ ░░░ MAX context 49% full 🏎️ 0 ███ ½ ░░░ MAX context 49% full ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🏎️ 0 ███ ½ ░░░ MAX context 49% full ⟨Claude Code's own drawing⟩
README

Speedometer

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.

What it runs and sends

Nothing goes anywhere. Here is each thing, plainly.

What it sends, and where.

  • Nothing. It reads how full the context window is from Claude Code, with $.session.usage, and shows it on your screen.
  • It stores nothing on disk.

Which programs it runs, and why.

  • None. It runs no programs.

What it puts in the prompts it submits.

  • It submits no prompts, and changes none of yours.

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.

More mods

See every Rostech mod, live, at rostech.app.

Source 2 files
hooks/register.tsx 93 lines
1import { 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}
93
types/index.d.ts 6 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'speedometer': { heat: number | null; isHidden: boolean }
4  }
5}
6