SLOPSHOPPER

context-bar

Your context window as a stacked bar above the prompt, a color per category, with token counts and the compaction point. /context-bar shows or hides it.

newbandcommand
★ 167v0.1.1MITupdated 2026-10-05hamzafer/claude-code-mods/mods/context-bar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-bar
› 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 › /context-bar ⎿ context-bar: Context bar hidden. /context-bar shows it again ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<h1 align="center">Claude Code mods</h1>

<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>

<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>

<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>

🚀 Install

Add the marketplace once, then install any mod by name:

claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods

Or install the general-purpose set in one go:

for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
  claude plugin install "$m@claude-code-mods"
done

Restart Claude Code after installing. To try one without installing:

git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar

Needs Claude Code 2.1.287+. A few mods need more (Chrome, gh, a connector); the tables say which.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+/cache-clock setup
📍where-am-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key/openai-balance
📡agent-radarOne live line per running subagent/radar
🔍review-watchOne live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends
🌐browser-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost/route

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours/gate
📏rulebook-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app/music

<a id="mission-control"></a>

🛰️ Flagship: mission-control

Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

mission-control at 4x: two subagents and a logout feature landing across four files

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.

  • 🤖 w shows the agents and every tool call, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

Source 2 files
hooks/register.tsx 183 lines
1// Context Bar: what is filling my context window?
2//   Above the prompt, the window as one stacked bar, a color per category as /context
3//   draws them (system prompt, tools, MCP tools, memory files, skills, messages, free),
4//   with a legend of tokens and shares and where auto-compaction runs. It refreshes after
5//   each turn. /context-bar shows or hides it, and the choice is kept across sessions.
6import { atom, read, update } from 'claude-code'
7import type { EngineInterface, Register } from 'claude-code'
8
9import type { Reading, Slice } from '../types'
10
11const MIN_WIDTH = 20 // narrower than this, the bar is not drawn
12const SPLIT = '   '
13// /context's theme gives several rows the same grey, so each used row gets its own color, in order.
14const PALETTE = ['#7aa2f7', '#7dcfff', '#bb9af7', '#9ece6a', '#e0af68', '#f7768e', '#73daca', '#ff9e64', '#c0caf5']
15const MESSAGES = '#d97757' // the row that grows, in the accent color
16const FREE = '#808080' // a mid grey thin line reads as empty on dark and light themes alike
17const BUFFER = '#808080'
18const GLYPH = { used: '█', free: '─', buffer: '░' } as const
19
20// Held by the host, so the bar survives a hot reload of this file.
21const reading = atom({ plugin: 'context-bar', key: 'reading' } as const, null as Reading | null)
22const isHidden = atom({ plugin: 'context-bar', key: 'isHidden' } as const, false)
23
24export const register: Register = on => {
25  on('session.start', async ($, e, next) => {
26    const r = await next(e)
27    await $.command.register({ name: 'context-bar', description: 'Show or hide the context window bar above the prompt' }).catch(() => {}) // a name Claude Code already has is refused: start anyway
28    const hidden = (await $.store.get('isHidden').catch(() => undefined)) === true
29    await update($, isHidden, () => hidden)
30    void refresh($).catch(() => {})
31    return r
32  })
33
34  on('turn.complete', async ($, e, next) => {
35    const r = await next(e)
36    if (!e.agentId) await refresh($).catch(() => {}) // a subagent's turn fills its own window, not this one
37    return r
38  })
39
40  on('session.compact', async ($, e, next) => {
41    const r = await next(e)
42    if (!e.agentId && 'messages' in r) void refresh($).catch(() => {}) // a /compact empties the window without a turn ending
43    return r
44  })
45
46  on('command.run', { command: 'context-bar' }, async $ => {
47    const hidden = await update($, isHidden, h => !h)
48    await $.store.set('isHidden', hidden).catch(() => {})
49    if (!hidden) await refresh($).catch(() => {})
50    return { text: hidden ? 'Context bar hidden. /context-bar shows it again' : 'Context bar on' }
51  })
52
53  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
54    const rest = await next(e) // what other mods and Claude Code draw here stays
55    const r = await read($, reading)
56    if (e.props.hasSurvey || (await read($, isHidden)) || !r) return rest
57    const { Box, Text } = $.ui.resolve(e)
58    const inner = e.props.bodyColumns - 4 // the border and padding take 4 cells
59    if (inner < MIN_WIDTH) return rest
60
61    const head = `${tokens(r.total)} of ${tokens(r.window)}${r.compactsAt ? ` · compacts at ${tokens(r.compactsAt)}` : ''}`
62    const pct = ` ${r.percent}% `
63    const level = r.compactsAt ? r.total / r.compactsAt : r.total / r.window
64    return (
65      <Box flexDirection="column">
66        <Box flexDirection="column" borderStyle="round" borderColor="inactive" paddingX={1}>
67          <Box flexDirection="row" justifyContent="space-between">
68            <Text wrap="truncate-end">
69              <Text color="#d97757">{'◆ '}</Text>
70              <Text bold>context</Text>
71            </Text>
72            <Text wrap="truncate-start">
73              <Text dimColor>{`${head} `}</Text>
74              <Text bold color="black" backgroundColor={level >= 0.9 ? 'red' : level >= 0.7 ? 'yellow' : 'green'}>{pct}</Text>
75            </Text>
76          </Box>
77          <Text>
78            {cells(r, inner).map(c => (
79              <Text color={c.color}>{c.text}</Text>
80            ))}
81          </Text>
82          {legend(r, inner).map(line => (
83            <Text wrap="truncate-end">
84              {line.map((s, i) => (
85                <Text>
86                  {i > 0 && <Text>{SPLIT}</Text>}
87                  <Text color={s.color}>{s.kind === 'used' ? '■ ' : `${GLYPH[s.kind]} `}</Text>
88                  <Text dimColor={s.kind !== 'used'}>{`${s.name} `}</Text>
89                  <Text bold={s.kind === 'used'}>{tokens(s.tokens)}</Text>
90                  {s.kind === 'used' && <Text dimColor>{` ${share(s.tokens, r.window)}`}</Text>}
91                </Text>
92              ))}
93            </Text>
94          ))}
95        </Box>
96        {rest}
97      </Box>
98    )
99  })
100}
101
102// Asks the engine for /context's breakdown, estimated locally (no token-count calls).
103async function refresh($: EngineInterface) {
104  if (await read($, isHidden)) return
105  const usage = await $.session.usage({ breakdown: 'summary' })
106  const b = usage.context.breakdown
107  if (!b || !(b.rawMaxTokens > 0)) return // no window to measure against
108  await update($, reading, () => toReading(b))
109}
110
111export function toReading(b: {
112  categories: { name: string; tokens: number; color: string; kind: string }[]
113  totalTokens: number
114  rawMaxTokens: number
115  percentage: number
116  autoCompactThreshold?: number
117  isAutoCompactEnabled: boolean
118}): Reading {
119  const slices: Slice[] = b.categories
120    .filter(c => c.kind !== 'deferred' && c.tokens > 0)
121    .map(c => ({ name: c.name.toLowerCase(), tokens: c.tokens, color: c.color, kind: c.kind as Slice['kind'] }))
122  const order = { used: 0, free: 1, buffer: 2 }
123  slices.sort((x, y) => order[x.kind] - order[y.kind]) // stable: used rows keep /context's order
124  let next = 0
125  for (const s of slices) {
126    s.color = s.kind === 'free' ? FREE : s.kind === 'buffer' ? BUFFER : s.name === 'messages' ? MESSAGES : PALETTE[next++ % PALETTE.length]!
127  }
128  return {
129    slices,
130    total: b.totalTokens,
131    window: b.rawMaxTokens,
132    percent: b.percentage,
133    compactsAt: b.isAutoCompactEnabled ? b.autoCompactThreshold : undefined,
134  }
135}
136
137// The bar as runs of cells: each slice gets its share of `width`, a used one at least one cell.
138export function cells(r: Reading, width: number) {
139  const sizes = r.slices.map(s => Math.max(s.kind === 'used' ? 1 : 0, Math.round((s.tokens / r.window) * width)))
140  // Rounding leaves the sum a few cells off: free space takes the difference first, then the largest slices.
141  let diff = width - sizes.reduce((a, n) => a + n, 0)
142  const isUsed = (i: number) => r.slices[i]!.kind === 'used'
143  const order = r.slices.map((_, i) => i).sort((a, b) => Number(isUsed(a)) - Number(isUsed(b)) || sizes[b]! - sizes[a]!)
144  for (const i of order) {
145    if (diff === 0) break
146    const size = Math.max(isUsed(i) ? 1 : 0, sizes[i]! + diff)
147    diff -= size - sizes[i]!
148    sizes[i] = size
149  }
150  return r.slices.map((s, i) => ({ color: s.color, kind: s.kind, text: GLYPH[s.kind].repeat(sizes[i]!) })).filter(c => c.text !== '')
151}
152
153// The legend, packed into lines no wider than `width`.
154export function legend(r: Reading, width: number) {
155  const lines: Slice[][] = [[]]
156  let used = 0
157  for (const s of r.slices) {
158    const size = 2 + s.name.length + 1 + tokens(s.tokens).length + (s.kind === 'used' ? 1 + share(s.tokens, r.window).length : 0)
159    const line = lines.at(-1)!
160    if (line.length > 0 && used + SPLIT.length + size > width) {
161      lines.push([s])
162      used = size
163    } else {
164      used += (line.length > 0 ? SPLIT.length : 0) + size
165      line.push(s)
166    }
167  }
168  return lines.filter(l => l.length > 0)
169}
170
171export function tokens(n: number) {
172  if (n >= 1_000_000) return `${+(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`
173  if (n >= 10_000) return `${Math.round(n / 1000)}k`
174  if (n >= 1000) return `${+(n / 1000).toFixed(1)}k`
175  return String(n)
176}
177
178export function share(n: number, window: number) {
179  const p = (n / window) * 100
180  if (p > 0 && p < 0.1) return '<0.1%'
181  return `${p >= 10 ? Math.round(p) : +p.toFixed(1)}%`
182}
183
types/index.d.ts 16 lines
1export type Slice = { name: string; tokens: number; color: string; kind: 'used' | 'free' | 'buffer' }
2
3export type Reading = {
4  slices: Slice[]
5  total: number // tokens in use
6  window: number // the window measured against
7  percent: number
8  compactsAt?: number // where auto-compaction runs, when it is on
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    'context-bar': { reading: Reading | null; isHidden: boolean }
14  }
15}
16