SLOPSHOPPER

token-weather

A live forecast of the context window, drawn above the prompt: a weather icon, the fill percentage, and a chart of the last turns.

newband
v0.1.2Apache-2.0updated 2026-10-06joaopasbento/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 ☀ Clear 49% of context 97.4k / 200k last turns ▄▄ steady ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
☀ Clear 49% of context 97.4k / 200k last turns ▄▄ steady
README

<!-- Modified by joaopasbento in 2026 from the token-weather example in anthropics/claude-code-playground (commit 569c5283d9a0). See "Origin" below. -->

Token Weather

A Claude Code mod that draws a live forecast of your context window in the band above the prompt: one hooks module that reads real usage figures and draws one line of UI.

What it shows

 ☂  Showers  72% of context  720k / 1M   last turns ▁▂▆  ▲ +560k last turn

The band shows:

  • a weather icon and word for how full the window is,
  • the percentage used, and the tokens used out of the window,
  • a chart of the last 12 turns, drawn with block characters, each bar measured against 90% of the window (a full bar means the stop was reached), and
  • how much the last turn added.
UsedForecastColor
under 50%☀ Cleargreen: the theme's success
50–69%☁ Cloudynone: the plain text color
70–79%☂ Showersamber: the theme's warning
80–89%☇ Stormorange: #cc5a14, fixed
90% up↯ Compact soonred: the theme's error

The limits compare the exact share of the window (tokens / window), not the rounded percentage: at 899,999 of 1M the band reads 90% but stays ☇ Storm, and ↯ shows from 900,000. The theme keys follow Claude Code's light and dark themes. No theme key is an orange that reads well on both, so Storm uses one fixed tone between the themes' amber and red.

The numbers are real, not estimated. The mod calls $.session.usage() and reads context:

  • tokens: the input tokens the last response was answered over (uncached, cache-written and cache-read together),
  • window: the context window of the session's model, and
  • percent: tokens over window.

These are the same figures the status line shows (total_input_tokens, context_window_size, used_percentage). The call is free: the mod doesn't ask for a breakdown, so it sends no token-count request.

HookWhat it does
session.startTakes a first reading, so the band shows before the first turn.
turn.completeTakes a reading after each main-loop turn. Subagent turns are skipped.
turn.stepAfter each model request of a main-loop turn (so after each tool use too), updates the icon, percentage and tokens from that request's usage. The chart and the last-turn change still count whole turns. Subagent requests are skipped.
ui.render with {component: "AbovePrompt"}Draws the band as one line. It gives way to a survey, and hides the chart when the band is narrower than 60 columns.

Install

Requires Claude Code 2.1.287 or later, where mods load by default. The band is drawn above the prompt in the terminal and in the desktop app's Code tab.

In Claude Code:

/plugin marketplace add joaopasbento/claude-mods
/plugin install token-weather@claude-mods

Then run /reload-plugins, or start a new session. No environment variables or configuration.

Test it

From a clone of this repository:

claude plugin validate ./token-weather
claude plugin test ./token-weather

Notes and limitations

  • The percentage is of the full window. Claude Code's own "context used" notice counts against the auto-compact point, which is lower, so the two can differ.
  • The band's state updates during a turn, after each model request of the main loop; the chart and the last-turn change update once per turn.
  • The chart's bars are absolute: each measures its turn against 90% of the window, so a full bar means the stop was reached, and a light session (160k of 1M) stays low.
  • The history resets when the session starts, or when the plugin reloads.
  • On a 1M-token window, ordinary work stays at ☀ for a long time. That's accurate.
  • One band per session. Another plugin that draws AbovePrompt competes for the same band.
  • Before the first response, the band reads 0%, because no response has reported usage yet.

Dependencies

None.

Origin

This mod started from the token-weather example in anthropics/claude-code-playground, folder claude-code/mods/token-weather, at commit 569c5283d9a0, licensed under the Apache License 2.0. What changed from that example:

  • Updates mid-response. A turn.step hook updates the icon, percentage and tokens after each model request of the main loop, from that request's own usage. The example only updated at session start and at the end of each turn.
  • New limits and colors. Clear under 50%, Cloudy 50–69%, Showers 70–79%, Storm 80–89%, Compact soon from 90%, compared on the exact share of the window. Colors are Claude Code theme keys (success, warning, error), the plain text color for Cloudy, and a fixed orange for Storm. The example used fixed ANSI colors and limits at 25/50/75/90% of the rounded percentage.
  • Absolute chart. Each bar measures against 90% of the window. The example scaled the bars to the fullest turn shown.
  • Fixed props. The mod reads hasSurvey and bodyColumns from e.props, where Claude Code puts them. The example read them from the event itself, so the band never gave way to surveys and the chart never hid in narrow windows.
  • Tests. tests/token-weather.test.ts is new and runs under claude plugin test.
  • Packaging. A new author and marketplace, this README rewritten, and the example's screenshots removed, since they showed the old colors.

License

Apache License 2.0. See LICENSE.

Source 1 files
hooks/token-weather.mjs 179 lines
1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Modified by joaopasbento in 2026 from the token-weather example in
5// anthropics/claude-code-playground (commit 569c5283d9a0): updates mid-response,
6// new limits and colors, an absolute chart, and props read from e.props.
7//
8// Token Weather: a live forecast of the context window, above the prompt.
9//
10// turn.complete: after each main-loop turn, read the context window's fill
11// from $.session.usage() (the same figures the status line shows) and keep
12// the last HISTORY readings.
13// turn.step: after each model request of a main-loop turn (each tool the
14// model uses ends one request and starts the next), take a live reading from
15// that request's own usage (the same sum the status line's tokens are), with
16// the window from $.session.usage(): the icon, percent and tokens follow it
17// mid-response, while the chart and the last-turn change count whole turns.
18// session.start: take a first reading, so the band shows before any turn.
19// ui.render (AbovePrompt): one line: icon, forecast word, percent, tokens
20// used of the window, and a block-character chart of the recent turns, each
21// bar measured against STOP of the window (a full bar is the stop reached).
22//
23// The host reads on(...) and $.noun.method(...) from source, so they are
24// spelled literally, and helpers that take $ are top-level functions.
25
26const HISTORY = 12;
27const BARS = "▁▂▃▄▅▆▇█";
28// The stop: the share of the window where the session is meant to end
29// (900k of 1M). The last forecast starts there and a full bar reaches it.
30const STOP = 0.9;
31
32// Forecast bands, by the exact share of the window used (tokens / window,
33// never the rounded percent). Colors are Claude Code theme keys, which follow
34// the light and dark themes, except Cloudy (the plain text color) and Storm:
35// no theme key is an orange that reads on both, so it is one fixed tone
36// between the themes' amber and red.
37const FORECAST = [
38// Single-width text symbols, not emoji: they line up in every terminal font.
39  { upTo: 0.5, icon: "☀", word: "Clear", color: "success" },
40  { upTo: 0.7, icon: "☁", word: "Cloudy", color: undefined },
41  { upTo: 0.8, icon: "☂", word: "Showers", color: "warning" },
42  { upTo: STOP, icon: "☇", word: "Storm", color: "#cc5a14" },
43  { upTo: Infinity, icon: "↯", word: "Compact soon", color: "error" },
44];
45
46// Readings: { tokens, window, percent }, oldest first.
47let readings = [];
48// The reading after the latest model request of the running turn; null
49// between turns, when the band shows the last turn's reading.
50let live = null;
51
52export function register(on) {
53  on("session.start", async ($, e, next) => {
54    const result = await next(e);
55    readings = [];
56    live = null;
57    await takeReading($);
58    return result;
59  });
60
61  on("turn.complete", async ($, e, next) => {
62    const result = await next(e);
63    if (e.agentId) {
64      return result;
65    }
66    await takeReading($);
67    return result;
68  });
69
70  on("turn.step", async function* ($, e, next) {
71    const result = yield* next(e);
72    if (!e.agentId) {
73      await takeLiveReading($, result && result.usage);
74    }
75    return result;
76  });
77
78  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
79    if (e.props.hasSurvey || (readings.length === 0 && !live)) {
80      return next(e);
81    }
82    const { Box, Text } = $.ui.resolve(e);
83    return band(Box, Text, e.props.bodyColumns ?? 80);
84  });
85}
86
87async function takeReading($) {
88  try {
89    const { context } = await $.session.usage();
90    if (!context || !context.window) {
91      return;
92    }
93    const tokens = context.tokens ?? 0;
94    const percent = Math.round(context.percent ?? (tokens / context.window) * 100);
95    // The session.start reading is 0 before any response; drop it once real readings arrive.
96    readings = readings.filter((r) => r.tokens > 0);
97    readings.push({ tokens, window: context.window, percent });
98    if (readings.length > HISTORY) {
99      readings = readings.slice(-HISTORY);
100    }
101    live = null;
102    $.ui.invalidate("ui.render");
103  } catch {
104    // No reading this turn; the band keeps the last one.
105  }
106}
107
108// usage: the request's own, as the API reported it; without it, the figures
109// $.session.usage() holds.
110async function takeLiveReading($, usage) {
111  try {
112    const { context } = await $.session.usage();
113    if (!context || !context.window) {
114      return;
115    }
116    let tokens = context.tokens ?? 0;
117    let percent = Math.round(context.percent ?? (tokens / context.window) * 100);
118    if (usage) {
119      tokens = (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
120      percent = Math.round((tokens / context.window) * 100);
121    }
122    live = { tokens, window: context.window, percent };
123    $.ui.invalidate("ui.render");
124  } catch {
125    // No reading this step; the band keeps the last one.
126  }
127}
128
129function band(Box, Text, columns) {
130  const now = live ?? readings[readings.length - 1];
131  const f = forecastFor(now.tokens / now.window);
132  const trend = trendWord();
133  const parts = [
134    Text({ ...colored(f), bold: true, children: `${f.icon}  ${f.word}` }),
135    Text({ children: `  ${now.percent}% of context` }),
136    Text({ dimColor: true, children: `  ${short(now.tokens)} / ${short(now.window)}` }),
137  ];
138  if (columns >= 60) {
139    parts.push(Text({ dimColor: true, children: "   last turns " }));
140    parts.push(Text({ ...colored(f), children: chart() }));
141    if (trend) {
142      parts.push(Text({ dimColor: true, children: `  ${trend}` }));
143    }
144  }
145  return Box({ flexDirection: "row", paddingX: 1, children: parts });
146}
147
148function forecastFor(share) {
149  return FORECAST.find((band) => share < band.upTo) ?? FORECAST[FORECAST.length - 1];
150}
151
152// The color prop, left out for a band drawn in the plain text color.
153function colored(f) {
154  return f.color ? { color: f.color } : {};
155}
156
157// Bars scale to STOP of each reading's window, so a full bar is the stop
158// reached and a light session stays low.
159function chart() {
160  const bars = readings.map((r) => BARS[Math.min(BARS.length - 1, Math.floor((r.tokens / (r.window * STOP)) * (BARS.length - 1)))]);
161  return bars.join("");
162}
163
164function trendWord() {
165  if (readings.length < 2) {
166    return "";
167  }
168  const delta = readings[readings.length - 1].tokens - readings[readings.length - 2].tokens;
169  if (delta > 0) return `▲ +${short(delta)} last turn`;
170  if (delta < 0) return `▼ ${short(-delta)} last turn`;
171  return "steady";
172}
173
174function short(n) {
175  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
176  if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}k`;
177  return String(n);
178}
179