SLOPSHOPPER

context-bar

Live context-window bar above the prompt: fill, compaction point and what fills it

newbandcommand
v0.1.0no licenseupdated 2026-10-10ianwilson97/claude-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. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

ianwilson-mods

Claude Code mods (function-hook plugins). Each folder is one plugin; .claude-plugin/marketplace.json makes this repo a marketplace.

ModWhat it does
cheap-worker/worker spins up Claude Code workers from a picker; /workers lists, messages and closes them. Team mode lets workers find each other (ListAgents) and talk (SendMessage).
agent-progress/progress: animated progress bar per agent (main, subagents, workers) from their task lists.
agent-map/map: project file tree; red 🔨 = an agent is editing it, green ✓ = done, ready for review.
context-barContext-window breakdown above the prompt.
usage-meter5-hour / weekly usage in the footer.
quick-settings⚙ footer button for model and effort.
skins/skins: colour themes (Tokyo Night, Dracula, Gruvbox, …).
cw-pane, driver-seat-guard, evidence-bandFor the driver-seat workflow; skip them elsewhere.

Install

/plugin marketplace add ianwilson97/claude-mods
/plugin install cheap-worker@ianwilson-mods
/plugin install agent-progress@ianwilson-mods
/plugin install agent-map@ianwilson-mods

On the machine where this folder is the live copy (loaded through CLAUDE_CODE_PLUGIN_DIRS), don't also install from the marketplace: each mod would load twice.

cheap-worker: two switches

Set per machine in /config (or pluginConfigs.cheap-worker.options in ~/.claude/settings.json):

OptionValues
provideropenrouter (default)Any OpenRouter model; key read from $CW_OPENROUTER_API_KEY (export it in ~/.zshenv). Workers skip user settings, hooks and plugins.
bedrockClaude only (Sonnet / Opus / Haiku). Workers keep your user settings, so the Bedrock env there (CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_PROFILE, …) applies. No call to openrouter.ai.
launchpanes (default)All workers tiled in tmux session cw: tmux new -A -s cw.
tabsEach worker gets a tmux session of its own and opens in a background tab: a cmux tab when Claude Code runs inside cmux, else a Ghostty tab (focus stays where you were).

Work laptop: provider = bedrock, launch = tabs.

Reaching a worker: crew

The mod installs crew into ~/.local/bin (that folder must be on your PATH; it never overwrites a different crew).

crew            # list running workers, pick one by number
crew ab12c      # attach to cw-ab12c

If Ghostty can't be scripted (macOS asks once to let Claude control Ghostty; a managed Mac may refuse), the worker still runs: press CMD+T and run crew. Closing a tab only detaches; the worker keeps running until you close it from /workers.

Needs: tmux; for auto-opened tabs, cmux or Ghostty 1.3+.

First run on a new machine: checks

  1. /plugin lists the mods as enabled. Managed settings can block marketplaces or plugins; if so, nothing loads.
  2. /worker, launch one with task say hi; a Ghostty tab opens (or the toast says CMD+T, then crew …).
  3. Turn on team, launch two workers, then from /workers send one a message: messaging is Claude Code's own (ListAgents / SendMessage) and works under Bedrock unless policy turns it off.
Source 2 files
hooks/register.tsx 138 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ContextView, Slice } from '../types'
5
6const view = atom({ plugin: 'context-bar', key: 'view' } as const, null as ContextView | null)
7const isHidden = atom({ plugin: 'context-bar', key: 'isHidden' } as const, false)
8
9const PALETTE: [RegExp, string][] = [
10  [/system prompt/i, '#7d8cf0'],
11  [/system tools|^tools/i, '#6cc5c9'],
12  [/mcp/i, '#9a7cf5'],
13  [/agent/i, '#9fd17a'],
14  [/memory/i, '#e8c46a'],
15  [/skill/i, '#f09ab8'],
16  [/slash|command/i, '#c9a0e8'],
17  [/message/i, '#e07b5a'],
18]
19const colorFor = (name: string, fallback: string) => PALETTE.find(([re]) => re.test(name))?.[1] ?? fallback
20
21export const k = (n: number) =>
22  n >= 1_000_000 ? `${+(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${+(n / 1000).toFixed(1)}k` : `${n}`
23
24// Splits `width` cells across used slices by share of `max`, largest remainder,
25// every non-empty slice at least one cell so small categories stay visible.
26export function cells(slices: readonly Slice[], max: number, width: number): number[] {
27  const raw = slices.map(s => (s.tokens / Math.max(max, 1)) * width)
28  const out = raw.map(r => (r > 0 ? Math.max(1, Math.floor(r)) : 0))
29  const target = Math.min(width, Math.max(Math.round(raw.reduce((a, b) => a + b, 0)), out.filter(n => n > 0).length))
30  const total = () => out.reduce((a, b) => a + b, 0)
31  while (total() > target) {
32    const i = out.indexOf(Math.max(...out))
33    if ((out[i] ?? 0) <= 1) break
34    out[i] = (out[i] ?? 0) - 1
35  }
36  const order = raw.map((r, i) => [r - Math.floor(r), i] as const).sort((a, b) => b[0] - a[0])
37  for (const [, i] of order) {
38    if (total() >= target) break
39    out[i] = (out[i] ?? 0) + 1
40  }
41  return out.map(n => Math.min(n, width))
42}
43
44export const tone = (pct: number) => (pct >= 85 ? '#e5534b' : pct >= 60 ? '#e8c46a' : '#57ab5a')
45
46async function compute($: EngineInterface): Promise<ContextView | null> {
47  const usage = await $.session.usage({ breakdown: 'summary' }).catch(() => null)
48  const b = usage?.context.breakdown
49  if (!b) return null
50  return {
51    used: b.totalTokens,
52    max: b.rawMaxTokens,
53    compactAt: b.isAutoCompactEnabled ? (b.autoCompactThreshold ?? null) : null,
54    slices: b.categories
55      .filter(c => c.kind === 'used' && c.tokens > 0)
56      .map(c => ({ name: c.name.toLowerCase(), tokens: c.tokens, color: colorFor(c.name, c.color), kind: c.kind })),
57  }
58}
59
60async function measure($: EngineInterface) {
61  const next = await compute($)
62  if (next) await update($, view, () => next)
63}
64
65export const register: Register = on => {
66  on('session.start', async ($, e, next) => {
67    await $.command.register({ name: 'context-bar', description: 'Show or hide the context bar' })
68    void measure($)
69    return next(e)
70  })
71
72  on('session.measure', async ($, e, next) => {
73    if (e.changed.includes('context')) await measure($)
74    return next(e)
75  })
76
77  on('command.run', { command: 'context-bar' }, async $ => {
78    await update($, isHidden, h => !h)
79    const hidden = await read($, isHidden)
80    if (!hidden) await measure($)
81    return { text: hidden ? 'Context bar hidden.' : 'Context bar shown.' }
82  })
83
84  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
85    const below = await next(e)
86    const v = (await read($, view)) ?? (await compute($))
87    if (v === null || e.props.hasSurvey || (await read($, isHidden))) return below
88    const { Box, Text } = $.ui.resolve(e)
89
90    const pct = Math.round((100 * v.used) / Math.max(v.max, 1))
91    const width = Math.max(20, (e.viewport?.columns ?? 100) - 6)
92    const sizes = cells(v.slices, v.max, width)
93    const usedCells = sizes.reduce((a, b) => a + b, 0)
94    const mark = v.compactAt === null ? -1 : Math.min(width - 1, Math.round((v.compactAt / v.max) * width))
95    const free = Array.from({ length: Math.max(0, width - usedCells) }, (_, i) =>
96      usedCells + i === mark ? '│' : '░',
97    ).join('')
98
99    return (
100      <Box flexDirection="column">
101        <Box flexDirection="column" borderStyle="round" borderColor="#3b3f4a" paddingX={1}>
102          <Box justifyContent="space-between">
103            <Text>
104              <Text color="#e07b5a">◆ </Text>
105              <Text bold>context</Text>
106            </Text>
107            <Text>
108              <Text bold>{k(v.used)}</Text>
109              <Text dimColor> of {k(v.max)}{v.compactAt ? ` · compacts at ${k(v.compactAt)}` : ''} </Text>
110              <Text backgroundColor={tone(pct)} color="#111111" bold>{` ${pct}% `}</Text>
111            </Text>
112          </Box>
113          <Text>
114            {v.slices.map((s, i) => (
115              <Text key={`seg-${s.name}`} color={s.color}>{'█'.repeat(sizes[i] ?? 0)}</Text>
116            ))}
117            <Text color="#3b3f4a">{free}</Text>
118          </Text>
119          <Box flexWrap="wrap" columnGap={2}>
120            {v.slices.map(s => (
121              <Text key={`leg-${s.name}`}>
122                <Text color={s.color}>■ </Text>
123                {s.name} <Text bold>{k(s.tokens)}</Text>
124                <Text dimColor> {Math.round((100 * s.tokens) / Math.max(v.max, 1))}%</Text>
125              </Text>
126            ))}
127            <Text>
128              <Text color="#3b3f4a">■ </Text>
129              <Text dimColor>free {k(Math.max(0, v.max - v.used))}</Text>
130            </Text>
131          </Box>
132        </Box>
133        {below}
134      </Box>
135    )
136  })
137}
138
types/index.d.ts 9 lines
1export type Slice = { name: string; tokens: number; color: string; kind: 'used' | 'free' | 'buffer' | 'deferred' }
2export type ContextView = { used: number; max: number; compactAt: number | null; slices: Slice[] }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'context-bar': { view: ContextView | null; isHidden: boolean }
7  }
8}
9