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

A Claude Code mod that draws a live forecast of your context window in the band above the prompt. We're sharing it as the smallest of the three mods here: one hooks module that reads real usage figures and draws one line of UI.
☂ Showers 67% of context 134.4k / 200k last turns ▁▂█ ▲ +98.3k last turn
After each turn, the band shows:
| Used | Forecast |
|---|---|
| under 25% | ☀ Clear |
| 25–49% | ☁ Cloudy |
| 50–74% | ☂ Showers |
| 75–89% | ☇ Storm |
| 90% up | ↯ Compact soon |
The numbers are real, not estimated. After each main-loop turn, 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, because 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. |
ui.render with {component: "AbovePrompt"} | Draws the band as one line. |
After the first turn, 18% full:

After reading four large files, 67% full:

After one more file, 81% full. Claude Code's own notice reads 90%, because it counts toward the auto-compact point (see the limitations):

Token Weather. A live forecast of your context window. A band above the prompt draws a small chart of context use for each turn, as weather icons. It's clear when the window is mostly empty, and stormy when it's nearly full. It uses
ui.renderon theAbovePromptcomponent, with theSvgelement, and it updates onturn.complete. I haven't confirmed which event or call gives the token counts, so that needs a check. Developers would share it because it looks good in a screenshot, and it answers "why did Claude forget that?" at a glance. The status line only shows text. A weekend build.
The build prompt, which picked this idea and two others by number:
implement 1,2,7. give me zips for them. test them in claude code and get me screenshots of what they look like when used.
$.session.usage(), which returns the same figures as the status line, so the mod shows real numbers rather than an estimate.Svg element. The terminal can't draw Svg, so the chart is a row of block characters.Requirements:
claude plugin validate passes on 2.1.285.AbovePrompt is drawn on the terminal surface only.No environment variables or configuration.
Steps:
git clone https://github.com/anthropics/claude-code-playground.git
cd claude-code-playground/claude-code/mods
claude plugin validate ./token-weather
claude --plugin-dir ./token-weather
Or install it, with the other mods here, from the local marketplace in this folder (see the mods README):
claude plugin marketplace add ./
claude plugin install token-weather@claude-code-playground-mods --scope user
AbovePrompt competes for the same band.| Name | Version | License (SPDX) | Source |
|---|---|---|---|
| None |
None.
Shared as-is as part of claude-code-playground. Not an official Anthropic product; no support or maintenance is implied. See the root README and LICENSE.
Codex CLI 대시보드와 같은 50/75/90% 기준을 사용합니다. session.measure에서도 읽고, 같은 수치의 중복 기록은 합칩니다. 갱신 실패는 STALE, 미제공 수치는 0으로 꾸미지 않습니다. 긴 표시는 한 줄로 자릅니다. 5h/7d 계정 한도와 현재 context 사용률은 서로 다른 값입니다.
anthropics/claude-code-playground의 claude-code/mods/token-weather를 수정한 버전입니다. Copyright 2026 Anthropic PBC, Apache License 2.0. 변경 내역은 저장소 루트의 NOTICE를 참고하세요. Anthropic 공식 배포물이 아닙니다.
hooks/token-weather.mjs 143 lines1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3// Modified 2026 by suhoon (Windows port / display changes). Original: https://github.com/anthropics/claude-code-playground — see NOTICE.
4//
5// Token Weather: a live traffic light for the context window, above the prompt.
6//
7// turn.complete: after each main-loop turn, read the context window's fill
8// from $.session.usage() (the same figures the status line shows) and keep
9// the last HISTORY readings.
10// session.start: take a first reading, so the band shows before any turn.
11// ui.render (AbovePrompt): one line: traffic light, signal word, percent, tokens
12// used of the window, and a block-character chart of the recent turns.
13//
14// The host reads on(...) and $.noun.method(...) from source, so they are
15// spelled literally, and helpers that take $ are top-level functions.
16
17const HISTORY = 12;
18const BARS = "▁▂▃▄▅▆▇█";
19
20// Traffic-light bands, by percent of the window used. One lamp (a single-width
21// ●, not an emoji) that changes color.
22const LAMP = "●";
23const SIGNAL = [
24 { upTo: 50, word: "Go", color: "green" },
25 { upTo: 75, word: "Caution", color: "yellow" },
26 { upTo: 90, word: "Stop", color: "red" },
27 { upTo: Infinity, word: "Compact soon", color: "red" },
28];
29
30// Readings: { tokens, window, percent }, oldest first.
31let readings = [];
32let stale = false;
33
34export function register(on) {
35 on("session.start", async ($, e, next) => {
36 const result = await next(e);
37 readings = [];
38 stale = false;
39 await takeReading($);
40 return result;
41 });
42
43 on("turn.complete", async ($, e, next) => {
44 const result = await next(e);
45 if (e.agentId) {
46 return result;
47 }
48 await takeReading($);
49 return result;
50 });
51
52 on("session.measure", async ($, e, next) => {
53 const result = await next(e);
54 await takeReading($);
55 return result;
56 });
57
58 // Stack the light over whatever the plugins beneath draw, so another mod's
59 // band (replay-theater's hint) never hides it and it never hides theirs.
60 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
61 if (e.hasSurvey || readings.length === 0) {
62 return next(e);
63 }
64 const { Box, Text } = $.ui.resolve(e);
65 const below = await next(e);
66 const light = band(Box, Text, e.bodyColumns ?? 80);
67 return below ? Box({ flexDirection: "column", children: [light, below] }) : light;
68 });
69}
70
71async function takeReading($) {
72 try {
73 const { context } = await $.session.usage();
74 if (!context || !Number.isFinite(context.window) || context.window <= 0 || !Number.isFinite(context.tokens) || context.tokens < 0) {
75 stale = true;
76 return;
77 }
78 const tokens = context.tokens;
79 const percent = Math.round(context.percent ?? (tokens / context.window) * 100);
80 // The session.start reading is 0 before any response; drop it once real readings arrive.
81 readings = readings.filter((r) => r.tokens > 0);
82 const last = readings[readings.length - 1];
83 if (!last || last.tokens !== tokens || last.window !== context.window || last.percent !== percent) {
84 readings.push({ tokens, window: context.window, percent });
85 }
86 stale = false;
87 if (readings.length > HISTORY) {
88 readings = readings.slice(-HISTORY);
89 }
90 $.ui.invalidate("ui.render");
91 } catch {
92 // No reading this turn; the band keeps the last one.
93 stale = true;
94 }
95}
96
97function band(Box, Text, columns) {
98 const now = readings[readings.length - 1];
99 const f = signalFor(now.percent);
100 const trend = trendWord();
101 const parts = [
102 Text({ wrap: "truncate-end", color: stale ? 'gray' : f.color, bold: true, children: `${LAMP} ${f.word}` }),
103 Text({ wrap: "truncate-end", children: ` ${now.percent}% of context` }),
104 Text({ wrap: "truncate-end", dimColor: true, children: ` ${short(now.tokens)} / ${short(now.window)}` }),
105 ];
106 if (stale) parts.push(Text({ wrap: "truncate-end", dimColor: true, children: ' STALE' }));
107 if (columns >= 60) {
108 parts.push(Text({ wrap: "truncate-end", dimColor: true, children: " last turns " }));
109 parts.push(Text({ wrap: "truncate-end", color: f.color, children: chart() }));
110 if (trend) {
111 parts.push(Text({ wrap: "truncate-end", dimColor: true, children: ` ${trend}` }));
112 }
113 }
114 return Box({ flexDirection: "row", paddingX: 1, children: parts });
115}
116
117export function signalFor(percent) {
118 return SIGNAL.find((band) => percent < band.upTo) ?? SIGNAL[SIGNAL.length - 1];
119}
120
121// Bars scale to the busiest reading shown, so growth shows at any fill level.
122function chart() {
123 const top = Math.max(...readings.map((r) => r.tokens), 1);
124 const bars = readings.map((r) => BARS[Math.min(BARS.length - 1, Math.floor((r.tokens / top) * (BARS.length - 1)))]);
125 return bars.join("");
126}
127
128function trendWord() {
129 if (readings.length < 2) {
130 return "";
131 }
132 const delta = readings[readings.length - 1].tokens - readings[readings.length - 2].tokens;
133 if (delta > 0) return `▲ +${short(delta)} last turn`;
134 if (delta < 0) return `▼ ${short(-delta)} last turn`;
135 return "steady";
136}
137
138function short(n) {
139 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
140 if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}k`;
141 return String(n);
142}
143