SLOPSHOPPER

token-weather

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

newband
★ 4v0.1.0no licenseupdated 2026-10-22tatthien/dotfiles/claude/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 ██ steady ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
☁ Cloudy 49% of context 97.4k / 200k last turns ██ steady
README

.dotfiles

There is no magic here. The bash script just creates symbolic links from ~/dotfiles.

# >>> start

cd ~
git clone git@github.com:tatthien/dotfiles.git
cd dotfiles
./install.sh

# >>> done
Source 2 files
hooks/register.mjs 82 lines
1const HISTORY = 12;
2const BARS = "▁▂▃▄▅▆▇█";
3const FORECAST = [
4  { upTo: 25, icon: "☀", word: "Clear", color: "yellow" },
5  { upTo: 50, icon: "☁", word: "Cloudy", color: "cyan" },
6  { upTo: 75, icon: "☂", word: "Showers", color: "blue" },
7  { upTo: 90, icon: "☇", word: "Storm", color: "magenta" },
8  { upTo: Infinity, icon: "↯", word: "Compact soon", color: "red" },
9];
10
11// Held by the host, so the history survives a hot reload of this file.
12const readings = { plugin: "token-weather", key: "readings" };
13
14export function register(on) {
15  on("session.start", async ($, e, next) => {
16    const result = await next(e);
17    await takeReading($);
18    return result;
19  });
20
21  on("turn.complete", async ($, e, next) => {
22    const result = await next(e);
23    if (!e.agentId) {
24      await takeReading($); // main-loop turns only, not subagents
25    }
26    return result;
27  });
28
29  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
30    const { value: history = [] } = await $.state.get(readings);
31    if (e.surface !== "terminal" || e.props.hasSurvey || history.length === 0) {
32      return next(e);
33    }
34    const { Box, Text } = $.ui.resolve(e);
35    return band(Box, Text, history, e.props.bodyColumns);
36  });
37}
38
39async function takeReading($) {
40  const { context } = await $.session.usage();
41  if (!context?.window) return;
42  const tokens = context.tokens ?? 0;
43  const percent = context.percent ?? Math.round((tokens / context.window) * 100);
44  const { value: history = [] } = await $.state.get(readings);
45  await $.state.set(readings, [...history, { tokens, window: context.window, percent }].slice(-HISTORY));
46}
47
48function band(Box, Text, history, columns) {
49  const now = history[history.length - 1];
50  const f = FORECAST.find((b) => now.percent < b.upTo);
51  const parts = [
52    Text({ color: f.color, bold: true, children: `${f.icon}  ${f.word}` }),
53    Text({ children: `  ${now.percent}% of context` }),
54    Text({ dimColor: true, children: `  ${short(now.tokens)} / ${short(now.window)}` }),
55  ];
56  if (columns >= 60) {
57    parts.push(Text({ dimColor: true, children: "   last turns " }));
58    parts.push(Text({ color: f.color, children: sparkline(history) }));
59    if (history.length > 1) {
60      parts.push(Text({ dimColor: true, children: trend(history) }));
61    }
62  }
63  return Box({ flexDirection: "row", paddingX: 1, children: parts });
64}
65
66function sparkline(history) {
67  const top = Math.max(...history.map((r) => r.tokens), 1);
68  return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
69}
70
71function trend(history) {
72  const delta = history[history.length - 1].tokens - history[history.length - 2].tokens;
73  if (delta === 0) return "  steady";
74  return delta > 0 ? `  ▲ +${short(delta)} last turn` : `  ▼ ${short(-delta)} last turn`;
75}
76
77function short(n) {
78  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
79  if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
80  return String(n);
81}
82
types/index.d.ts 8 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}
8