Context window usage as one line above the prompt, told as a Steel Ball Run race: Gyro's five lessons and a horse running to the finish flag.

A Claude Code mod that shows how full the context window is, as one line above the prompt, told as a Steel Ball Run race.
🤠 Lesson 1: Don't have any weird hopes in me 🏁┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄🐎━━ 9%
💪 Lesson 2: Work those muscles 🏁┄┄┄┄┄┄┄┄┄┄┄┄┄┄🐎━━━━━━ 30%
💅 Lesson 3: Believe in the spin 🏁┄┄┄┄┄┄┄┄🐎━━━━━━━━━━━━ 60%
꩜ Lesson 4: Pay your respects 🏁┄┄┄┄🐎━━━━━━━━━━━━━━━━ 80%
💫 Lesson 5: The shortest route was a detour 🏁┄🐎━━━━━━━━━━━━━━━━━━━ 95%
▲ +3.5k last turn.Type this at the prompt of a Claude Code terminal session:
/plugin install jojo-sbr-token-usage --marketplace dodofk/jojo-sbr-token-usage
Answer y to add the marketplace, then press Enter to pick the user scope. The band appears after the next turn.
Tested on Claude Code 2.1.295.
The levels are the LEVELS table at the top of hooks/band.mjs, one row each: threshold, icon, text and color. iconColor tints a non-emoji icon separately from the text.
The Lesson 4 icon ꩜ (U+AA5C) needs a font that has it, such as Noto Sans Cham. If it shows as a box, swap it for ❂ or ◎.
To work on the mod from a clone:
claude plugin validate .
claude plugin test .
Adapted from Anthropic's token-weather sample mod in claude-code-playground (Apache-2.0). Changes from the sample: the weather levels became lessons, and the recent-turns chart became the race track.
This is an unofficial fan project. JoJo's Bizarre Adventure: Steel Ball Run belongs to Hirohiko Araki and Shueisha; this mod is not affiliated with or endorsed by them. The lesson wording follows common fan translations.
hooks/band.mjs 121 lines1// Adapted from token-weather, Copyright 2026 Anthropic PBC
2// SPDX-License-Identifier: Apache-2.0
3//
4// jojo-sbr-token-usage: how full the context window is, as one line above the prompt,
5// told as a Steel Ball Run race: the levels are Gyro's five lessons, from
6// Lesson 1 on an empty window to Lesson 5 when a compaction is near.
7//
8// turn.complete: after each main-loop turn, read the context window's fill
9// from $.session.usage() (the same figures the status line shows) and keep
10// the last HISTORY readings.
11// session.start: take a first reading, so the band shows before any turn.
12// ui.render (AbovePrompt): one line: icon, the level's word, a race track with
13// a horse running toward the finish flag, percent, and the last turn's change.
14//
15// The host reads on(...) and $.noun.method(...) from source, so they are
16// spelled literally, and helpers that take $ are top-level functions.
17
18const HISTORY = 2;
19
20// Levels, by percent of the window used. To re-theme the band, change the
21// icon, word and color of each row; a color is a name or a hex. iconColor
22// tints a non-emoji icon separately from the word.
23const LEVELS = [
24 { upTo: 25, icon: "🤠", word: "Lesson 1: Don't have any weird hopes in me", color: "#7BC47F" },
25 { upTo: 50, icon: "💪", word: "Lesson 2: Work those muscles", color: "#F6AE2D" },
26 { upTo: 75, icon: "💅", word: "Lesson 3: Believe in the spin", color: "#E8C547" },
27 { upTo: 90, icon: "꩜ ", iconColor: "#FFC93C", word: "Lesson 4: Pay your respects", color: "#F4A6C0" },
28 { upTo: Infinity, icon: "💫", word: "Lesson 5: The shortest route was a detour", color: "#FF5F5F" },
29];
30
31// Readings: { tokens, window, percent }, oldest first.
32let readings = [];
33
34export function register(on) {
35 on("session.start", async ($, e, next) => {
36 const result = await next(e);
37 readings = [];
38 await takeReading($);
39 return result;
40 });
41
42 on("turn.complete", async ($, e, next) => {
43 const result = await next(e);
44 if (e.agentId) {
45 return result;
46 }
47 await takeReading($);
48 return result;
49 });
50
51 on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
52 if (e.props.hasSurvey || readings.length === 0) {
53 return next(e);
54 }
55 const { Box, Text } = $.ui.resolve(e);
56 return band(Box, Text, e.props.bodyColumns ?? 80);
57 });
58}
59
60async function takeReading($) {
61 try {
62 const { context } = await $.session.usage();
63 if (!context || !context.window) {
64 return;
65 }
66 const tokens = context.tokens ?? 0;
67 const percent = Math.round(context.percent ?? (tokens / context.window) * 100);
68 // The session.start reading is 0 before any response; drop it once real readings arrive.
69 readings = readings.filter((r) => r.tokens > 0);
70 readings.push({ tokens, window: context.window, percent });
71 if (readings.length > HISTORY) {
72 readings = readings.slice(-HISTORY);
73 }
74 $.ui.invalidate("ui.render");
75 } catch {
76 // No reading this turn; the band keeps the last one.
77 }
78}
79
80function band(Box, Text, columns) {
81 const now = readings[readings.length - 1];
82 const level = levelFor(now.percent);
83 const trend = trendWord();
84 // The horse emoji faces left, so the race runs right to left: the flag is
85 // at the left end and the horse closes in on it as the window fills.
86 const track = columns >= 80 ? 20 : 10;
87 const behind = Math.max(0, Math.min(track, Math.round((now.percent / 100) * track)));
88 const ahead = track - behind;
89 const parts = [
90 Text({ color: level.iconColor ?? level.color, bold: true, children: `${level.icon} ` }),
91 Text({ color: level.color, bold: true, children: level.word }),
92 Text({ dimColor: true, children: ` 🏁${"┄".repeat(ahead)}` }),
93 Text({ children: "🐎" }),
94 Text({ color: level.color, children: `${"━".repeat(behind)} ${now.percent}%` }),
95 ];
96 if (trend && columns >= 100) {
97 parts.push(Text({ dimColor: true, children: ` ${trend}` }));
98 }
99 return Box({ flexDirection: "row", paddingX: 1, children: parts });
100}
101
102function levelFor(percent) {
103 return LEVELS.find((level) => percent < level.upTo) ?? LEVELS[LEVELS.length - 1];
104}
105
106function trendWord() {
107 if (readings.length < 2) {
108 return "";
109 }
110 const delta = readings[readings.length - 1].tokens - readings[readings.length - 2].tokens;
111 if (delta > 0) return `▲ +${short(delta)} last turn`;
112 if (delta < 0) return `▼ ${short(-delta)} last turn`;
113 return "steady";
114}
115
116function short(n) {
117 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
118 if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}k`;
119 return String(n);
120}
121