SLOPSHOPPER

token-weather

A live forecast of the context window, drawn above the prompt.

newband
v0.1.0no licenseupdated 2026-10-06soenkenils/claude-code-mods/token-weather
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · token-weather
› 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 ☁ Cloudy 49% of context 97.4k / 200k last turns █ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
☁ Cloudy 49% of context 97.4k / 200k last turns █
Source 2 files
hooks/token-weather.ts 73 lines
1// Token Weather: a live forecast of the context window, above the prompt.
2
3import type { On } from "claude-code";
4import type { TokenWeatherReading as Reading } from "../types";
5
6const HISTORY = 12;
7const BARS = "▁▂▃▄▅▆▇█";
8const FORECAST = [
9  { upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
10  { upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
11  { upTo: 75, icon: "☂", word: "Showers", color: "blue" },
12  { upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
13  { upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
14] as const;
15
16// Held by the host, so the history survives a hot reload of this file.
17const readings = { plugin: "token-weather", key: "readings" } as const;
18
19export function register(on: On) {
20  // Fires after each main-thread turn (never a subagent's) with the usage figures.
21  on("session.measure", async ($, e, next) => {
22    const { tokens, window, percent } = e.context;
23    if (e.changed.includes("context") && tokens !== undefined) {
24      const reading = { tokens, window, percent: percent ?? Math.round((tokens / window) * 100) };
25      const { value: history = [] } = await $.state.get(readings);
26      await $.state.set(readings, [...history, reading].slice(-HISTORY));
27    }
28    return next(e);
29  });
30
31  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
32    const { value: history = [] } = await $.state.get(readings);
33    const now = history.at(-1);
34    if (e.props.hasSurvey || !now) {
35      return next(e);
36    }
37    const { Text, Box } = $.ui.resolve(e);
38    // The last tier's upTo is Infinity, so find() always matches.
39    const f = FORECAST.find((b) => now.percent < b.upTo)!;
40    const parts = [
41      Text({ color: f.color, bold: true, children: `${f.icon}  ${f.word}` }),
42      Text({ children: `  ${now.percent}% of context` }),
43      Text({ dimColor: true, children: `  ${short(now.tokens)} / ${short(now.window)}` }),
44    ];
45    if (e.props.bodyColumns >= 60) {
46      parts.push(Text({ dimColor: true, children: "   last turns " }));
47      parts.push(Text({ color: f.color, children: sparkline(history) }));
48      const prev = history.at(-2);
49      if (prev) {
50        parts.push(Text({ dimColor: true, children: trend(prev, now) }));
51      }
52    }
53    return Box({ flexDirection: "row", paddingX: 1, children: parts });
54  });
55}
56
57function sparkline(history: Reading[]) {
58  const top = Math.max(...history.map((r) => r.tokens), 1);
59  return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
60}
61
62function trend(prev: Reading, now: Reading) {
63  const delta = now.tokens - prev.tokens;
64  if (delta === 0) return "  steady";
65  return delta > 0 ? `  ▲ +${short(delta)} last turn` : `  ▼ ${short(-delta)} last turn`;
66}
67
68function short(n: number) {
69  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
70  if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
71  return String(n);
72}
73
types/index.d.ts 7 lines
1export type TokenWeatherReading = { tokens: number; window: number; percent: number };
2
3declare module "claude-code" {
4  interface PluginState {
5    "token-weather": { readings: TokenWeatherReading[] };
6  }
7}