A pane listing this project's subagents with a run button and live status (/agents-panel to toggle)

Claude Code mods by Bao: a context-window bar with plan usage, and a subagents panel.

| Mod | What it does |
|---|---|
context-bar | A colored context-window bar above the prompt, broken down by category, with plan usage badges and reset times. |
agents-panel | A pane listing your subagents, with a run button, live status and each run's report. |
Requires a Claude Code version that supports mods (developed on 2.1.310).
/plugin marketplace add baoxingx/claude-mods
/plugin install context-bar@baoxingx-mods
/plugin install agents-panel@baoxingx-mods
Then run /reload-plugins (or start a new session).
A box above the prompt that shows how the context window is filling up and how much of your plan you have used.
◆ Context Usage [ 1% ] ↻ 01:30 Week [ 10% ] ↻ Wed 20:00 139k of 1M · compacts at 967k [ 14% ]
████████▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░
■ System Prompt 5k 1% ■ Tools 28k 3% ■ MCP Tools 2.7k 1% ■ Agents 77 1% ■ Memory Files 177 1%
■ Skills 9.9k 1% ■ Messages 93k 9% ■ Free 828k
| Part | Meaning |
|---|---|
| Usage | Plan usage of the 5-hour session window, and the local time it resets (↻ 01:30). |
| Week | Plan usage of the 7-day window (all models), and the local day and time it resets (↻ Wed 20:00). |
| 139k of 1M | Tokens currently in context, out of the model's context window. |
| compacts at 967k | Where auto-compact kicks in; drawn as a yellow marker on the bar. Hidden when auto-compact is off. |
| 14% badge | How full the context window is. |
| Bar segments | Tokens per category, in a fixed order and color: System Prompt, Tools, MCP Tools, Agents, Memory Files, Skills, Messages. Any other category the engine reports is shown in gray. |
| Free | Tokens left in the window. |
Badge colors, for all three percentages:
| Usage | Color |
|---|---|
| under 50% | green |
| 50% – 79% | yellow |
| 80% and over | red |
| Command | Effect |
|---|---|
/context-bar | Toggle the bar. |
/context-bar on | Show it. |
/context-bar off | Hide it. |
The on/off choice is remembered across sessions. The bar is off until you turn it on the first time.
/clear, /resume and /compact.Until the first reply it shows measuring….
A pane that lists the subagents you have defined and lets you run them with one click.
Run /agents-panel to open the pane, and run it again (or close the pane) to hide it.
The pane lists agents from two places:
| Section | Folder |
|---|---|
| PROJECT | .claude/agents/*.md in the current project |
| USER | ~/.claude/agents/*.md |
A project agent hides a user agent with the same name. Each row shows the agent's name, model (inherit when not set) and description, read from the file's frontmatter.
| Status | Meaning |
|---|---|
◌ running · 12s | Running, with elapsed time (ticks every second). |
✓ done · 1m 4s | Finished, with how long it took. Click report to read its final answer, hide to fold it away. |
✕ stopped | Failed, was aborted, or was killed. |
The header shows how many agents are running, or idle. Agents that Claude starts on its own also appear as running, as long as they are listed in the pane. When a run finishes you get a toast.
If no agents are found, the pane says so: ask Claude to create one (for example a code reviewer), then run /agents-panel again.
Each mod is a folder with .claude-plugin/plugin.json, hooks/register.tsx (the mod), types/index.d.ts and tests/.
To load a local copy instead of the installed one, point CLAUDE_CODE_PLUGIN_DIRS at the mod folders (separated by ; on Windows, : elsewhere), for example in ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-mods/context-bar;/path/to/claude-mods/agents-panel"
}
}
Run /reload-plugins after a change.
Tests:
claude plugin test context-bar
claude plugin test agents-panel
hooks/register.tsx 306 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AgentDef, Run } from '../types'
5
6const PANE = 'agents-panel'
7const agents = atom({ plugin: 'agents-panel', key: 'agents' } as const, [])
8const runs = atom({ plugin: 'agents-panel', key: 'runs' } as const, {})
9const expanded = atom({ plugin: 'agents-panel', key: 'expanded' } as const, null)
10const task = atom({ plugin: 'agents-panel', key: 'task' } as const, '')
11const isOpen = atom({ plugin: 'agents-panel', key: 'isOpen' } as const, false)
12
13const DOTS = ['#7aa2f7', '#5bb8c8', '#e9c46a', '#e0805a', '#b48ead', '#e06c75', '#98c379', '#d19a66']
14const ACCENT = '#e07f5a'
15
16const DEFAULT_TASK =
17 'Do your job on the current project now, as your instructions describe. ' +
18 'Use the working tree and recent git changes for context, and finish with a short report.'
19
20// ---------- reading .claude/agents ----------
21
22export function parseAgent(text: string, fallbackName: string): Omit<AgentDef, 'scope'> | null {
23 const m = /^---\s*\n([\s\S]*?)\n---/.exec(text)
24 if (!m) return null
25 const fields: Record<string, string> = {}
26 const lines = (m[1] ?? '').split('\n')
27 for (let i = 0; i < lines.length; i++) {
28 const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(lines[i] ?? '')
29 if (!kv) continue
30 const key = (kv[1] ?? '').toLowerCase()
31 let value = (kv[2] ?? '').trim()
32 // A folded or literal block: gather the indented lines under it
33 if (value === '|' || value === '>' || value === '|-' || value === '>-') {
34 const parts: string[] = []
35 while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1] ?? '')) parts.push((lines[++i] ?? '').trim())
36 value = parts.join(' ')
37 }
38 fields[key] = value.replace(/^["']|["']$/g, '').replace(/\\n/g, ' ')
39 }
40 const name = fields['name'] || fallbackName
41 return {
42 name,
43 model: fields['model'] || 'inherit',
44 description: (fields['description'] || '').replace(/\s+/g, ' ').trim(),
45 }
46}
47
48async function readDir($: EngineInterface, dir: string, scope: 'project' | 'user'): Promise<AgentDef[]> {
49 if (!(await $.fs.exists(dir))) return []
50 const found: AgentDef[] = []
51 for (const entry of await $.fs.list(dir)) {
52 if (entry.kind !== 'file' || !entry.name.endsWith('.md')) continue
53 try {
54 const parsed = parseAgent(await $.fs.read(`${dir}/${entry.name}`), entry.name.replace(/\.md$/, ''))
55 if (parsed) found.push({ ...parsed, scope })
56 } catch {
57 // An unreadable file is left out
58 }
59 }
60 return found.sort((a, b) => a.name.localeCompare(b.name))
61}
62
63async function loadAgents($: EngineInterface): Promise<void> {
64 try {
65 const cwd = await $.session.cwd()
66 const project = await readDir($, `${cwd}/.claude/agents`, 'project')
67 const home = await $.env.get('HOME')
68 const user = home ? await readDir($, `${home}/.claude/agents`, 'user') : []
69 const names = new Set(project.map(a => a.name))
70 await update($, agents, () => [...project, ...user.filter(a => !names.has(a.name))])
71 } catch {
72 // Keep the last list
73 }
74}
75
76// ---------- runs ----------
77
78async function startRun($: EngineInterface, agent: AgentDef): Promise<void> {
79 const typed = (await read($, task)).trim()
80 try {
81 const result = await $.agent.spawn({
82 prompt: typed || DEFAULT_TASK,
83 subagentType: agent.name,
84 description: `${agent.name} (agents panel)`,
85 })
86 if (result.deny !== undefined) {
87 $.ui.toast(`${agent.name}: ${result.deny}`)
88 return
89 }
90 if (!result.agentId) {
91 $.ui.toast(`${agent.name} started`)
92 return
93 }
94 await record($, agent.name, result.agentId)
95 if (typed) await update($, task, () => '')
96 } catch (err) {
97 $.ui.toast(`${agent.name}: ${err instanceof Error ? err.message : 'could not start'}`)
98 }
99}
100
101async function record($: EngineInterface, name: string, agentId: string): Promise<void> {
102 const now = await $.clock.now()
103 await update($, runs, all =>
104 all[name]?.agentId === agentId ? all : { ...all, [name]: { agentId, startedAt: now, status: 'running' as const } },
105 )
106}
107
108async function finish($: EngineInterface, agentId: string, status: Run['status'], answer?: string): Promise<string | null> {
109 const now = await $.clock.now()
110 let finished: string | null = null
111 await update($, runs, all => {
112 const name = Object.keys(all).find(n => all[n]?.agentId === agentId && all[n]?.status === 'running')
113 if (!name) return all
114 finished = name
115 const run = all[name] as Run
116 return { ...all, [name]: { ...run, status, endedAt: now, answer: answer ?? run.answer } }
117 })
118 return finished
119}
120
121export function elapsed(ms: number): string {
122 const s = Math.max(0, Math.round(ms / 1000))
123 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`
124}
125
126// ---------- the mod ----------
127
128export const register: Register = on => {
129 on('session.start', async ($, e, next) => {
130 await $.command.register({
131 name: 'agents-panel',
132 description: "Show this project's subagents in a pane, with a run button for each",
133 immediate: true,
134 })
135 void loadAgents($)
136
137 // While anything runs: tick the elapsed time, and catch runs that ended
138 // without a turn.complete (killed, failed)
139 $.clock.every(1000, () => {
140 void (async () => {
141 const all = await read($, runs)
142 const live = Object.values(all).filter(r => r.status === 'running')
143 if (live.length === 0) return
144 $.ui.invalidate('ui.render')
145 try {
146 const list = await $.agent.list()
147 for (const run of live) {
148 const info = list.find(a => a.id === run.agentId)
149 if (info && info.status !== 'running' && info.status !== 'pending') {
150 await finish($, run.agentId, info.status === 'completed' ? 'done' : 'failed')
151 }
152 }
153 } catch {
154 // Try again next tick
155 }
156 })()
157 })
158 return next(e)
159 })
160
161 on('command.run', { command: 'agents-panel' }, async $ => {
162 if (await read($, isOpen)) {
163 await $.ui.close({ id: PANE })
164 await update($, isOpen, () => false)
165 return { text: 'Agents panel closed.' }
166 }
167 await loadAgents($)
168 await $.ui.open({ id: PANE, title: 'Agents' })
169 await update($, isOpen, () => true)
170 return { text: 'Agents panel open.' }
171 })
172
173 on('ui.close', { id: PANE }, async ($, e, next) => {
174 const result = await next(e)
175 await update($, isOpen, () => false)
176 return result
177 }).catch(($, e, next) => next(e))
178
179 // Agents the model starts show up as running too
180 on('agent.spawn', async ($, e, next) => {
181 const result = await next(e)
182 if (result.agentId) {
183 const known = (await read($, agents)).some(a => a.name === e.subagentType)
184 if (known) await record($, e.subagentType, result.agentId)
185 }
186 return result
187 }).catch(($, e, next) => next(e))
188
189 on('turn.complete', async ($, e, next) => {
190 const result = await next(e)
191 if (e.agentId) {
192 const name = await finish($, e.agentId, e.isAborted || e.reason === 'error' ? 'failed' : 'done', e.answer)
193 if (name) $.ui.toast(`${name} finished — open the Agents panel to read its report`)
194 }
195 return result
196 })
197
198 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
199 const els = $.ui.resolve(e)
200 const { Box, Text, Button } = els
201 const Input = 'Input' in els ? els.Input : undefined
202 const list = await read($, agents)
203 const all = await read($, runs)
204 const open = await read($, expanded)
205 const draft = await read($, task)
206 const now = await $.clock.now()
207 const running = Object.values(all).filter(r => r.status === 'running').length
208 const project = list.filter(a => a.scope === 'project')
209 const user = list.filter(a => a.scope === 'user')
210 const ruleWidth = Math.max(4, e.props.bodyColumns - 30)
211
212 const row = (agent: AgentDef, i: number) => {
213 const run = all[agent.name]
214 const isRunning = run?.status === 'running'
215 const status = !run
216 ? null
217 : isRunning
218 ? <Text color="suggestion">◌ running · {elapsed(now - run.startedAt)}</Text>
219 : run.status === 'done'
220 ? <Text color="success">✓ done · {elapsed((run.endedAt ?? now) - run.startedAt)}</Text>
221 : <Text color="error">✕ stopped</Text>
222 const isExpanded = open === agent.name && !!run?.answer
223 return (
224 <Box key={`agent-${agent.name}`} flexDirection="column" marginBottom={1}>
225 <Box flexDirection="row" justifyContent="space-between">
226 <Text wrap="truncate-end">
227 <Text color={DOTS[i % DOTS.length]}>● </Text>
228 <Text bold>{agent.name}</Text>
229 <Text dimColor> {agent.model}</Text>
230 </Text>
231 <Box flexDirection="row" columnGap={2}>
232 {status}
233 {run?.answer && !isRunning && (
234 <Button
235 key={`view-${agent.name}`}
236 label={isExpanded ? 'hide' : 'report'}
237 plain
238 dimColor
239 onPress={() => update($, expanded, cur => (cur === agent.name ? null : agent.name))}
240 />
241 )}
242 {!isRunning && (
243 <Button key={`run-${agent.name}`} label="▶ run" onPress={() => startRun($, agent)} />
244 )}
245 </Box>
246 </Box>
247 <Box paddingLeft={2}>
248 <Text dimColor wrap="truncate-end">{agent.description || '(no description)'}</Text>
249 </Box>
250 {isExpanded && (
251 <Box paddingLeft={2} marginTop={1} borderStyle="single" borderDimColor paddingX={1}>
252 <Text wrap="wrap">{(run?.answer ?? '').slice(0, 4000)}</Text>
253 </Box>
254 )}
255 </Box>
256 )
257 }
258
259 const section = (label: string, path: string, items: AgentDef[], offset: number) => (
260 <Box flexDirection="column">
261 <Text>
262 <Text bold color="ide">{label}</Text>
263 <Text dimColor> {path} · {items.length} {'─'.repeat(ruleWidth)}</Text>
264 </Text>
265 <Text> </Text>
266 {items.map((a, i) => row(a, i + offset))}
267 </Box>
268 )
269
270 return (
271 <Box flexDirection="column">
272 <Box flexDirection="row" justifyContent="space-between">
273 <Text>
274 <Text color={ACCENT}>◆ </Text>
275 <Text bold>Agents</Text>
276 <Text dimColor> in this project</Text>
277 </Text>
278 <Text dimColor>{running > 0 ? `◌ ${running} running` : 'idle'} </Text>
279 </Box>
280 <Text> </Text>
281 {list.length === 0 && (
282 <Text dimColor wrap="wrap">
283 No subagents found in .claude/agents or ~/.claude/agents. Ask Claude to make one (for example a
284 code-reviewer), then run /agents-panel again.
285 </Text>
286 )}
287 {project.length > 0 && section('PROJECT', '.claude/agents', project, 0)}
288 {user.length > 0 && section('USER', '~/.claude/agents', user, project.length)}
289 {list.length > 0 && Input && (
290 <Input
291 key="task"
292 label="Task"
293 placeholder="optional task for the next run (blank = the agent's own job)"
294 value={draft}
295 submitLabel="keep"
296 onInput={(value: string) => update($, task, () => value)}
297 onSubmit={(value: string) => update($, task, () => value)}
298 />
299 )}
300 <Text> </Text>
301 <Text dimColor>click ▶ run to start one · /agents-panel to hide</Text>
302 </Box>
303 )
304 })
305}
306types/index.d.ts 27 lines1export type AgentDef = {
2 name: string
3 model: string
4 description: string
5 scope: 'project' | 'user'
6}
7
8export type Run = {
9 agentId: string
10 startedAt: number
11 endedAt?: number
12 status: 'running' | 'done' | 'failed'
13 answer?: string
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'agents-panel': {
19 agents: AgentDef[]
20 runs: Record<string, Run>
21 expanded: string | null
22 task: string
23 isOpen: boolean
24 }
25 }
26}
27