SLOPSHOPPER

jojo-sbr-token-usage

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.

newband
★ 1v0.1.0Apache-2.0updated 2026-10-13dodofk/jojo-sbr-token-usage
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · jojo-sbr-token-usage
› 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 💪 Lesson 2: Work those muscles 🏁┄┄┄┄┄┄┄┄┄┄🐎━━━━━━━━━━ 49% steady ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
💪 Lesson 2: Work those muscles 🏁┄┄┄┄┄┄┄┄┄┄🐎━━━━━━━━━━ 49% steady
README

jojo-sbr-token-usage

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%
  • The level is one of Gyro's five lessons, picked by how much of the window is used: under 25%, 50%, 75%, 90%, and above.
  • The horse runs right to left (the emoji faces left) toward the flag. The solid line behind it is context already used; the flag is 100% of the window.
  • On terminals at least 100 columns wide, the line ends with how much the last turn added, such as ▲ +3.5k last turn.

Install

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.

Customize

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 .

Credits

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.

License

Apache-2.0

Source 1 files
hooks/band.mjs 121 lines
1// 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