SLOPSHOPPER

usage-meter

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…

newpanebandguardcommandprocess
v0.4.0MITupdated 2026-10-26PointerLex/claude-usage-meter
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-meter
│ ┃ Usage ✕ › fix the failing auth test and add an audit log call │ ┃ CONTEXT │ ┃ No breakdown yet. ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ CONTEXT HISTORY % used per reading ⏺ 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 │ ┃ OUTPUT / TURN max 1.5k · last 1.5k │ ┃ › /usage-meter │ ┃ ⎿ usage-meter: Usage pane opened. │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ LIMITS │ ┃ 5H ████████░░░░ 69% NaNhNaNm │ ┃ │ ┃ TOKENS │ ┃ ↑97.4k in ↓1.5k out │ ┃ ▓▒░ USAGE ░▒▓ │ 5H ████████░░░░ 69% left resets NaNhNaNm │ CTX ████░░░░ 49% used │ ↑97.4k ↓1.5k TOK ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
▓▒░ USAGE ░▒▓ │ 5H ████████░░░░ 69% left resets NaNhNaNm │ CTX ████░░░░ 49% used │ ↑97.4k ↓1.5k TOK
Pane · Usage
CONTEXT No breakdown yet. CONTEXT HISTORY % used per reading █ █ █ █ OUTPUT / TURN max 1.5k · last 1.5k █ █ █ █ LIMITS 5H ████████░░░░ 69% NaNhNaNm TOKENS ↑97.4k in ↓1.5k out GIT not a git repo a1b2c3d fix: refresh expired tokens (2 minutes ago) 9f8e7d6 feat: add audit log (3 hours ago) TASKS 0/0 Nothing handed to subagents yet. AGENTS No subagents yet this session. PULL REQUESTS gh not available here. CONTAINERS No containers.
README

usage-meter

usage-meter: Clawd next to the usage band

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 band above the prompt

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

The pane

The usage pane beside the chat

A black side pane (/usage-meter opens it; it docks by itself on terminals 144 columns or wider) shows:

  • Context: the breakdown by category (system prompt, tools, messages, free space…) as a stacked bar and a table of tokens and percentages.
  • Charts: context % used per reading, and output tokens per turn, as gradient column charts.
  • Limits and tokens: the 5-hour and weekly windows, and tokens spent across sessions.
  • Tasks: a to-do list of what was handed to subagents, checked off as each one finishes; agents started by a Workflow (orchestrator) show their run id and place in it.
  • Agents: each subagent's type, task, tools used, elapsed time, status and last tool call, plus the model and effort its requests actually use, styled by level (low is dim, xhigh a cyan-to-magenta gradient, max a flowing rainbow with a glint while the agent works) and the model colored by family.
  • Git: current branch, ahead/behind, changed files, and a colored graph of the last 20 commits on all branches.
  • Pull requests: open PRs of the repository, through the GitHub CLI (gh).
  • Containers: every Docker container with its state (running, exited…).

Subagent tasks and agents, the max-effort rainbow flowing

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

Install

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.

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