Live context-window bar above the prompt, split by category; /context-bar toggles it

A Claude Code mod that shows a live context-window bar above the prompt.
/context-bar toggles itData comes from $.session.usage({ breakdown: 'summary' }), the same numbers /context reports.
Needs a Claude Code build with function-hook mods (developed on 2.1.289).
git clone https://github.com/Olympus-Work/context-bar ~/Gits/context-bar
claude --plugin-dir ~/Gits/context-bar
To load it in every session, set the CLAUDE_CODE_PLUGIN_DIRS environment variable to the cloned folder.
.claude-plugin/plugin.json — manifesthooks/hooks.json — points at the modulehooks/register.tsx — the command, the timer and the render hookCategories are matched by the names Claude Code reports (e.g. "MCP tools"). If a future release renames one, its legend entry will read 0 until the matcher in register.tsx is updated.
MIT, see LICENSE.
hooks/register.tsx 97 lines1import type { Register } from 'claude-code'
2
3const KEYS = [
4 { id: 'system', label: 'system prompt', color: '#7aa2f7', match: (n: string) => n.includes('system prompt') },
5 { id: 'tools', label: 'tools', color: '#73daca', match: (n: string) => n.includes('system tools') || n === 'tools' },
6 { id: 'mcp', label: 'mcp tools', color: '#bb9af7', match: (n: string) => n.includes('mcp') },
7 { id: 'agents', label: 'agents', color: '#9ece6a', match: (n: string) => n.includes('agent') },
8 { id: 'memory', label: 'memory files', color: '#e0af68', match: (n: string) => n.includes('memory') },
9 { id: 'skills', label: 'skills', color: '#f7a8c8', match: (n: string) => n.includes('skill') },
10 { id: 'messages', label: 'messages', color: '#e07b5a', match: (n: string) => n.includes('message') },
11] as const
12
13const FREE = '#3b4261'
14
15const fmt = (n: number) =>
16 n >= 1_000_000 ? `${+(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${+(n / 1000).toFixed(n >= 100_000 ? 0 : 1)}k` : `${n}`
17
18const pct = (n: number, of: number) => {
19 const p = (n / of) * 100
20 return p > 0 && p < 0.1 ? '<0.1%' : `${p < 10 ? p.toFixed(1) : Math.round(p)}%`
21}
22
23export const register: Register = on => {
24 let isOn = true
25
26 on('session.start', async ($, e, next) => {
27 await $.command.register({
28 name: 'context-bar',
29 description: 'Toggle the live context-window bar above the prompt',
30 })
31 $.clock.every(2000, () => $.ui.invalidate('ui.render'))
32
33 return next(e)
34 })
35
36 on('command.run', { command: 'context-bar' }, async $ => {
37 isOn = !isOn
38 $.ui.invalidate('ui.render')
39
40 return { text: isOn ? 'Context bar shown.' : 'Context bar hidden.' }
41 })
42
43 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44 if (!isOn) return next(e)
45 const { Box, Text } = $.ui.resolve(e)
46 const { context } = await $.session.usage({ breakdown: 'summary' })
47 const b = context.breakdown
48 if (!b) return <Text dimColor>No context data yet.</Text>
49
50 const total = b.rawMaxTokens
51 const sums: Record<string, number> = {}
52 for (const c of b.categories) {
53 if (c.kind !== 'used') continue
54 const k = KEYS.find(key => key.match(c.name.toLowerCase()))
55 if (k) sums[k.id] = (sums[k.id] ?? 0) + c.tokens
56 }
57 const used = b.totalTokens
58 const free = Math.max(0, total - used)
59 const width = Math.max(20, (e.viewport?.columns ?? 80) - 6)
60 const cells = (n: number) => Math.round((n / total) * width)
61 const bar = KEYS.map(k => ({ color: k.color, n: cells(sums[k.id] ?? 0) })).filter(s => s.n > 0)
62 const filled = bar.reduce((a, s) => a + s.n, 0)
63 const percent = Math.round(b.percentage)
64 const badge = percent >= 80 ? '#f7768e' : percent >= 60 ? '#e0af68' : '#7ec97e'
65 const compactAt = b.autoCompactThreshold
66
67 return (
68 <Box flexDirection="column" borderStyle="round" borderColor="#3b4261" paddingX={1}>
69 <Box justifyContent="space-between">
70 <Text bold color="#e07b5a">◆ context</Text>
71 <Text>
72 <Text bold>{fmt(used)}</Text>
73 <Text dimColor> of {fmt(total)}</Text>
74 {compactAt ? <Text dimColor> · compacts at {fmt(compactAt)} </Text> : <Text> </Text>}
75 <Text bold color="#1a1b26" backgroundColor={badge}> {percent}% </Text>
76 </Text>
77 </Box>
78 <Text>
79 {bar.map(s => <Text backgroundColor={s.color}>{' '.repeat(s.n)}</Text>)}
80 <Text backgroundColor={FREE}>{' '.repeat(Math.max(0, width - filled))}</Text>
81 </Text>
82 <Box flexWrap="wrap" columnGap={3}>
83 {KEYS.map(k => (
84 <Text>
85 <Text backgroundColor={k.color}> </Text> {k.label} {fmt(sums[k.id] ?? 0)}{' '}
86 <Text dimColor>{pct(sums[k.id] ?? 0, total)}</Text>
87 </Text>
88 ))}
89 <Text>
90 <Text backgroundColor={FREE}> </Text> free {fmt(free)}
91 </Text>
92 </Box>
93 </Box>
94 )
95 })
96}
97