SLOPSHOPPER

context-band

Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

newbandcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-band
› 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-band ⎿ context-band: Context band hidden. Run /context-band to show it again. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

context-band

Leia em português

A thin bar above the box where you type that shows how full Claude's memory of the conversation is.

What is a mod?

Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.

What it does

Claude can only keep a limited amount of text in mind at once. That limit is called the context window. When it fills up, Claude Code compacts the conversation (it summarizes older parts to make room), and some detail can be lost. This mod shows, all the time, how much of that space is in use, so you know when it is a good moment to start a fresh conversation or wrap up a task.

What you see

  • A line above the prompt with the percentage in use, the amount of text in tokens (a token is roughly a short word or a piece of a word), how much the last turn added, and a word that sums it up:
  • "roomy" (green, under 50%), "filling" (yellow, 50 to 74%), "tight" (yellow, 75 to 89%), "compact soon" (red, 90% or more).
  • A "hide" button at the end of the line.

How to use it

  1. Install the mod. The bar appears on its own after Claude's first answer.
  2. To hide it, press the "hide" button at the end of the bar.
  3. To show it again, type /context-band.

Keyboard shortcuts

KeyWhat it does
Tab then EnterReach the "hide" button and press it

Commands

TypeWhat it does
/context-bandShow the bar again, or hide it if it is showing

Settings

Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.

SettingWhat it meansDefault
language (Language)Language of the text the mod shows: en (English) or pt-BR (Brazilian Portuguese).en

Good to know

  • Before the first answer there is no number to show yet, so the bar says it is waiting.
  • On narrow windows the bar shortens its text so it fits on one line.

Install

You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.

  1. Copy this folder anywhere on your computer.
  2. Inside the copy, rename the folder _claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.
  3. Start Claude Code with the mod:
claude --plugin-dir path/to/context-band

To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").

Check it

From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 4 automated tests of this mod:

node scripts/check-mod.mjs examples/context-band

The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.

For developers: the examples table lists the hooks and API calls this mod demonstrates.

Source 3 files
hooks/register.tsx 97 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Reading } from '../types'
5import { strings } from './i18n'
6import type { Strings } from './i18n'
7
8// Data source: $.session.usage().context — the same figures the status line shows.
9// No estimate, no made-up number: when the call gives nothing, the band shows nothing.
10const HISTORY = 12
11const BARS = '▁▂▃▄▅▆▇█'
12
13const readings = atom({ plugin: 'context-band', key: 'readings' } as const, [])
14const isHidden = atom({ plugin: 'context-band', key: 'isHidden' } as const, false)
15
16function level(percent: number, t: Strings): { color: string; word: string } {
17  if (percent < 50) return { color: 'success', word: t.roomy }
18  if (percent < 75) return { color: 'warning', word: t.filling }
19  if (percent < 90) return { color: 'warning', word: t.tight }
20  return { color: 'error', word: t.compactSoon }
21}
22
23function short(n: number): string {
24  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
25  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
26  return String(n)
27}
28
29function chart(list: Reading[]): string {
30  const top = Math.max(...list.map(r => r.tokens), 1)
31  return list.map(r => BARS[Math.min(7, Math.floor((r.tokens / top) * 7))] ?? BARS[0]).join('')
32}
33
34async function takeReading($: EngineInterface) {
35  try {
36    const { context } = await $.session.usage()
37    if (!context.window || context.tokens === undefined) return
38    const percent = Math.round(context.percent ?? (context.tokens / context.window) * 100)
39    const reading: Reading = { tokens: context.tokens, window: context.window, percent }
40    // The reading before the first response is 0; drop zeros once real ones arrive.
41    await update($, readings, list => [...list.filter(r => r.tokens > 0), reading].slice(-HISTORY))
42  } catch {
43    // No reading this time; the band keeps the last one.
44  }
45}
46
47export const register: Register = (on, options) => {
48  const t = strings(options.language)
49
50  on('session.start', async ($, e, next) => {
51    const result = await next(e)
52    await takeReading($)
53    try {
54      // The way back after "hide": every control needs an undo.
55      await $.command.register({ name: 'context-band', description: t.commandDescription, immediate: true })
56    } catch {
57      // Name taken: the band still works, only the toggle is missing.
58    }
59    return result
60  })
61
62  on('command.run', { command: 'context-band' }, async $ => {
63    const hidden = await read($, isHidden)
64    await update($, isHidden, () => !hidden)
65    return { text: hidden ? t.shown : t.hidden }
66  })
67
68  on('turn.complete', async ($, e, next) => {
69    const result = await next(e)
70    if (e.agentId === undefined) await takeReading($) // subagent turns use their own window
71    return result
72  })
73
74  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
75    const list = await read($, readings)
76    const now = list[list.length - 1]
77    if (e.props.hasSurvey || now === undefined || (await read($, isHidden))) return next(e)
78
79    const { Box, Button, Text } = $.ui.resolve(e)
80    const lv = level(now.percent, t)
81    const prev = list[list.length - 2]
82    const delta = prev === undefined ? 0 : now.tokens - prev.tokens
83    const isWide = e.props.bodyColumns >= 70
84
85    return (
86      <Box gap={1} paddingX={1}>
87        <Text color={lv.color} bold>● {now.percent}%</Text>
88        <Text dimColor>{t.context} · {lv.word}</Text>
89        <Text dimColor>{short(now.tokens)} / {short(now.window)}</Text>
90        {isWide && list.length > 1 && <Text color="claude">{chart(list)}</Text>}
91        {isWide && delta !== 0 && <Text dimColor>{delta > 0 ? `▲ +${short(delta)}` : `▼ ${short(-delta)}`} {t.lastTurn}</Text>}
92        <Button key="hide" label={t.hide} plain dimColor onPress={() => update($, isHidden, () => true)} />
93      </Box>
94    )
95  })
96}
97
hooks/i18n.ts 38 lines
1// The text this mod shows, per language. Pure: no $ calls, so tests can cover it directly.
2// Anything other than a known language falls back to English.
3
4export type Language = 'en' | 'pt-BR'
5
6const en = {
7  roomy: 'roomy',
8  filling: 'filling',
9  tight: 'tight',
10  compactSoon: 'compact soon',
11  context: 'context',
12  lastTurn: 'last turn',
13  hide: 'hide (/context-band)',
14  commandDescription: 'Show or hide the context band above the prompt',
15  shown: 'Context band shown.',
16  hidden: 'Context band hidden. Run /context-band to show it again.',
17}
18
19const ptBR: typeof en = {
20  roomy: 'folgado',
21  filling: 'enchendo',
22  tight: 'apertado',
23  compactSoon: 'compactar logo',
24  context: 'contexto',
25  lastTurn: 'no último turno',
26  hide: 'ocultar (/context-band)',
27  commandDescription: 'Mostra ou oculta a faixa de contexto acima do prompt',
28  shown: 'Faixa de contexto visível.',
29  hidden: 'Faixa de contexto oculta. Use /context-band para mostrar de novo.',
30}
31
32export type Strings = typeof en
33const TABLE: Record<Language, Strings> = { en, 'pt-BR': ptBR }
34
35export function strings(language: unknown): Strings {
36  return language === 'pt-BR' ? TABLE['pt-BR'] : TABLE.en
37}
38
types/index.d.ts 8 lines
1export type Reading = { tokens: number; window: number; percent: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'context-band': { readings: Reading[]; isHidden: boolean }
6  }
7}
8