Draws the context window as a stacked bar above the prompt, one colour per /context category; toggle with /context-bar

Four Claude Code mods (function-hook plugins) that save tokens and keep you aware of your limits.
| Mod | What it does |
|---|---|
smart-model-router | Picks a heavy or light model for each turn |
usage-bar | Shows 5-hour and weekly rate-limit usage above the prompt |
context-bar | Shows how full the context window is, by category, above the prompt |
research-offloader | Keeps research work out of the main context |
Kaizen is continuous improvement through small changes, each removing one source of waste (muda). These mods apply it to a Claude Code workflow:
| Waste | Mod that removes it |
|---|---|
| The most expensive model doing routine work | smart-model-router |
| Raw web pages filling the main context | research-offloader |
| Hitting a rate limit by surprise | usage-bar (and the router's usage guard) |
| Context filling up unnoticed | context-bar |
None of these is a big idea; each is one small fix. Run the cycle on your own usage:
usage-bar.usageGuard, the model options and the research trigger phrases, then repeat.What you can see, you can improve.
claude --plugin-dir D:\ai\cc-mods\smart-model-router --plugin-dir D:\ai\cc-mods\usage-bar --plugin-dir D:\ai\cc-mods\context-bar --plugin-dir D:\ai\cc-mods\research-offloader
Saving a file reloads the mod in a running session.
/plugin install smart-model-router --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install usage-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install context-bar --marketplace timothylok/ClaudeCodeModsByTimLok
/plugin install research-offloader --marketplace timothylok/ClaudeCodeModsByTimLok
Answer y to add the marketplace, then choose a scope. Each mod is active immediately.
Mods with options show them as rows in /config, or you can set them in settings.json under pluginConfigs.<mod>.options. Changing one reloads the mod.
Chooses a model for every turn and sends the main thread's requests to it. Subagents keep their own model.
How a model is chosen, in order:
heavy or light, use that model.auto mode, a prompt that mentions architecture, designing a system, multi-file work, rewriting the whole thing, deep reasoning, trade-offs, migrations, root cause, or planning, or one longer than 2,500 characters, gets the heavy model. Anything else gets the light model.The status line shows the choice and the reason, e.g. router: claude-opus-5-5 (complex prompt).
| Command | Effect |
|---|---|
/route | Show the current mode |
/route auto | Pick per prompt (default) |
/route heavy | Always use the heavy model |
/route light | Always use the light model |
/route off | Router does nothing; the session's own model is used |
The mode is remembered across sessions.
| Option | Default | Meaning |
|---|---|---|
heavyModel | claude-opus-5-5 | Model for complex turns |
lightModel | claude-sonnet-5-5 | Model for everyday turns |
usageGuard | 85 | Usage % above which the light model is always used |
| Prompt | Model |
|---|---|
Design the architecture for a multi-tenant billing service | heavy |
Plan out a migration from REST to gRPC, with the trade-offs | heavy |
Find the root cause of this flaky test | heavy |
fix the typo in README | light |
write a unit test for parseDate | light |
Switching models starts a new prompt cache, so flipping often costs extra input tokens. Use /route heavy or /route light to pin a model for a stretch of work.
A band above the prompt with a meter for each rate-limit window Claude Code reports:
Usage 5h █████░░░░░ 42% 2h30m week █████████░ 91% 3d
| Command | Effect |
|---|---|
/usage-bar | Hide or show the band (toggle) |
None.
A stacked bar above the prompt showing the context window, one colour per /context category, with a legend of the biggest categories:
████████▒▒▒▒░░░░░░░░░░░░░░░░░░░░ 38% · 76k/200k
■ Messages 41k ■ System tools 18k ■ Memory files 6k
█ is used space, ▒ is the autocompact buffer, ░ is free space.usage-bar and stacks with it. It is hidden while a survey is showing.| Command | Effect |
|---|---|
/context-bar | Hide or show the bar (toggle) |
None.
When you type a research-style prompt, the mod keeps the page fetching and reading out of the main thread, so only a summary enters your context.
What counts as research: prompts containing "research", "look up", "summarize this url/page/article/docs", "compare sources", "read this page/article/docs", "what is the latest", or "search the web". It only reacts to prompts you type yourself.
Two modes, depending on the endpoint option:
{ "query": "<your prompt>" } to it, expects { "summary": "..." } back, and gives Claude that summary as context. If the endpoint fails or returns no summary, it falls back to the subagent mode below.subagentModel.A toast tells you which mode was used.
| Option | Default | Meaning |
|---|---|---|
endpoint | empty | URL of your own research bridge (for example a NotebookLM wrapper). Leave empty to use subagents |
subagentModel | haiku | Model for research subagents: haiku, sonnet or opus |
| Prompt | Result |
|---|---|
Research the best Rust ORMs in 2026 | offloaded |
Can you look up how Vite handles HMR? | offloaded |
summarize this page for me: https://example.com | offloaded |
compare sources on WebGPU support | offloaded |
fix the failing test in auth.ts | untouched |
NotebookLM has no public API, so the endpoint is yours to provide. Any service that takes {query} and returns {summary} works.
usage-bar hid other bands above the promptusage-bar installed, a separate context bar disappeared.ui.render on AbovePrompt). The outermost hook returned its own tree without calling next(e), so the hook beneath it never ran.usage-bar now calls next(e) and stacks what comes back under its own row. context-bar does the same from 0.1.4, so the two show together in either order.ui.render hook on a shared site must await next(e) and include the result in its tree. In tests, add a stand-in ui.render hook beneath the mod so next has something to return.Each mod has hooks/register.ts(x), a manifest in .claude-plugin/plugin.json, and tests in tests/.
claude plugin validate <mod folder>
claude plugin test <mod folder>
hooks/register.tsx 109 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ContextBarSnapshot } from '../types'
5import { formatTokens, glyph, layout } from './layout'
6
7const isOn = atom({ plugin: 'context-bar', key: 'isOn' } as const, true)
8const snapshot = atom({ plugin: 'context-bar', key: 'snapshot' } as const, null)
9
10// "summary" estimates every category locally from the last response's usage,
11// so a refresh sends no token-count requests (the "full" count /context runs
12// does). Deferred tool schemas sit outside the window, as /context leaves
13// them out of its grid.
14async function refresh($: EngineInterface): Promise<void> {
15 const { context } = await $.session.usage({ breakdown: 'summary' })
16 const b = context.breakdown
17 if (!b) return
18 const next: ContextBarSnapshot = {
19 categories: b.categories
20 .filter(c => c.kind !== 'deferred')
21 .map(c => ({ name: c.name, tokens: c.tokens, color: c.color, kind: c.kind as 'used' | 'free' | 'buffer' })),
22 totalTokens: b.totalTokens,
23 maxTokens: b.rawMaxTokens,
24 percentage: b.percentage,
25 }
26 await update($, snapshot, () => next)
27}
28
29export const register: Register = on => {
30 on('session.start', async ($, e, next) => {
31 await $.command.register({
32 name: 'context-bar',
33 description: 'Show or hide the context-window bar above the prompt',
34 immediate: true,
35 })
36 const result = await next(e)
37 await refresh($)
38
39 return result
40 })
41
42 on('turn.complete', async ($, e, next) => {
43 const result = await next(e)
44 await refresh($)
45
46 return result
47 })
48
49 on('session.compact', async ($, e, next) => {
50 const result = await next(e)
51 await refresh($)
52
53 return result
54 })
55
56 on('command.run', { command: 'context-bar' }, async $ => {
57 const shown = await update($, isOn, was => !was)
58 if (shown) await refresh($)
59
60 return { text: shown ? 'Context bar shown.' : 'Context bar hidden.' }
61 })
62
63 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
64 const snap = await read($, snapshot)
65 if (e.props.hasSurvey || !(await read($, isOn)) || !snap) {
66 return next(e)
67 }
68
69 const { Box, Text } = $.ui.resolve(e)
70 const suffix = ` ${snap.percentage}% · ${formatTokens(snap.totalTokens)}/${formatTokens(snap.maxTokens)}`
71 const width = Math.max(10, e.props.bodyColumns - suffix.length - 1)
72 const segments = layout(snap.categories, snap.maxTokens, width)
73 // Legend reads biggest share first; the bar keeps /context's order.
74 const used = snap.categories
75 .filter(c => c.kind === 'used' && c.tokens > 0)
76 .sort((a, b) => b.tokens - a.tokens)
77
78 // Other mods (e.g. usage-bar) draw in this band too; answering without
79 // next() would replace them, so stack whatever sits beneath us.
80 const below = await next(e)
81
82 return (
83 <Box flexDirection="column">
84 <Text wrap="truncate-end">
85 {segments.map(s => (
86 <Text key={s.name} color={s.color} dimColor={s.kind !== 'used'}>
87 {glyph(s.kind).repeat(s.cells)}
88 </Text>
89 ))}
90 <Text dimColor>{suffix}</Text>
91 </Text>
92 <Text wrap="truncate-end">
93 {used.map(c => (
94 <Text key={c.name}>
95 <Text color={c.color}>■</Text>
96 <Text dimColor>
97 {' '}
98 {c.name} {formatTokens(c.tokens)}
99 {' '}
100 </Text>
101 </Text>
102 ))}
103 </Text>
104 {below}
105 </Box>
106 )
107 })
108}
109hooks/layout.ts 53 lines1import type { ContextBarCategory } from '../types'
2
3export type Segment = { name: string; color: string; kind: ContextBarCategory['kind']; cells: number }
4
5const GLYPH: Record<ContextBarCategory['kind'], string> = { used: '█', buffer: '▒', free: '░' }
6
7export const glyph = (kind: ContextBarCategory['kind']) => GLYPH[kind]
8
9/**
10 * Splits `width` cells across the categories in proportion to their tokens
11 * (largest remainder, so the cells always sum to `width`). A used category
12 * with any tokens keeps at least one cell, taken from the widest
13 * non-used segment, so a small row like Memory files never vanishes.
14 */
15export function layout(categories: ContextBarCategory[], maxTokens: number, width: number): Segment[] {
16 const rows = categories.filter(c => c.tokens > 0)
17 const total = Math.max(maxTokens, rows.reduce((sum, c) => sum + c.tokens, 0))
18 if (width <= 0 || total <= 0 || rows.length === 0) return []
19
20 const segs = rows.map(c => {
21 const exact = (c.tokens / total) * width
22 return { name: c.name, color: c.color, kind: c.kind, cells: Math.floor(exact), rest: exact - Math.floor(exact) }
23 })
24 let left = width - segs.reduce((n, s) => n + s.cells, 0)
25 for (const s of [...segs].sort((a, b) => b.rest - a.rest)) {
26 if (left <= 0) break
27 s.cells += 1
28 left -= 1
29 }
30
31 const widest = (pick: (s: (typeof segs)[number]) => boolean) =>
32 segs.filter(s => pick(s) && s.cells > 1).sort((a, b) => b.cells - a.cells)[0]
33 for (const s of segs) {
34 if (s.kind !== 'used' || s.cells > 0) continue
35 const donor = widest(d => d.kind !== 'used') ?? widest(() => true)
36 if (donor) {
37 donor.cells -= 1
38 s.cells = 1
39 }
40 }
41
42 return segs
43 .filter(s => s.cells > 0)
44 .map(({ name, color, kind, cells }) => ({ name, color, kind, cells }))
45}
46
47export function formatTokens(n: number): string {
48 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
49 if (n >= 10_000) return `${Math.round(n / 1000)}k`
50 if (n >= 1000) return `${(n / 1000).toFixed(1)}k`
51 return String(Math.round(n))
52}
53types/index.d.ts 23 lines1/** One /context category as the bar draws it. */
2export type ContextBarCategory = {
3 name: string
4 tokens: number
5 /** Theme key /context draws the category in. */
6 color: string
7 kind: 'used' | 'free' | 'buffer'
8}
9
10/** The last breakdown read, kept so drawing never waits on a count. */
11export type ContextBarSnapshot = {
12 categories: ContextBarCategory[]
13 totalTokens: number
14 maxTokens: number
15 percentage: number
16}
17
18declare module 'claude-code' {
19 interface PluginState {
20 'context-bar': { isOn: boolean; snapshot: ContextBarSnapshot | null }
21 }
22}
23