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

Claude Code mods (function-hook plugins). Each folder is one plugin; .claude-plugin/marketplace.json makes this repo a marketplace.
| Mod | What 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-bar | Context-window breakdown above the prompt. |
usage-meter | 5-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-band | For the driver-seat workflow; skip them elsewhere. |
/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.
Set per machine in /config (or pluginConfigs.cheap-worker.options in ~/.claude/settings.json):
| Option | Values | |
|---|---|---|
provider | openrouter (default) | Any OpenRouter model; key read from $CW_OPENROUTER_API_KEY (export it in ~/.zshenv). Workers skip user settings, hooks and plugins. |
bedrock | Claude 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. | |
launch | panes (default) | All workers tiled in tmux session cw: tmux new -A -s cw. |
tabs | Each 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.
crewThe 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+.
/plugin lists the mods as enabled. Managed settings can block marketplaces or plugins; if so, nothing loads./worker, launch one with task say hi; a Ghostty tab opens (or the toast says CMD+T, then crew …)./workers send one a message: messaging is Claude Code's own (ListAgents / SendMessage) and works under Bedrock unless policy turns it off.hooks/register.tsx 138 lines1import { 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}
138types/index.d.ts 9 lines1export 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