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

<!-- Modified by joaopasbento in 2026 from the token-weather example in anthropics/claude-code-playground (commit 569c5283d9a0). See "Origin" below. -->
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.
☂ Showers 72% of context 720k / 1M last turns ▁▂▆ ▲ +560k last turn
The band shows:
| Used | Forecast | Color |
|---|---|---|
| under 50% | ☀ Clear | green: the theme's success |
| 50–69% | ☁ Cloudy | none: the plain text color |
| 70–79% | ☂ Showers | amber: the theme's warning |
| 80–89% | ☇ Storm | orange: #cc5a14, fixed |
| 90% up | ↯ Compact soon | red: 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, andpercent: 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.
| Hook | What it does |
|---|---|
session.start | Takes a first reading, so the band shows before the first turn. |
turn.complete | Takes a reading after each main-loop turn. Subagent turns are skipped. |
turn.step | After 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. |
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.
From a clone of this repository:
claude plugin validate ./token-weather
claude plugin test ./token-weather
AbovePrompt competes for the same band.None.
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:
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.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.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/token-weather.test.ts is new and runs under claude plugin test.Apache License 2.0. See LICENSE.
hooks/token-weather.mjs 179 lines1// 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