Cyberpunk usage band and pane: 5h and weekly limits, tokens, context breakdown and history charts, subagent tasks with model and effort, git graph, PRs and…

A Claude Code mod that draws a cyberpunk-style band above the prompt: how much is left of your 5-hour and weekly usage windows (with a cyan-to-magenta gradient bar and the time until each resets), plus the tokens you have spent, accumulated across sessions.

The usage windows appear after the first response of a session, and only on a Claude subscription (Pro/Max).

A black side pane (/usage-meter opens it; it docks by itself on terminals 144 columns or wider) shows:
gh).
Git, PRs and containers refresh every 30 seconds for the folder the session runs in; any tool that is missing is skipped with a note. To keep some containers out of the pane, set the Hide containers option in /config to comma-separated parts of their names (for example prod,client-a).
At the prompt of a terminal session:
/plugin install usage-meter --marketplace PointerLex/claude-usage-meter
Answer y to add the marketplace, then pick a scope (user scope is first; press Enter).
From a shell:
claude plugin install usage-meter --marketplace PointerLex/claude-usage-meter
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with PointerLex/claude-usage-meter, then Add plugin.
hooks/register.tsx 601 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Agent, Container, Ctx, Dev, GraphLine, Limit, Pr, Tokens } from '../types'
5
6const limits = atom({ plugin: 'usage-meter', key: 'limits' } as const, [] as Limit[])
7const tokens = atom({ plugin: 'usage-meter', key: 'tokens' } as const, { in: 0, out: 0 } as Tokens)
8const ctx = atom({ plugin: 'usage-meter', key: 'ctx' } as const, { window: 0 } as Ctx)
9const ctxHist = atom({ plugin: 'usage-meter', key: 'ctxHist' } as const, [] as number[])
10const turnHist = atom({ plugin: 'usage-meter', key: 'turnHist' } as const, [] as number[])
11const tick = atom({ plugin: 'usage-meter', key: 'tick' } as const, 0)
12const agents = atom({ plugin: 'usage-meter', key: 'agents' } as const, [] as Agent[])
13const dev = atom({ plugin: 'usage-meter', key: 'dev' } as const, { ahead: 0, behind: 0, changes: 0, graph: [] } as Dev)
14
15const PANE = 'usage-meter'
16const CYAN = '#00f0ff'
17const MAGENTA = '#ff00e6'
18const HOT = '#ff2a6d'
19const OFF = '#2b2b45'
20const LABELS: Record<string, string> = { five_hour: '5H', seven_day: 'WEEK' }
21const BAR = 12
22const BG = '#000000'
23const HIST = 200
24const BLOCKS = '▁▂▃▄▅▆▇█'
25const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
26const LANES = [CYAN, MAGENTA, '#b967ff', '#05ffa1', '#fffb96']
27const GREEN = '#05ffa1'
28const SEP = '\x1f'
29
30// `git status --porcelain=v2 --branch` -> branch, ahead/behind, changed paths.
31export const parseStatus = (out: string) => {
32 let branch: string | undefined
33 let ahead = 0
34 let behind = 0
35 let changes = 0
36 for (const l of out.split('\n')) {
37 if (l.startsWith('# branch.head ')) branch = l.slice(14).trim()
38 else if (l.startsWith('# branch.ab ')) {
39 const [a, b] = l.slice(12).trim().split(' ')
40 ahead = Math.abs(Number(a))
41 behind = Math.abs(Number(b))
42 } else if (l.trim() && !l.startsWith('#')) changes++
43 }
44 return { branch, ahead, behind, changes }
45}
46
47// `git log --graph --format=<SEP>%h<SEP>%d<SEP>%s` -> lines; graph-only lines have no hash.
48export const parseGraph = (out: string): GraphLine[] =>
49 out
50 .split('\n')
51 .filter(l => l.trim())
52 .map(l => {
53 const [graph = '', hash = '', refs = '', subject = ''] = l.split(SEP)
54 return { graph, hash, refs: refs.trim(), subject }
55 })
56
57// `docker ps -a --format {{json .}}` -> containers.
58export const parseDocker = (out: string): Container[] =>
59 out
60 .split('\n')
61 .filter(l => l.trim().startsWith('{'))
62 .map(l => {
63 const j = JSON.parse(l) as Record<string, string>
64 return { name: j.Names ?? '', image: j.Image ?? '', state: j.State ?? '', status: j.Status ?? '' }
65 })
66
67async function sh($: EngineInterface, argv: string[]) {
68 try {
69 const r = await $.process.run(argv, { timeoutMs: 8000 })
70 return r.exitCode === 0 ? r.stdout : undefined
71 } catch {
72 return undefined
73 }
74}
75
76const LIVE = ['pending', 'running', 'waiting', 'idle']
77const AGENT_COLOR: Record<string, string> = { running: CYAN, pending: '#fffb96', waiting: '#fffb96', idle: '#fffb96', completed: GREEN }
78const CHECK: Record<string, string> = { running: '▸', pending: ' ', waiting: ' ', idle: ' ', completed: '✓' }
79
80// One short line for what a tool call is about: its command, path, pattern or description.
81export const hint = (tool: string, input: Record<string, unknown>) => {
82 const v = ['command', 'file_path', 'pattern', 'url', 'query', 'description', 'path']
83 .map(f => input[f])
84 .find(x => typeof x === 'string' && x)
85 return v ? `${tool}: ${String(v).split('\n')[0]}` : tool
86}
87
88// Model family colour: the bigger the model, the hotter.
89export const modelColor = (m = '') =>
90 /opus/.test(m) ? MAGENTA : /fable/.test(m) ? '#ffb000' : /sonnet/.test(m) ? CYAN : /haiku/.test(m) ? GREEN : '#fffb96'
91
92const RAINBOW = ['#ff2a6d', '#ff8a00', '#fffb96', '#05ffa1', '#00f0ff', '#b967ff', '#ff00e6']
93
94// Per-letter colours for an effort level, quiet at low up to a rainbow at max (like Ultracode's).
95export const effortStyle = (text: string, effort = '', t = 0) => {
96 const n = text.length
97 const pick: Record<string, (i: number) => string> = {
98 low: () => '#5c5c7a',
99 medium: () => '#b8b8d0',
100 high: () => CYAN,
101 xhigh: i => lerp(CYAN, MAGENTA, i, n),
102 }
103 // max: the rainbow flows one letter per tick, and a white glint sweeps across with a gap after it
104 const glint = t % (n + 6)
105 const rainbow = (i: number) =>
106 i === glint ? '#ffffff' : Math.abs(i - glint) === 1 ? '#ffd6f5' : (RAINBOW[(i + t) % RAINBOW.length] ?? CYAN)
107 const f = pick[effort] ?? (effort === 'max' ? rainbow : () => '#fffb96')
108 return [...text].map((ch, i) => ({ ch, color: f(i) }))
109}
110
111export const elapsed = (ms: number) => {
112 const s = Math.max(0, Math.round(ms / 1000))
113 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`
114}
115
116// Status from the engine's agent list; an agent it no longer lists has finished.
117async function syncAgents($: EngineInterface) {
118 const mine = await read($, agents)
119 if (!mine.some(a => LIVE.includes(a.status))) return
120 const live = await $.agent.list()
121 const now = await $.clock.now()
122 await update($, agents, as =>
123 as.map(a => {
124 if (!LIVE.includes(a.status) || a.run) return a
125 const status = live.find(l => l.id === a.id)?.status ?? 'completed'
126 return { ...a, status, endedAt: LIVE.includes(status) ? undefined : now }
127 }),
128 )
129}
130
131// Advances the max-effort rainbow only while such an agent is working, so an idle pane does not redraw.
132type Breakdown = Awaited<ReturnType<EngineInterface['session']['usage']>>['context']['breakdown']
133let bd: { key: string; b: Breakdown } | undefined
134
135async function shimmer($: EngineInterface) {
136 const as = await read($, agents)
137 if (as.some(a => a.effort === 'max' && LIVE.includes(a.status))) await update($, tick, t => t + 1)
138}
139
140async function refresh($: EngineInterface, hide: string[]) {
141 const [status, log, prs, docker] = await Promise.all([
142 sh($, ['git', 'status', '--porcelain=v2', '--branch']),
143 sh($, ['git', 'log', '--graph', '--all', '-n', '20', '--decorate=short', `--format=${SEP}%h${SEP}%d${SEP}%s`]),
144 sh($, ['gh', 'pr', 'list', '-L', '8', '--json', 'number,title,headRefName,isDraft']),
145 sh($, ['docker', 'ps', '-a', '--format', '{{json .}}']),
146 ])
147 let prList: Pr[] | undefined
148 try {
149 prList = prs === undefined ? undefined : (JSON.parse(prs) as Pr[])
150 } catch {
151 prList = undefined
152 }
153 const next: Dev = {
154 ...(status === undefined ? { ahead: 0, behind: 0, changes: 0 } : parseStatus(status)),
155 graph: log === undefined ? [] : parseGraph(log),
156 prs: prList,
157 containers: docker === undefined ? undefined : parseDocker(docker).filter(c => !hide.some(h => c.name.includes(h))),
158 dockerNote: docker === undefined ? 'Docker not reachable.' : undefined,
159 }
160 await update($, dev, () => next)
161}
162
163// Hex color i/(n-1) of the way from `a` to `b`.
164export const lerp = (a: string, b: string, i: number, n: number) => {
165 const t = n > 1 ? i / (n - 1) : 0
166 const ch = (s: string, o: number) => parseInt(s.slice(o, o + 2), 16)
167 return (
168 '#' +
169 [1, 3, 5]
170 .map(o => Math.round(ch(a, o) + (ch(b, o) - ch(a, o)) * t).toString(16).padStart(2, '0'))
171 .join('')
172 )
173}
174
175// `n` gradient cells, the first `pct`% lit.
176export const cells = (pct: number, n: number) => {
177 const lit = Math.round((Math.min(100, Math.max(0, pct)) / 100) * n)
178 return Array.from({ length: n }, (_, i) =>
179 i < lit ? { ch: '█', color: lerp(CYAN, MAGENTA, i, n) } : { ch: '░', color: OFF },
180 )
181}
182
183export const b64 = (u8: Uint8Array) => {
184 let s = ''
185 for (let i = 0; i < u8.length; i += 3) {
186 const a = u8[i] ?? 0
187 const b = u8[i + 1]
188 const c = u8[i + 2]
189 const n = (a << 16) | ((b ?? 0) << 8) | (c ?? 0)
190 s += B64[(n >> 18) & 63]! + B64[(n >> 12) & 63]! + (b === undefined ? '=' : B64[(n >> 6) & 63]!) + (c === undefined ? '=' : B64[n & 63]!)
191 }
192 return s
193}
194
195// Column chart as Raster cells: newest value at the right, eighth-block tops, cyan->magenta by height, black ground.
196export const columnChart = (values: number[], cols: number, rows: number) => {
197 const max = Math.max(1, ...values)
198 const vs = values.slice(-cols)
199 const off = cols - vs.length
200 const w = new Uint32Array(cols * rows * 3)
201 for (let x = 0; x < cols; x++) {
202 const eighths = x < off ? 0 : Math.round(((vs[x - off] ?? 0) / max) * rows * 8)
203 for (let r = 0; r < rows; r++) {
204 const fill = Math.max(0, Math.min(8, eighths - r * 8))
205 const i = ((rows - 1 - r) * cols + x) * 3
206 w[i] = fill ? BLOCKS.charCodeAt(fill - 1) : 0x20
207 w[i + 1] = parseInt(lerp(CYAN, MAGENTA, r, rows).slice(1), 16)
208 w[i + 2] = 0
209 }
210 }
211 return b64(new Uint8Array(w.buffer))
212}
213
214export const k = (n: number) =>
215 n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`
216
217export const until = (iso: string | undefined, now: number) => {
218 if (!iso) return ''
219 const min = Math.max(0, Math.round((Date.parse(iso) - now) / 60000))
220 return min >= 1440
221 ? `${Math.floor(min / 1440)}d${Math.floor((min % 1440) / 60)}h`
222 : `${Math.floor(min / 60)}h${min % 60}m`
223}
224
225export const register: Register = (on, options) => {
226 const hide = String(options.hideContainers ?? '')
227 .split(',')
228 .map(x => x.trim())
229 .filter(Boolean)
230 on('session.start', async ($, e, next) => {
231 const saved = (await $.store.get('tokens')) as Tokens | undefined
232 if (saved) await update($, tokens, () => saved)
233 await $.command.register({ name: 'usage-meter', description: 'Open the usage and context pane' })
234 void $.ui.open({ id: PANE, title: 'Usage' })
235 void refresh($, hide)
236 // ponytail: polls every 30s; a git/docker event stream if that ever feels stale
237 $.clock.every(30_000, () => void refresh($, hide))
238 $.clock.every(2_000, () => void syncAgents($))
239 $.clock.every(120, () => void shimmer($))
240 return next(e)
241 })
242
243 on('command.run', { command: 'usage-meter' }, async $ => {
244 await $.ui.open({ id: PANE, title: 'Usage' })
245 return { text: 'Usage pane opened.' }
246 })
247
248 on('agent.spawn', async ($, e, next) => {
249 const r = await next(e)
250 const id = r.agentId
251 if (id) {
252 const now = await $.clock.now()
253 const run = e.workflow ? `${e.workflow.runId.slice(0, 9)} #${e.workflow.agentIndex}` : undefined
254 const a: Agent = {
255 id,
256 type: e.subagentType,
257 description: e.description,
258 status: 'running',
259 tools: 0,
260 out: 0,
261 model: r.model,
262 run,
263 startedAt: now,
264 }
265 await update($, agents, as => [...as, a].slice(-12))
266 }
267 return r
268 })
269
270 on('tool.call', async ($, e, next) => {
271 const id = e.agentId
272 if (id) {
273 const last = hint(e.tool, e as unknown as Record<string, unknown>)
274 await update($, agents, as => as.map(a => (a.id === id ? { ...a, tools: a.tools + 1, last } : a)))
275 }
276 return next(e)
277 })
278
279 // The effort a subagent's requests ask for; the model can change per step too.
280 on('turn.step', async function* ($, e, next) {
281 const id = e.agentId
282 if (id) {
283 const effort = e.effort === undefined ? undefined : String(e.effort)
284 const mine = (await read($, agents)).find(a => a.id === id)
285 if (mine && (mine.effort !== effort || mine.model !== e.model))
286 await update($, agents, as => as.map(a => (a.id === id ? { ...a, effort, model: e.model } : a)))
287 }
288 return yield* next(e)
289 })
290
291 on('session.measure', async ($, e, next) => {
292 await update($, limits, () => e.rateLimits)
293 await update($, ctx, () => ({ percent: e.context.percent, tokens: e.context.tokens, window: e.context.window }))
294 const pct = e.context.percent
295 if (pct !== undefined && e.changed.includes('context')) await update($, ctxHist, h => [...h, pct].slice(-HIST))
296 return next(e)
297 })
298
299 on('turn.complete', async ($, e, next) => {
300 const r = await next(e)
301 const id = e.agentId
302 if (id) {
303 const now = await $.clock.now()
304 const out = r.usage?.output_tokens ?? 0
305 await update($, agents, as =>
306 as.map(a =>
307 a.id !== id
308 ? a
309 : {
310 ...a,
311 out: a.out + out,
312 ...(a.run ? { status: e.isAborted ? 'killed' : 'completed', endedAt: now } : {}),
313 },
314 ),
315 )
316 }
317 if (r.usage) {
318 const u = r.usage
319 // ponytail: read-add-write; two sessions finishing at the same instant can lose one turn
320 const prev = ((await $.store.get('tokens')) as Tokens | undefined) ?? { in: 0, out: 0 }
321 const total = {
322 in: prev.in + u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens,
323 out: prev.out + u.output_tokens,
324 }
325 await $.store.set('tokens', total)
326 await update($, tokens, () => total)
327 await update($, turnHist, h => [...h, u.output_tokens].slice(-HIST))
328 }
329 return r
330 })
331
332 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
333 if (e.props.hasSurvey) return next(e)
334
335 const { Box, Text } = $.ui.resolve(e)
336 const lims = (await read($, limits)).filter(l => LABELS[l.kind])
337 const tok = await read($, tokens)
338 const c = await read($, ctx)
339 const now = await $.clock.now()
340 const title = '▓▒░ USAGE ░▒▓'
341
342 return (
343 <Box flexDirection="row">
344 <Text weight="bold">
345 {[...title].map((ch, i) => (
346 <Text key={`t${i}`} color={lerp(CYAN, MAGENTA, i, title.length)}>
347 {ch}
348 </Text>
349 ))}
350 </Text>
351 {lims.map(l => {
352 const left = Math.max(0, 100 - l.percentUsed)
353 return (
354 <Text key={l.kind}>
355 <Text color={MAGENTA}> │ </Text>
356 <Text color={CYAN} weight="bold">{LABELS[l.kind]} </Text>
357 {cells(left, BAR).map((x, i) => (
358 <Text key={`${l.kind}${i}`} color={x.color}>{x.ch}</Text>
359 ))}
360 <Text color={left < 20 ? HOT : CYAN} weight="bold"> {left.toFixed(0)}% left</Text>
361 {l.resetsAt ? <Text dimColor> resets {until(l.resetsAt, now)}</Text> : null}
362 </Text>
363 )
364 })}
365 {c.percent === undefined ? null : (
366 <Text>
367 <Text color={MAGENTA}> │ </Text>
368 <Text color={CYAN} weight="bold">CTX </Text>
369 {cells(c.percent, 8).map((x, i) => (
370 <Text key={`c${i}`} color={x.color}>{x.ch}</Text>
371 ))}
372 <Text color={c.percent > 80 ? HOT : CYAN} weight="bold"> {c.percent}% used</Text>
373 </Text>
374 )}
375 <Text color={MAGENTA}> │ </Text>
376 <Text color={CYAN}>↑{k(tok.in)} </Text>
377 <Text color={MAGENTA}>↓{k(tok.out)}</Text>
378 <Text dimColor> TOK</Text>
379 </Box>
380 )
381 })
382
383 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
384 const { Box, Text } = $.ui.resolve(e)
385 const Raster = e.surface === 'terminal' ? $.ui.resolve(e).Raster : undefined
386 const c = await read($, ctx) // redraw whenever the window moves
387 const lims = (await read($, limits)).filter(l => LABELS[l.kind])
388 const tok = await read($, tokens)
389 const ch = await read($, ctxHist)
390 const th = await read($, turnHist)
391 const d = await read($, dev)
392 const ags = await read($, agents)
393 const t = await read($, tick)
394 const now = await $.clock.now()
395 // the breakdown is the slow part of a redraw: ask again only when the context moved, not on every shimmer tick
396 const ck = `${c.tokens}/${c.window}`
397 if (bd?.key !== ck) bd = { key: ck, b: (await $.session.usage({ breakdown: 'summary' })).context.breakdown }
398 const b = bd.b
399 const w = Math.max(10, e.props.bodyColumns - 2)
400
401 return (
402 <Box flexDirection="column" backgroundColor={BG} minHeight={e.props.scroll.bodyRows} paddingX={1}>
403 <Text color={CYAN} weight="bold">CONTEXT</Text>
404 {b ? (
405 <Box flexDirection="column">
406 <Text>
407 <Text color={b.percentage > 80 ? HOT : MAGENTA} weight="bold">{b.percentage}% used </Text>
408 <Text dimColor>{k(b.totalTokens)} / {k(b.rawMaxTokens)}</Text>
409 </Text>
410 <Text>
411 {b.categories
412 .filter(r => r.kind !== 'deferred' && r.tokens > 0)
413 .map(r => (
414 <Text key={`s${r.name}`} color={r.kind === 'free' ? OFF : r.color}>
415 {(r.kind === 'free' ? '░' : '█').repeat(Math.round((r.tokens / b.rawMaxTokens) * w))}
416 </Text>
417 ))}
418 </Text>
419 {b.categories
420 .filter(r => r.tokens > 0)
421 .map(r => (
422 <Box key={`r${r.name}`} flexDirection="row" justifyContent="space-between">
423 <Text>
424 <Text color={r.kind === 'free' ? OFF : r.color}>■ </Text>
425 <Text dimColor={r.isDeferred}>{r.name}</Text>
426 </Text>
427 <Text dimColor={r.isDeferred}>
428 {k(r.tokens)}{' '}
429 {r.isDeferred ? ' —' : `${((r.tokens / b.rawMaxTokens) * 100).toFixed(1).padStart(5)}%`}
430 </Text>
431 </Box>
432 ))}
433 </Box>
434 ) : (
435 <Text dimColor>No breakdown yet.</Text>
436 )}
437 <Text> </Text>
438 <Text>
439 <Text color={CYAN} weight="bold">CONTEXT HISTORY </Text>
440 <Text dimColor>% used per reading</Text>
441 </Text>
442 {Raster && ch.length > 0 ? (
443 <Raster key="ctx-hist" columns={w} rows={6} cells={columnChart(ch, w, 6)} />
444 ) : (
445 <Text dimColor>Fills in as the session runs.</Text>
446 )}
447 <Text> </Text>
448 <Text>
449 <Text color={CYAN} weight="bold">OUTPUT / TURN </Text>
450 <Text dimColor>{th.length ? `max ${k(Math.max(...th))} · last ${k(th[th.length - 1] ?? 0)}` : ''}</Text>
451 </Text>
452 {Raster && th.length > 0 ? (
453 <Raster key="turn-hist" columns={w} rows={6} cells={columnChart(th, w, 6)} />
454 ) : (
455 <Text dimColor>Fills in after each turn.</Text>
456 )}
457 <Text> </Text>
458 <Text color={CYAN} weight="bold">LIMITS</Text>
459 {lims.length === 0 ? <Text dimColor>After the first response (subscription only).</Text> : null}
460 {lims.map(l => {
461 const left = Math.max(0, 100 - l.percentUsed)
462 return (
463 <Text key={`p${l.kind}`}>
464 <Text color={CYAN}>{(LABELS[l.kind] ?? '').padEnd(5)}</Text>
465 {cells(left, BAR).map((x, i) => (
466 <Text key={`p${l.kind}${i}`} color={x.color}>{x.ch}</Text>
467 ))}
468 <Text color={left < 20 ? HOT : CYAN}> {left.toFixed(0)}%</Text>
469 {l.resetsAt ? <Text dimColor> {until(l.resetsAt, now)}</Text> : null}
470 </Text>
471 )
472 })}
473 <Text> </Text>
474 <Text color={CYAN} weight="bold">TOKENS</Text>
475 <Text>
476 <Text color={CYAN}>↑{k(tok.in)} in </Text>
477 <Text color={MAGENTA}>↓{k(tok.out)} out</Text>
478 </Text>
479 <Text> </Text>
480 <Text>
481 <Text color={CYAN} weight="bold">GIT </Text>
482 {d.branch ? (
483 <Text>
484 <Text color={MAGENTA}>{d.branch}</Text>
485 <Text dimColor>
486 {' '}↑{d.ahead} ↓{d.behind} · {d.changes} changed
487 </Text>
488 </Text>
489 ) : (
490 <Text dimColor>not a git repo</Text>
491 )}
492 </Text>
493 {d.graph.map((g, i) => (
494 <Text key={`g${i}`} wrap="truncate-end">
495 {[...g.graph].map((c, j) => (
496 <Text key={`g${i}c${j}`} color={c === '*' ? '#ffffff' : LANES[Math.floor(j / 2) % LANES.length]}>
497 {c === '*' ? '●' : c}
498 </Text>
499 ))}
500 {g.hash ? <Text color="#fffb96"> {g.hash}</Text> : null}
501 {g.refs ? <Text color={MAGENTA}> {g.refs}</Text> : null}
502 {g.subject ? <Text> {g.subject}</Text> : null}
503 </Text>
504 ))}
505 <Text> </Text>
506 <Text color={CYAN} weight="bold">
507 TASKS{' '}
508 <Text dimColor>
509 {ags.filter(a => a.status === 'completed').length}/{ags.length}
510 </Text>
511 </Text>
512 {ags.length === 0 ? <Text dimColor>Nothing handed to subagents yet.</Text> : null}
513 {ags.map(a => (
514 <Text key={`td${a.id}`} wrap="truncate-end">
515 <Text color={AGENT_COLOR[a.status] ?? HOT}>[{CHECK[a.status] ?? '✗'}] </Text>
516 <Text dimColor={a.status === 'completed'}>{a.description}</Text>
517 {a.run ? <Text color="#b967ff"> {a.run}</Text> : null}
518 </Text>
519 ))}
520 <Text> </Text>
521 <Text color={CYAN} weight="bold">AGENTS</Text>
522 {ags.length === 0 ? <Text dimColor>No subagents yet this session.</Text> : null}
523 {ags.map(a => (
524 <Box key={`ag${a.id}`} flexDirection="column">
525 <Box flexDirection="row" justifyContent="space-between" columnGap={1}>
526 <Box flexShrink={1}>
527 <Text wrap="truncate-end">
528 <Text color={AGENT_COLOR[a.status] ?? HOT}>● </Text>
529 <Text color={MAGENTA}>{a.type} </Text>
530 <Text>{a.description}</Text>
531 </Text>
532 </Box>
533 <Box flexShrink={0}>
534 <Text dimColor>
535 {a.tools} tools · {elapsed((a.endedAt ?? now) - a.startedAt)} · {a.status}
536 </Text>
537 </Box>
538 </Box>
539 <Text wrap="truncate-end">
540 {' '}
541 <Text color={modelColor(a.model)} weight="bold">
542 {a.model ?? '?'}
543 </Text>
544 {a.effort ? <Text dimColor> · </Text> : null}
545 {a.effort
546 ? effortStyle(`effort ${a.effort}`, a.effort, LIVE.includes(a.status) ? t : 0).map((c, i) => (
547 <Text key={`ef${a.id}${i}`} color={c.color} weight={a.effort === 'max' ? 'bold' : undefined}>
548 {c.ch}
549 </Text>
550 ))
551 : null}
552 <Text dimColor>
553 {' '}· ↓{k(a.out)} out{a.run ? ` · ${a.run}` : ''}
554 </Text>
555 </Text>
556 {a.last ? (
557 <Text dimColor wrap="truncate-end">
558 {' └ '}
559 {a.last}
560 </Text>
561 ) : null}
562 </Box>
563 ))}
564 <Text> </Text>
565 <Text color={CYAN} weight="bold">PULL REQUESTS</Text>
566 {d.prs === undefined ? (
567 <Text dimColor>gh not available here.</Text>
568 ) : d.prs.length === 0 ? (
569 <Text dimColor>No open PRs.</Text>
570 ) : (
571 d.prs.map(pr => (
572 <Text key={`pr${pr.number}`} wrap="truncate-end">
573 <Text color={pr.isDraft ? OFF : GREEN}>#{pr.number} </Text>
574 <Text>{pr.title}</Text>
575 <Text dimColor> {pr.headRefName}</Text>
576 </Text>
577 ))
578 )}
579 <Text> </Text>
580 <Text color={CYAN} weight="bold">CONTAINERS</Text>
581 {d.containers === undefined ? (
582 <Text dimColor>{d.dockerNote}</Text>
583 ) : d.containers.length === 0 ? (
584 <Text dimColor>No containers.</Text>
585 ) : (
586 d.containers.map(c => (
587 <Box key={`dk${c.name}`} flexDirection="row" justifyContent="space-between">
588 <Text wrap="truncate-end">
589 <Text color={c.state === 'running' ? GREEN : c.state === 'exited' ? HOT : '#fffb96'}>● </Text>
590 <Text>{c.name}</Text>
591 <Text dimColor> {c.image}</Text>
592 </Text>
593 <Text dimColor>{c.status}</Text>
594 </Box>
595 ))
596 )}
597 </Box>
598 )
599 })
600}
601types/index.d.ts 46 lines1export type Tokens = { in: number; out: number }
2export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
3export type Ctx = { percent?: number; tokens?: number; window: number }
4export type GraphLine = { graph: string; hash: string; refs: string; subject: string }
5export type Pr = { number: number; title: string; headRefName: string; isDraft: boolean }
6export type Container = { name: string; image: string; state: string; status: string }
7export type Agent = {
8 id: string
9 type: string
10 description: string
11 status: string
12 tools: number
13 last?: string
14 model?: string
15 effort?: string
16 run?: string
17 out: number
18 startedAt: number
19 endedAt?: number
20}
21export type Dev = {
22 branch?: string
23 ahead: number
24 behind: number
25 changes: number
26 graph: GraphLine[]
27 prs?: Pr[]
28 containers?: Container[]
29 dockerNote?: string
30}
31
32declare module 'claude-code' {
33 interface PluginState {
34 'usage-meter': {
35 limits: Limit[]
36 tokens: Tokens
37 ctx: Ctx
38 ctxHist: number[]
39 turnHist: number[]
40 dev: Dev
41 agents: Agent[]
42 tick: number
43 }
44 }
45}
46