Draws the context window as a stacked bar above the prompt, one color per /context category. Toggle with /context-bar.

A Claude Code mod that draws your context window as one stacked bar above the prompt, split into the same categories and colors as /context.
████████████▓▓▓▓▓▓███████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒
48.2k/200k 24% ■ System prompt 3.1k ■ System tools 12.4k ■ Memory files 2.0k ■ Messages 30.7k
░ is free space and ▒ is the autocompact buffer.The bar updates after every turn, and after /clear, /resume, /branch and compaction.
| Command | What it does | | :- | :- | | /context-bar | Hide the bar, or show it again | | /context-bar on | Show the bar | | /context-bar off | Hide the bar |
The command works while Claude is busy, and your choice is kept across sessions.
Mods need Claude Code v2.1.287 or later.
git clone https://github.com/Nooralwachi/context-bar.git
claude --plugin-dir ./context-bar
The bar appears after Claude's first reply. To check that the mod loaded, run /plugin; it should say 1 mod active · context-bar.
To load it in every session, including the Code tab of the Claude desktop app, add its absolute path to CLAUDE_CODE_PLUGIN_DIRS in ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/absolute/path/to/context-bar"
}
}
After each turn the mod calls $.session.usage({ breakdown: 'summary' }), which returns the same per-category breakdown /context shows. The summary mode estimates locally and sends no token-count requests, so the bar costs no API calls. The counts are estimates, so the total can differ slightly from the status line's percentage; run /context for exact numbers.
Each category gets a share of the band's width in proportion to its tokens, with at least one cell so small categories stay visible. Deferred tool schemas sit outside the window and are left out, as in /context's grid.
hooks/register.js 153 lines1// context-bar: the context window as one stacked bar above the prompt,
2// split into the same categories and theme colors /context uses.
3
4// Whether the bar shows; saved in $.store so the toggle survives restarts
5let isOn = true
6// The last breakdown from $.session.usage, or null before the first one
7let breakdown = null
8// True while a breakdown request is in flight, so bursts fold into one
9let isMeasuring = false
10
11// Glyph per row kind: solid for what's used, light shade for free space,
12// medium shade for the autocompact buffer
13const GLYPH = { used: '█', free: '░', buffer: '▒' }
14
15// 12345 -> "12.3k", 1234567 -> "1.2M"
16function fmt(n) {
17 if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
18 if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'k'
19 return String(n)
20}
21
22// Split `width` cells across rows by tokens, largest remainder first.
23// Every row with tokens gets at least one cell, so small categories stay visible.
24function allocate(rows, width) {
25 const total = rows.reduce((sum, r) => sum + r.tokens, 0)
26 if (total <= 0 || width <= 0) return rows.map(() => 0)
27 const exact = rows.map((r) => (r.tokens / total) * width)
28 const cells = exact.map((x, i) => (rows[i].tokens > 0 ? Math.max(1, Math.floor(x)) : 0))
29 let left = width - cells.reduce((a, b) => a + b, 0)
30 const order = exact.map((x, i) => i).sort((a, b) => (exact[b] % 1) - (exact[a] % 1))
31 for (let k = 0; left > 0 && k < order.length * 2; k++) {
32 const i = order[k % order.length]
33 if (rows[i].tokens > 0) { cells[i] += 1; left -= 1 }
34 }
35 // The minimum of one cell can overshoot on a narrow band: take back from the widest
36 while (left < 0) {
37 const widest = cells.indexOf(Math.max(...cells))
38 if (cells[widest] <= 1) break
39 cells[widest] -= 1
40 left += 1
41 }
42 return cells
43}
44
45// Ask Claude Code for the /context breakdown. 'summary' estimates locally and
46// sends no token-count requests, so it's free to run after every turn.
47async function measure($) {
48 if (!isOn || isMeasuring) return
49 isMeasuring = true
50 try {
51 const usage = await $.session.usage({ breakdown: 'summary' })
52 breakdown = usage.context.breakdown ?? null
53 $.ui.invalidate('ui.render')
54 } finally {
55 isMeasuring = false
56 }
57}
58
59export function register(on) {
60 on('session.start', async ($, e, next) => {
61 const saved = await $.store.get('isOn')
62 if (typeof saved === 'boolean') isOn = saved
63 await measure($)
64 await $.command.register({
65 name: 'context-bar',
66 description: 'Show or hide the context bar above the prompt',
67 argumentHint: '[on|off]',
68 immediate: true,
69 })
70 return next(e)
71 })
72
73 // After each turn, and when the figures move
74 on('session.measure', async ($, e, next) => {
75 await measure($)
76 return next(e)
77 })
78
79 // After /clear, /resume, /branch and compaction the window starts over
80 on('classic.SessionStart', async ($, e, next) => {
81 breakdown = null
82 await measure($)
83 return next(e)
84 })
85
86 on('command.run', { command: 'context-bar' }, async ($, e) => {
87 const arg = (e.args || '').trim().toLowerCase()
88 isOn = arg === 'on' ? true : arg === 'off' ? false : !isOn
89 await $.store.set('isOn', isOn)
90 if (isOn) await measure($)
91 $.ui.invalidate('ui.render')
92 $.ui.toast('Context bar ' + (isOn ? 'on' : 'off'))
93 return {}
94 })
95
96 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
97 // Hidden, nothing measured yet, or a survey wants the band: pass
98 if (!isOn || !breakdown || e.props.hasSurvey) return next(e)
99 const { Box, Text } = $.ui.resolve(e)
100
101 // Deferred tool schemas sit outside the window, as in /context's grid
102 const rows = breakdown.categories.filter((c) => c.kind !== 'deferred')
103 const width = Math.max(10, e.props.bodyColumns)
104 const cells = allocate(rows, width)
105
106 const bar = Box({
107 key: 'bar',
108 flexDirection: 'row',
109 children: rows
110 .map((r, i) =>
111 cells[i] > 0
112 ? Text({ key: 'seg-' + i, color: r.color, children: [(GLYPH[r.kind] ?? '█').repeat(cells[i])] })
113 : null,
114 )
115 .filter(Boolean),
116 })
117
118 // One line: the total, then a swatch, name and size per category
119 const used = rows.filter((r) => r.kind === 'used')
120 const legend = Box({
121 key: 'legend',
122 flexDirection: 'row',
123 flexWrap: 'wrap',
124 columnGap: 2,
125 children: [
126 Text({
127 key: 'total',
128 bold: true,
129 wrap: 'truncate',
130 children: [fmt(breakdown.totalTokens) + '/' + fmt(breakdown.rawMaxTokens) + ' ' + breakdown.percentage + '%'],
131 }),
132 ...used.map((r, i) =>
133 Box({
134 key: 'leg-' + i,
135 flexDirection: 'row',
136 flexShrink: 0,
137 children: [
138 Text({ key: 'sw', color: r.color, children: ['■ '] }),
139 Text({ key: 'nm', dimColor: true, wrap: 'truncate', children: [r.name + ' ' + fmt(r.tokens)] }),
140 ],
141 }),
142 ),
143 ],
144 })
145
146 const mine = Box({ key: 'context-bar', flexDirection: 'column', children: e.props.maxRows >= 2 ? [bar, legend] : [bar] })
147
148 // Keep whatever mods after this one draw in the band, under the bar
149 const theirs = await next(e)
150 return theirs ? Box({ flexDirection: 'column', children: [mine, theirs] }) : mine
151 })
152}
153