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

Mod Claude Code qui affiche, au-dessus du prompt, trois puces :
95k/200k) et le coût de la session.Les puces 5h et Week n'apparaissent que si le compte remonte ces limites. Le mod demande Claude Code 2.1.287 ou plus, dans un terminal.
git clone <url-du-depot> token_info
claude --plugin-dir ./token_info
Dérivé du mod token-weather de claude-code-playground (Anthropic PBC, Apache-2.0), modifié pour la mise en page en puces et les limites. Licence Apache-2.0, voir LICENSE. Partagé tel quel, sans support.
hooks/token-weather.mjs 140 lines1// Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Token Weather, three-chip layout: context bar + cost, 5h limit, weekly limit.
5// Based on the token-weather mod from claude-code-playground.
6//
7// The host reads on(...) and $.noun.method(...) from source, so they are
8// spelled literally, and helpers that take $ are top-level functions.
9
10const WINDOWS = {
11 five_hour: { label: "5h", ms: 5 * 3600_000 },
12 seven_day: { label: "Week", ms: 7 * 24 * 3600_000 },
13};
14
15// Last reading: { tokens, window, usd, limits: [{ kind, percentUsed, resetsAt }] }
16let reading = null;
17
18export function register(on) {
19 on("session.start", async ($, e, next) => {
20 const result = await next(e);
21 reading = null;
22 await takeReading($);
23 return result;
24 });
25
26 on("turn.complete", async ($, e, next) => {
27 const result = await next(e);
28 if (e.agentId) {
29 return result;
30 }
31 await takeReading($);
32 return result;
33 });
34
35 on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
36 if (e.hasSurvey || e.props?.hasSurvey || reading === null) {
37 return next(e);
38 }
39 const { Box, Text } = $.ui.resolve(e);
40 return band(Box, Text);
41 });
42}
43
44async function takeReading($) {
45 try {
46 const { context, rateLimits, cost } = await $.session.usage();
47 if (!context || !context.window) {
48 return;
49 }
50 reading = {
51 tokens: context.tokens ?? 0,
52 window: context.window,
53 usd: cost ? cost.usd : null,
54 limits: rateLimits ?? [],
55 };
56 $.ui.invalidate("ui.render");
57 } catch {
58 // No reading this turn; the band keeps the last one.
59 }
60}
61
62// Green when there is room, yellow then red as the share used grows.
63function tone(pct) {
64 return pct >= 85 ? "red" : pct >= 60 ? "yellow" : "green";
65}
66
67function short(n) {
68 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
69 if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
70 return String(n);
71}
72
73function duration(ms) {
74 const m = Math.max(0, Math.round(ms / 60_000));
75 if (m >= 1440) return `${Math.floor(m / 1440)}j`;
76 if (m >= 60) return `${Math.floor(m / 60)}h`;
77 return `${m}min`;
78}
79
80// A thick bar with the label inside: coloured part, then grey part.
81function fatBar(Text, pct, width, label, color) {
82 const pad = Math.max(0, width - label.length);
83 const text = " ".repeat(Math.floor(pad / 2)) + label + " ".repeat(Math.ceil(pad / 2));
84 const full = Math.max(0, Math.min(width, Math.round((pct / 100) * width)));
85 return [
86 Text({ backgroundColor: color, color: "white", bold: true, children: text.slice(0, full) }),
87 Text({ backgroundColor: "#3a3a3a", color: "white", bold: true, children: text.slice(full) }),
88 ];
89}
90
91// A thin line with a dot-sized fill, as in the 5h / Week chips.
92function thinBar(Text, pct, width, color) {
93 const full = Math.max(1, Math.min(width, Math.round((pct / 100) * width)));
94 return [
95 Text({ color, children: "━".repeat(full) }),
96 Text({ color: "#3a3a3a", children: "━".repeat(width - full) }),
97 ];
98}
99
100function chip(Box, key, children) {
101 return Box({ key, borderStyle: "round", borderDimColor: true, paddingX: 1, gap: 1, children });
102}
103
104function band(Box, Text) {
105 const chips = [];
106
107 const pct = Math.min(100, (reading.tokens / reading.window) * 100);
108 const ctx = [
109 Text({ dimColor: true, children: "S" }),
110 Box({ children: fatBar(Text, pct, 20, `${short(reading.tokens)}/${short(reading.window)}`, tone(pct)) }),
111 ];
112 if (reading.usd !== null) {
113 ctx.push(Text({ bold: true, children: `$${reading.usd.toFixed(2)}` }));
114 }
115 chips.push(chip(Box, "ctx", ctx));
116
117 for (const l of reading.limits) {
118 const w = WINDOWS[l.kind];
119 const used = l.percentUsed;
120 const parts = [
121 Text({ dimColor: true, children: w ? w.label : l.kind }),
122 Box({ children: thinBar(Text, used, 8, tone(used)) }),
123 Text({ bold: true, children: `${Math.round(used)}%` }),
124 ];
125 if (l.resetsAt) {
126 const left = Date.parse(l.resetsAt) - Date.now();
127 if (w) {
128 // Margin: share of the window already elapsed minus share used; positive = ahead.
129 const elapsed = Math.min(100, Math.max(0, ((w.ms - left) / w.ms) * 100));
130 const d = elapsed - used;
131 parts.push(Text({ color: d >= 0 ? "green" : d > -10 ? "yellow" : "red", children: `${d >= 0 ? "+" : ""}${d.toFixed(1)}%` }));
132 }
133 parts.push(Text({ dimColor: true, children: duration(left) }));
134 }
135 chips.push(chip(Box, l.kind, parts));
136 }
137
138 return Box({ flexDirection: "row", flexWrap: "wrap", children: chips });
139}
140