SLOPSHOPPER

context-bar

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

newpanebandcommand
v0.2.0MITupdated 2026-10-07imranbarbhuiya/claude-context-bar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-bar
│ ┃ Context Usage ✕ › fix the failing auth test and add an audit log call │ ┃ No context usage yet. │ ⏺ 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 shown. │ │ ctx ████████████████████░░░░░░░░░░░░░░░░░░░░ 49% · 97.4k / 200k [ Details ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
ctx ████████████████████░░░░░░░░░░░░░░░░░░░░ 49% · 97.4k / 200k [ Details ]
Pane · Context Usage
No context usage yet.
README

claude-context-bar

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.

Usage

CommandWhat it does
/context-barShow or hide the bar
/context-detailsShow or hide the /context-style breakdown in a side pane (also the bar's Details button)

Options

You're asked for these when installing; change them later in /config (or /plugin configure context-bar@claude-context-bar).

showThe bar at session start
on-demand (default)Hidden until /context-bar
alwaysShown; /context-bar hides it for the session
rememberAs /context-bar last left it, across sessions

Install

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).

Develop

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/

License

MIT

Source 2 files
hooks/register.tsx 199 lines
1import { 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}
199
types/index.d.ts 8 lines
1export 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