SLOPSHOPPER

context-meter

Context window and session cost meter, drawn above the prompt.

newband
★ 11v0.1.0NOASSERTIONupdated 2026-10-02promptadvisers/claude-mods-starter-kit/plugins/context-meter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-meter
› 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 ⟨Claude Code's own drawing⟩ ◔ 49% 97.4k/200k · $0.42 · 5h 31% ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ◔ 49% 97.4k/200k · $0.42 · 5h 31%
README

Context Meter

Shows conversation fullness, session cost and available usage-limit readings above the prompt. Think of it as a dashboard gauge.

See the complete guide and creation prompt.

Test from the kit root:

claude plugin validate plugins/context-meter
claude plugin test plugins/context-meter
Source 2 files
hooks/context-meter.mjs 122 lines
1// Context Meter: context fill, session cost and rate limits, above the prompt.
2
3const HISTORY = 12;
4const BARS = "▁▂▃▄▅▆▇█";
5const LEVELS = [
6  { upTo: 50, icon: "◔", color: "green" },
7  { upTo: 75, icon: "◑", color: "yellow" },
8  { upTo: 90, icon: "◕", color: "magenta" },
9  { upTo: Infinity, icon: "●", color: "red" },
10];
11const LIMIT_NAMES = { five_hour: "5h", seven_day: "7d", spend_limit: "spend" };
12
13// Held by the host, so the history survives a hot reload of this file.
14const readings = { plugin: "context-meter", key: "readings" };
15const limits = { plugin: "context-meter", key: "limits" };
16
17export function register(on) {
18  // Seed on start (and on resume), so the band shows before the first turn ends.
19  on("session.start", async ($, e, next) => {
20    const result = await next(e);
21    const u = await $.session.usage();
22    const { value: history = [] } = await $.state.get(readings);
23    const reading = toReading(u);
24    if (reading && !sameAs(history[history.length - 1], reading)) {
25      await $.state.set(readings, [...history, reading].slice(-HISTORY));
26    }
27    await $.state.set(limits, toLimits(u.rateLimits));
28    return result;
29  });
30
31  // Pushed by the engine after each main-thread turn and when a limit moves.
32  on("session.measure", async ($, e, next) => {
33    const result = await next(e);
34    if (e.changed.includes("context") || e.changed.includes("cost")) {
35      const { value: history = [] } = await $.state.get(readings);
36      const reading = toReading(e);
37      if (reading && !sameAs(history[history.length - 1], reading)) {
38        await $.state.set(readings, [...history, reading].slice(-HISTORY));
39      }
40    }
41    if (e.changed.includes("rateLimits")) {
42      await $.state.set(limits, toLimits(e.rateLimits));
43    }
44    return result;
45  });
46
47  on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
48    const original = await next(e);
49    const { value: history = [] } = await $.state.get(readings);
50    const { value: windows = [] } = await $.state.get(limits);
51    if (e.props.hasSurvey || history.length === 0) return original;
52    const { Box, Text } = $.ui.resolve(e);
53    const meter = band(Box, Text, history, windows, e.props.bodyColumns);
54    if (!original) return meter;
55    return Box({ flexDirection: "column", children: [original, meter] });
56  });
57}
58
59function toReading(u) {
60  const window = u.context?.window;
61  if (!window || u.context.tokens === undefined) return null;
62  const tokens = u.context.tokens;
63  const percent = u.context.percent ?? Math.round((tokens / window) * 100);
64  return { tokens, window, percent, usd: u.cost?.usd ?? 0 };
65}
66
67function toLimits(rateLimits = []) {
68  return rateLimits.map((r) => ({ kind: r.kind, percentUsed: r.percentUsed }));
69}
70
71function sameAs(a, b) {
72  return a !== undefined && a.tokens === b.tokens && a.usd === b.usd && a.window === b.window;
73}
74
75function band(Box, Text, history, windows, columns) {
76  const now = history[history.length - 1];
77  const prev = history.length > 1 ? history[history.length - 2] : undefined;
78  const level = LEVELS.find((l) => now.percent < l.upTo);
79  const dim = (children) => Text({ dimColor: true, children });
80
81  const parts = [
82    Text({ color: level.color, bold: true, children: `${level.icon} ${now.percent}%` }),
83    dim(` ${short(now.tokens)}/${short(now.window)}`),
84  ];
85  if (columns >= 70 && history.length > 1) {
86    parts.push(Text({ color: level.color, children: ` ${sparkline(history)}` }));
87  }
88
89  parts.push(dim("  ·  "));
90  parts.push(Text({ bold: true, children: usd(now.usd) }));
91  if (columns >= 60 && prev && now.usd > prev.usd) {
92    parts.push(dim(` +${usd(now.usd - prev.usd)}`));
93  }
94
95  if (columns >= 50) {
96    for (const w of windows) {
97      const name = LIMIT_NAMES[w.kind] ?? w.kind;
98      const color = w.percentUsed >= 90 ? "red" : w.percentUsed >= 75 ? "yellow" : undefined;
99      parts.push(dim("  ·  "));
100      parts.push(dim(`${name} `));
101      parts.push(Text({ color, bold: color !== undefined, children: `${Math.round(w.percentUsed)}%` }));
102    }
103  }
104
105  return Box({ flexDirection: "row", paddingX: 1, children: parts });
106}
107
108function sparkline(history) {
109  const top = Math.max(...history.map((r) => r.tokens), 1);
110  return history.map((r) => BARS[Math.floor((r.tokens / top) * (BARS.length - 1))]).join("");
111}
112
113function usd(n) {
114  return n < 10 ? `$${n.toFixed(2)}` : `$${n.toFixed(1)}`;
115}
116
117function short(n) {
118  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(1)}M`;
119  if (n >= 1_000) return `${+(n / 1_000).toFixed(1)}k`;
120  return String(n);
121}
122
types/index.d.ts 9 lines
1export type ContextMeterReading = { tokens: number; window: number; percent: number; usd: number };
2export type ContextMeterLimit = { kind: string; percentUsed: number };
3
4declare module "claude-code" {
5  interface PluginState {
6    "context-meter": { readings: ContextMeterReading[]; limits: ContextMeterLimit[] };
7  }
8}
9