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.

band
v0.1.0Apache-2.0updated 2026-10-11chaillme/conso-claude-mod
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 ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ S 97k/200k $0.42 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ S 97k/200k $0.42 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
README

token_info

Mod Claude Code qui affiche, au-dessus du prompt, trois puces :

  • S : le remplissage du contexte (95k/200k) et le coût de la session.
  • 5h : la limite de session, en % consommé, avec l'écart de rythme et le temps avant la remise à zéro.
  • Week : la limite hebdomadaire, même présentation.

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.

Essayer

git clone <url-du-depot> token_info
claude --plugin-dir ./token_info

Origine

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.

Source 1 files
hooks/token-weather.mjs 140 lines
1// 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