A live context-window usage bar above the Claude Code prompt

A live context-window usage bar above the Claude Code prompt.
ctx ████████████████░░░░░░░░░░░░░░░ 52% · 104k / 200k [ Details ]
It updates after every turn and changes colour as the window fills: green below 70%, yellow from 70%, red from 85%. It reads the same figures as the status line, so it makes no extra API calls.
| Command | What it does |
|---|---|
/context-bar | Show or hide the bar |
/context-details | Show or hide the /context-style breakdown in a side pane (also the bar's Details button) |
You're asked for these when installing; change them later in /config (or /plugin configure context-bar@claude-context-bar).
show | The bar at session start |
|---|---|
on-demand (default) | Hidden until /context-bar |
always | Shown; /context-bar hides it for the session |
remember | As /context-bar last left it, across sessions |
In a Claude Code terminal session:
/plugin install context-bar --marketplace imranbarbhuiya/claude-context-bar
Answer y to add the marketplace, then pick a scope (user scope loads it in every session).
The claude-code module is provided by Claude Code at runtime, not npm. Its type declarations are generated into .claude-plugin/types/ (gitignored) whenever Claude Code loads this folder directly, so run claude --plugin-dir . once after cloning and after upgrading Claude Code to get editor types.
claude --plugin-dir . # run Claude Code with the local copy loaded
claude plugin validate . # check the manifest and hooks module
claude plugin test . # run the tests in tests/
MIT
hooks/register.tsx 199 lines1import { atom, read, update } from 'claude-code'
2import type { ContextCategoryKind, EngineInterface, Register } from 'claude-code'
3
4import type { ContextFill } from '../types'
5
6const PANE = 'context-details'
7const fill = atom({ plugin: 'context-bar', key: 'fill' } as const, null)
8const isVisible = atom({ plugin: 'context-bar', key: 'isVisible' } as const, false)
9
10const formatTokens = (n: number) =>
11 n >= 1_000_000 ? `${+(n / 1_000_000).toFixed(1)}m` : n >= 1000 ? `${+(n / 1000).toFixed(1)}k` : `${n}`
12
13const colorFor = (percent: number) => (percent >= 85 ? 'error' : percent >= 70 ? 'warning' : 'success')
14
15const squareFor = (kind: ContextCategoryKind | undefined, fullness: number) =>
16 kind === 'free' ? '⛶' : kind === 'buffer' ? '⛝' : fullness >= 0.7 ? '⛁' : '⛀'
17
18const percentOf = (tokens: number, window: number) => `${+((tokens / window) * 100).toFixed(1)}%`
19
20async function toggleDetails($: EngineInterface) {
21 const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
22 if (isOpen) {
23 await $.ui.close({ id: PANE })
24 } else {
25 await $.ui.open({ id: PANE, title: 'Context Usage', focus: true, closeOnEscape: true })
26 }
27 return !isOpen
28}
29
30async function store($: EngineInterface, { tokens, window, percent }: ContextFill) {
31 await update($, fill, () => ({ tokens, window, percent }))
32}
33
34export const register: Register = (on, options) => {
35 const show = options.show
36
37 on('session.start', async ($, e, next) => {
38 const result = await next(e)
39 await $.command.register({ name: 'context-bar', description: 'Show or hide the context usage bar' })
40 await $.command.register({ name: 'context-details', description: 'Show or hide the context usage breakdown' })
41 const isShown = show === 'always' || (show === 'remember' && (await $.store.get('isVisible')) === true)
42 await update($, isVisible, () => isShown)
43 await store($, (await $.session.usage()).context)
44 return result
45 })
46
47 on('session.measure', async ($, e, next) => {
48 if (e.changed.includes('context')) {
49 await store($, e.context)
50 }
51 return next(e)
52 })
53
54 on('session.compact', async ($, e, next) => {
55 const result = await next(e)
56 await update($, fill, () => null).catch(() => {})
57 return result
58 })
59
60 on('command.run', { command: 'context-bar' }, async $ => {
61 const isShown = !(await read($, isVisible))
62 await update($, isVisible, () => isShown)
63 if (show === 'remember') {
64 await $.store.set('isVisible', isShown)
65 }
66 return { text: `Context bar ${isShown ? 'shown' : 'hidden'}.` }
67 })
68
69 on('command.run', { command: 'context-details' }, async $ => {
70 const isOpen = await toggleDetails($)
71 return { text: `Context usage pane ${isOpen ? 'opened' : 'closed'}.` }
72 })
73
74 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
75 const current = await read($, fill)
76 if (e.props.hasSurvey || !current || !(await read($, isVisible))) {
77 return next(e)
78 }
79
80 const { Box, Button, Text } = $.ui.resolve(e)
81 const percent = current.percent ?? 0
82 const width = Math.max(10, Math.min(40, e.props.bodyColumns - 42))
83 const filled = Math.min(width, Math.round((percent / 100) * width))
84 const tokens = current.tokens === undefined ? '—' : formatTokens(current.tokens)
85
86 return (
87 <Box>
88 <Text dimColor>ctx </Text>
89 <Text color={colorFor(percent)}>{'█'.repeat(filled)}</Text>
90 <Text dimColor>{'░'.repeat(width - filled)}</Text>
91 <Text color={colorFor(percent)}> {percent}%</Text>
92 <Text dimColor>
93 {' '}
94 · {tokens} / {formatTokens(current.window)}{' '}
95 </Text>
96 <Button key="details" label="Details" hotkey="d" dimColor onPress={() => toggleDetails($)} />
97 </Box>
98 )
99 })
100
101 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
102 const { Box, Text } = $.ui.resolve(e)
103 await read($, fill)
104 const { breakdown } = (await $.session.usage({ breakdown: 'summary', columns: e.props.bodyColumns })).context
105
106 if (!breakdown) {
107 return <Text dimColor>No context usage yet.</Text>
108 }
109
110 const kindOf = new Map(breakdown.categories.map(c => [c.name, c.kind]))
111 const window = breakdown.rawMaxTokens
112 const gridWidth = (breakdown.gridRows[0]?.length ?? 10) * 2
113 const isSideBySide = e.props.bodyColumns >= gridWidth + 44
114 const loadedMcp = breakdown.mcpTools.filter(tool => tool.isLoaded)
115 const sum = (rows: readonly { tokens: number }[]) => rows.reduce((total, row) => total + row.tokens, 0)
116
117 return (
118 <Box flexDirection="column">
119 <Box flexDirection={isSideBySide ? 'row' : 'column'}>
120 <Box flexDirection="column" marginRight={3} marginBottom={isSideBySide ? 0 : 1}>
121 {breakdown.gridRows.map(row => (
122 <Text>
123 {row.map(square => (
124 <Text color={square.color}>{squareFor(kindOf.get(square.categoryName), square.squareFullness)} </Text>
125 ))}
126 </Text>
127 ))}
128 </Box>
129 <Box flexDirection="column">
130 <Text bold>{breakdown.model}</Text>
131 <Text dimColor>
132 {formatTokens(breakdown.totalTokens)}/{formatTokens(window)} tokens ({breakdown.percentage}%)
133 </Text>
134 <Text> </Text>
135 <Text dimColor italic>
136 Estimated usage by category
137 </Text>
138 {breakdown.categories
139 .filter(c => c.tokens > 0 && c.kind !== 'deferred')
140 .map(c => (
141 <Text>
142 <Text color={c.color}>{squareFor(c.kind, 1)} </Text>
143 <Text bold>{c.name}: </Text>
144 <Text dimColor>
145 {formatTokens(c.tokens)}
146 {c.kind === 'free' ? '' : ' tokens'} ({percentOf(c.tokens, window)})
147 </Text>
148 </Text>
149 ))}
150 </Box>
151 </Box>
152 <Text> </Text>
153 {breakdown.isAutoCompactEnabled && breakdown.autoCompactThreshold !== undefined && (
154 <Text>
155 <Text bold>Auto-compact at: </Text>
156 <Text dimColor>{formatTokens(breakdown.autoCompactThreshold)} tokens</Text>
157 </Text>
158 )}
159 {breakdown.mcpTools.length > 0 && (
160 <Text>
161 <Text bold>MCP tools</Text>
162 <Text dimColor>
163 {' '}
164 · {breakdown.mcpTools.length} tools, {loadedMcp.length} loaded · {formatTokens(sum(loadedMcp))} tokens
165 </Text>
166 </Text>
167 )}
168 {breakdown.memoryFiles.length > 0 && (
169 <Text>
170 <Text bold>Memory files</Text>
171 <Text dimColor>
172 {' '}
173 · {breakdown.memoryFiles.length} files · {formatTokens(sum(breakdown.memoryFiles))} tokens
174 </Text>
175 </Text>
176 )}
177 {breakdown.skills && (
178 <Text>
179 <Text bold>Skills</Text>
180 <Text dimColor>
181 {' '}
182 · {breakdown.skills.includedSkills} skills · {formatTokens(breakdown.skills.tokens)} tokens
183 </Text>
184 </Text>
185 )}
186 {breakdown.agents.length > 0 && (
187 <Text>
188 <Text bold>Agents</Text>
189 <Text dimColor>
190 {' '}
191 · {breakdown.agents.length} agents · {formatTokens(sum(breakdown.agents))} tokens
192 </Text>
193 </Text>
194 )}
195 </Box>
196 )
197 })
198}
199types/index.d.ts 8 lines1export type ContextFill = { tokens?: number; window: number; percent?: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'context-bar': { fill: ContextFill | null; isVisible: boolean }
6 }
7}
8