SLOPSHOPPER

session-hud

A pane that shows the model, the effort, the output style, the context, the subagents, the cost, the rate limits and the prompt cache

newpaneguardcommandprompttimer
v0.1.0MITupdated 2026-10-10fspathara/hud-claude-mod/session-hud
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · session-hud
│ ┃ Session HUD ✕ › fix the failing auth test and add an audit log call │ ┃ ── Character │ ┃ ─────────────────────────────────────────── ⏺ Read(src/auth.ts) │ ┃ · · · · · ⎿ Read 6 lines │ ┃ Knuckles claude-opus-5-5 ⏺ Update(src/auth.ts) │ ┃ Effort none ⎿ Added 2 lines, removed 1 line │ ┃ Style default ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ ── Load │ ┃ ──────────────────────────────────────────── ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ──── │ ┃ no data ✻ Worked for 42s · done 4:20 PM │ ┃ │ ┃ Subagents none running › /hud │ ┃ Main calls 8 ok 1 failed ⎿ session-hud: Session HUD pane opened. │ ┃ Subagent calls 0 ok 0 failed │ ┃ Bash 4 │ ┃ Write 2 │ ┃ Edit 1 │ ┃ Grep 1 │ ┃ Read 1 │ ┃ Last calls │ ┃ ✓ Bash │ ┃ ✓ Bash │ ┃ ✓ Bash │ ┃ ✗ Bash ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Session HUD
── Character ─────────────────────────────────────────── · · · · · Knuckles claude-opus-5-5 Effort none Style default ── Load ──────────────────────────────────────────────── no data Subagents none running Main calls 8 ok 1 failed Subagent calls 0 ok 0 failed Bash 4 Write 2 Edit 1 Grep 1 Read 1 Last calls ✓ Bash ✓ Bash ✓ Bash ✗ Bash ✓ Write ── Treasury ──────────────────────────────────────────── ● $0.42 this session 5-hour limit ● ● ● ● ● ● ● ○ ○ ○ 31% used ── Cache ─────────────────────────────────────────────── Last request ·············· 🎯 - Session ·············· 🎯 - Expires no request yet
README

claude-hud-mod

A HUD pane for Claude Code. The HUD is a mod: a plugin of hooks that reloads while you work.

The pane shows the state of your session at a glance: the model, the context, the tool calls, the cost, the rate limits, and the prompt cache.

What the pane shows

The pane has four sections:

  • Character: a figure for the model, and more figures for more effort. The pane also shows the effort level and the output style.
  • Load: the context bar and the subagents that run now. The pane also counts the tool calls of the main loop and of the subagents. It shows the most used tools and the last 5 calls.
  • Treasury: the cost of the session, and the rate-limit windows with exact values.
  • Cache: an arrow that flies to a target. A hit of 100% puts the arrow next to the target. The pane also shows the time left for the cache.

Rate limits show only on a subscription. The cost shows only when the host keeps a cost ledger.

Privacy and security

Nothing leaves your PC. The mod sends no data to any server.

  • The mod has no network code. It makes no web request.
  • The mod does not read or write files, run programs, or call a model.
  • The mod reads only the data that Claude Code gives to its hooks: the model, the usage, the settings, and the tool names.
  • The mod keeps its values in the memory of the session. It does not save them across sessions.
  • The mod does not read your prompts, the content of your files, or the output of your tools.

All the code is in session-hud/hooks/register.tsx. You can read it before you install the mod.

Install

  1. Run /plugin marketplace add fspathara/hud-claude-mod. You can also use a local path.
  2. Run /plugin install session-hud@claude-hud-mod.
  3. Run /hud to open the pane.

At the start of a session, the pane opens by itself from 144 terminal columns. In a narrower terminal, run /hud.

Options

OptionValuesDefaultUse
characterssonic, chesssonicThe figures for the model and the effort
cacheTtl5m, 1h1hThe time that the prompt cache stays warm

To change an option, run /plugin and open the configuration of session-hud.

The Sonic set uses emoji. Emoji have double width, so the row can look uneven in some terminals. The chess set uses single-width glyphs.

Develop

  1. Put the session-hud folder in your dev-mods folder, or start Claude Code with claude --plugin-dir <path>/session-hud.
  2. Run claude plugin validate session-hud.
  3. Run claude plugin test session-hud.

All logic is in session-hud/hooks/register.tsx. The tests are in session-hud/tests/hud.test.ts.

To add a character set, add a list of 4 figures to the piece function in session-hud/hooks/register.tsx. The order is by model power.

Repository layout

  • .claude-plugin/marketplace.json: the marketplace file.
  • session-hud/: the plugin.

License

This project uses the MIT License.

Source 2 files
hooks/register.tsx 468 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { HudAgent, HudCalls, HudCache, HudContext, HudLimit } from '../types'
5
6const PANE = 'session-hud'
7const TITLE = 'Session HUD'
8
9const EMPTY_CACHE: HudCache = {
10  lastRead: 0,
11  lastWrite: 0,
12  lastUncached: 0,
13  totalRead: 0,
14  totalWrite: 0,
15  totalUncached: 0,
16  lastResponseAt: null,
17}
18
19const model = atom({ plugin: 'session-hud', key: 'model' } as const, '')
20const effort = atom({ plugin: 'session-hud', key: 'effort' } as const, '')
21const outputStyle = atom({ plugin: 'session-hud', key: 'outputStyle' } as const, 'default')
22const context = atom({ plugin: 'session-hud', key: 'context' } as const, {
23  percent: null,
24  tokens: null,
25  window: null,
26} as HudContext)
27const cache = atom({ plugin: 'session-hud', key: 'cache' } as const, EMPTY_CACHE)
28const now = atom({ plugin: 'session-hud', key: 'now' } as const, 0)
29const cost = atom({ plugin: 'session-hud', key: 'cost' } as const, null as number | null)
30const limits = atom({ plugin: 'session-hud', key: 'limits' } as const, [] as HudLimit[])
31
32const agents = atom({ plugin: 'session-hud', key: 'agents' } as const, [] as HudAgent[])
33
34const EMPTY_CALLS: HudCalls = { main: { ok: 0, failed: 0 }, sub: { ok: 0, failed: 0 }, tools: {}, recent: [] }
35const calls = atom({ plugin: 'session-hud', key: 'calls' } as const, EMPTY_CALLS)
36
37/** Add one tool call to the tally of the main loop or of the subagents. */
38export function addCall(c: HudCalls, fromSubagent: boolean, ok: boolean, tool: string): HudCalls {
39  const who = fromSubagent ? 'sub' : 'main'
40  const t = c[who]
41  return {
42    ...c,
43    [who]: ok ? { ...t, ok: t.ok + 1 } : { ...t, failed: t.failed + 1 },
44    tools: { ...c.tools, [tool]: (c.tools?.[tool] ?? 0) + 1 },
45    // The saved state of an older load can have no `recent` field.
46    recent: [...(c.recent ?? []), { tool, ok, sub: fromSubagent }].slice(-MAX_RECENT),
47  }
48}
49
50const MAX_TOOL_ROWS = 5
51const MAX_RECENT = 5
52
53/** The tools by call count, the most used first; equal counts keep name order. */
54// The saved state of an older load can have no `tools` field.
55export function topTools(tools: Record<string, number> | undefined): [string, number][] {
56  return Object.entries(tools ?? {}).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
57}
58
59const ACTIVE = ['pending', 'running', 'waiting']
60const MAX_AGENT_ROWS = 4
61
62/** The agents that still work: running, waiting for a tool, or not yet started. */
63export function activeAgents(list: HudAgent[]): HudAgent[] {
64  return list.filter(a => ACTIVE.includes(a.status))
65}
66
67const GOLD = '#f2c230'
68const RINGS = 10
69
70/** The rings left in a window: one ring for each 10 % not yet used, as many as `RINGS`. */
71export function ringsLeft(percentUsed: number): number {
72  const left = Math.round(((100 - Math.min(100, Math.max(0, percentUsed))) / 100) * RINGS)
73  return Math.min(RINGS, Math.max(0, left))
74}
75
76/** Dollars with two decimals, or three under one cent so a small cost does not show as zero. */
77export function dollars(usd: number): string {
78  return usd > 0 && usd < 0.01 ? `$${usd.toFixed(3)}` : `$${usd.toFixed(2)}`
79}
80
81/** Milliseconds as `Xd Yh`, `Xh Ym` or `Xm`. */
82export function until(ms: number): string {
83  const m = Math.max(0, Math.ceil(ms / 60_000))
84  const d = Math.floor(m / 1440)
85  const h = Math.floor((m % 1440) / 60)
86  if (d > 0) return `${d}d ${h}h`
87  if (h > 0) return `${h}h ${m % 60}m`
88  return `${m}m`
89}
90
91function limitName(kind: string): string {
92  if (kind === 'five_hour') return '5-hour limit'
93  if (kind === 'seven_day') return '7-day limit'
94  if (kind === 'spend_limit') return 'Spend limit'
95  return kind
96}
97
98const GREEN = '#2e7d32'
99const ORANGE = '#ef6c00'
100const RED = '#c62828'
101const TRACK = '#3a3a3a'
102
103/** Green under 40 %, orange under 65 %, red after. */
104export function barColor(percent: number): string {
105  if (percent < 40) return GREEN
106  if (percent < 65) return ORANGE
107  return RED
108}
109
110/** Cache reads over all input tokens, as a whole percentage; null with no input. */
111export function hitPercent(read: number, write: number, uncached: number): number | null {
112  const total = read + write + uncached
113  return total === 0 ? null : Math.round((read / total) * 100)
114}
115
116/** Milliseconds as `m:ss`, or `h:mm:ss` from one hour. */
117export function clock(ms: number): string {
118  const s = Math.max(0, Math.ceil(ms / 1000))
119  const h = Math.floor(s / 3600)
120  const m = Math.floor((s % 3600) / 60)
121  const ss = String(s % 60).padStart(2, '0')
122  return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${ss}` : `${m}:${ss}`
123}
124
125function tokens(n: number | null): string {
126  if (n === null) return '?'
127  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
128  if (n >= 1000) return `${Math.round(n / 1000)}k`
129  return String(n)
130}
131
132/** The bar's text: the label put in the middle of `width` cells. */
133export function barText(label: string, width: number): string {
134  const w = Math.max(label.length, width)
135  const left = Math.floor((w - label.length) / 2)
136  return ' '.repeat(left) + label + ' '.repeat(w - left - label.length)
137}
138
139const LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
140const LEVEL_COLORS = ['#7a8b99', '#8fb573', '#e0b84a', '#e07a3a', '#d64545']
141const SLOTS = LEVELS.length
142
143export type Theme = 'sonic' | 'chess'
144
145type Figure = { on: string; off: string | null; name: string }
146
147/**
148 * The figure of the model family, by power. Sonic: Tails, Sonic, Knuckles, Super Sonic.
149 * Chess: pawn, knight, rook, queen, with an outline glyph for the empty slots.
150 */
151export function piece(model: string, theme: Theme = 'sonic'): Figure {
152  const m = model.toLowerCase()
153  const rank = m.includes('fable') ? 3 : m.includes('opus') ? 2 : m.includes('sonnet') ? 1 : 0
154  if (theme === 'chess') {
155    return [
156      { on: '♟', off: '♙', name: 'Pawn' },
157      { on: '♞', off: '♘', name: 'Knight' },
158      { on: '♜', off: '♖', name: 'Rook' },
159      { on: '♛', off: '♕', name: 'Queen' },
160    ][rank]
161  }
162  return [
163    { on: '🦊', off: null, name: 'Tails' },
164    { on: '🦔', off: null, name: 'Sonic' },
165    { on: '🥊', off: null, name: 'Knuckles' },
166    { on: '⭐', off: null, name: 'Super Sonic' },
167  ][rank]
168}
169
170/** Filled slots for an effort level (1 to 5); 0 when the effort is unknown or a plain number. */
171export function troops(effort: string): number {
172  return LEVELS.indexOf(effort) + 1
173}
174
175/** A section title with a rule that fills the line: `── Load ─────`. */
176export function heading(title: string, width: number): string {
177  const head = `── ${title} `
178  return head + '─'.repeat(Math.max(0, width - head.length))
179}
180
181/** An arrow that flies toward the target: the closer the hit share is to 100, the closer the arrow. */
182export function arrowTrack(hit: number | null, cells: number): string {
183  const n = Math.max(3, cells)
184  if (hit === null) return '·'.repeat(n) + ' 🎯'
185  const pos = Math.round((Math.min(100, Math.max(0, hit)) / 100) * (n - 1))
186  return '·'.repeat(pos) + '➤' + '·'.repeat(n - 1 - pos) + ' 🎯'
187}
188
189export const register: Register = (on, options) => {
190  const theme: Theme = options.characters === 'chess' ? 'chess' : 'sonic'
191  const ttlMs = options.cacheTtl === '5m' ? 5 * 60_000 : 60 * 60_000
192
193  on('session.start', async ($, e, next) => {
194    await $.command.register({
195      name: 'hud',
196      description: 'Show the session HUD pane (model, output style, context, cache)',
197    })
198    await update($, model, () => '')
199    const settings = await $.settings.read()
200    const style = typeof settings.outputStyle === 'string' ? settings.outputStyle : 'default'
201    await update($, outputStyle, () => style)
202    void $.session.model().then(m => update($, model, () => m))
203
204    // Redraw the expiry timer one time each second.
205    $.clock.every(1000, () => {
206      void update($, now, () => Date.now())
207      void $.agent.list().then(list =>
208        update($, agents, () =>
209          list.map(a => ({ id: a.id, type: a.type, description: a.description, status: a.status })),
210        ),
211      )
212    })
213    void $.ui.open({ id: PANE, title: TITLE })
214
215    return next(e)
216  })
217
218  on('command.run', { command: 'hud' }, async $ => {
219    // Read the cost and the limits now: no request may have happened since the load.
220    const usage = await $.session.usage()
221    await update($, cost, () => usage.cost?.usd ?? null)
222    await update($, limits, () =>
223      usage.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt ?? null })),
224    )
225    await $.ui.open({ id: PANE, title: TITLE })
226
227    return {
228      text:
229        usage.rateLimits.length === 0
230          ? 'Session HUD pane opened. The engine gives no rate limits yet.'
231          : 'Session HUD pane opened.',
232    }
233  })
234
235  // The output style can change during the session; each request composes it again.
236  on('prompt.compose', async ($, e, next) => {
237    const result = await next(e)
238    const style = e.outputStyle?.name ?? 'default'
239    await update($, outputStyle, () => style)
240
241    return result
242  })
243
244  on('turn.step', async function* ($, e, next) {
245    if (e.agentId === undefined) await update($, effort, () => (e.effort === undefined ? '' : String(e.effort)))
246    const result = yield* next(e)
247    // Subagents have their own cache prefix; count the main loop only.
248    if (e.agentId !== undefined || result.usage === null) return result
249
250    const u = result.usage
251    const at = Date.now()
252    await update($, model, () => u.model || e.model)
253    await update($, cache, c => ({
254      lastRead: u.cache_read_input_tokens,
255      lastWrite: u.cache_creation_input_tokens,
256      lastUncached: u.input_tokens,
257      totalRead: (c ?? EMPTY_CACHE).totalRead + u.cache_read_input_tokens,
258      totalWrite: (c ?? EMPTY_CACHE).totalWrite + u.cache_creation_input_tokens,
259      totalUncached: (c ?? EMPTY_CACHE).totalUncached + u.input_tokens,
260      lastResponseAt: at,
261    }))
262    const usage = await $.session.usage()
263    await update($, cost, () => usage.cost?.usd ?? null)
264    await update($, limits, () =>
265      usage.rateLimits.map(r => ({ kind: r.kind, percentUsed: r.percentUsed, resetsAt: r.resetsAt ?? null })),
266    )
267    await update($, context, () => ({
268      percent: usage.context.percent ?? null,
269      tokens: usage.context.tokens ?? null,
270      window: usage.context.window,
271    }))
272
273    return result
274  })
275
276  // Count each tool call by its result. A deny, an error result or a throw is a failed call.
277  on('tool.call', async ($, e, next) => {
278    const sub = e.agentId !== undefined
279    try {
280      const result = await next(e)
281      const ok = result.deny === undefined && result.isError !== true
282      await update($, calls, c => addCall(c, sub, ok, e.tool)).catch(() => {})
283      return result
284    } catch (err) {
285      await update($, calls, c => addCall(c, sub, false, e.tool)).catch(() => {})
286      throw err
287    }
288  })
289
290  // A /clear starts a new conversation: the old cache prefix and context are gone.
291  on('session.end', async ($, e, next) => {
292    if (e.reason === 'clear') {
293      await update($, cache, () => EMPTY_CACHE)
294      await update($, calls, () => EMPTY_CALLS)
295      await update($, context, () => ({ percent: null, tokens: null, window: null }))
296    }
297
298    return next(e)
299  })
300
301  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
302    const { Box, Text } = $.ui.resolve(e)
303    try {
304    const width = Math.max(10, e.props.bodyColumns)
305
306    const m = await read($, model)
307    const eff = await read($, effort)
308    const all = await read($, agents)
309    const busy = activeAgents(all)
310    const tally = await read($, calls)
311    const used = topTools(tally.tools)
312    const spent = await read($, cost)
313    const windows = await read($, limits)
314    const n = troops(eff)
315    const army = piece(m, theme)
316    const rule = '─'.repeat(width)
317    const style = await read($, outputStyle)
318    const ctx = await read($, context)
319    const c = await read($, cache)
320    const t = (await read($, now)) || Date.now()
321
322    // Context bar: the filled part in the level color, the rest dark; white text on both.
323    const pct = ctx.percent
324    const label = pct === null ? 'no data' : `${pct}%  ${tokens(ctx.tokens)}/${tokens(ctx.window)}`
325    const text = barText(label, width)
326    const filled = pct === null ? 0 : Math.min(width, Math.round((Math.min(pct, 100) / 100) * width))
327    const color = barColor(pct ?? 0)
328
329    const lastHit = hitPercent(c.lastRead, c.lastWrite, c.lastUncached)
330    const totalHit = hitPercent(c.totalRead, c.totalWrite, c.totalUncached)
331    const fmtHit = (p: number | null) => (p === null ? '-' : `${p}% hit / ${100 - p}% miss`)
332
333    let expiry = 'no request yet'
334    let expiryColor: string = 'inactive'
335    if (c.lastResponseAt !== null) {
336      const left = c.lastResponseAt + ttlMs - t
337      if (left > 0) {
338        expiry = `${clock(left)} left (${options.cacheTtl})`
339        expiryColor = left < 60_000 ? 'warning' : 'success'
340      } else {
341        expiry = 'expired'
342        expiryColor = 'error'
343      }
344    }
345
346    const lastTrack = arrowTrack(lastHit, Math.min(14, width - 4))
347    const totalTrack = arrowTrack(totalHit, Math.min(14, width - 4))
348    const hitColor = (p: number | null) => (p === null ? 'inactive' : p >= 80 ? 'success' : p >= 50 ? 'warning' : 'error')
349
350    return (
351      <Box flexDirection="column">
352        <Text bold color={GOLD}>{heading('Character', width)}</Text>
353        <Text>
354          {Array.from({ length: SLOTS }, (_, i) => (
355            <Text key={i} color={TRACK}>
356              {i < n ? army.on + ' ' : army.off === null ? ' · ' : army.off + ' '}
357            </Text>
358          ))}
359        </Text>
360        <Text wrap="truncate-end">
361          <Text bold>{army.name}</Text>
362          <Text dimColor>{`  ${m || '?'}`}</Text>
363        </Text>
364        <Text wrap="truncate-end">
365          <Text dimColor>{'Effort  '}</Text>
366          <Text color={n > 0 ? LEVEL_COLORS[n - 1] : 'inactive'}>{eff === '' ? 'none' : eff}</Text>
367        </Text>
368        <Text wrap="truncate-end">
369          <Text dimColor>{'Style   '}</Text>
370          <Text>{style}</Text>
371        </Text>
372        <Text> </Text>
373        <Text bold color={GOLD}>{heading('Load', width)}</Text>
374        <Box flexDirection="row">
375          {filled > 0 && (
376            <Text color="white" backgroundColor={color} bold>
377              {text.slice(0, filled)}
378            </Text>
379          )}
380          {filled < text.length && (
381            <Text color="white" backgroundColor={TRACK}>
382              {text.slice(filled)}
383            </Text>
384          )}
385        </Box>
386        <Text wrap="truncate-end" color={busy.length > 0 ? 'success' : 'inactive'}>
387          {'Subagents  '}
388          {busy.length > 0 ? `${busy.length} running` : 'none running'}
389          {all.length > busy.length ? `, ${all.length - busy.length} done` : ''}
390        </Text>
391        {busy.slice(0, MAX_AGENT_ROWS).map(a => (
392          <Text key={a.id} wrap="truncate-end" dimColor>
393            {`${a.type}: ${a.description}`}
394          </Text>
395        ))}
396        {busy.length > MAX_AGENT_ROWS && <Text dimColor>{`and ${busy.length - MAX_AGENT_ROWS} more`}</Text>}
397        <Text wrap="truncate-end">
398          <Text dimColor>{'Main calls      '}</Text>
399          <Text color="success">{`${tally.main.ok} ok`}</Text>
400          <Text color={RED}>{`  ${tally.main.failed} failed`}</Text>
401        </Text>
402        <Text wrap="truncate-end">
403          <Text dimColor>{'Subagent calls  '}</Text>
404          <Text color="success">{`${tally.sub.ok} ok`}</Text>
405          <Text color={RED}>{`  ${tally.sub.failed} failed`}</Text>
406        </Text>
407        {used.slice(0, MAX_TOOL_ROWS).map(([name, count]) => (
408          <Text key={name} wrap="truncate-end">
409            <Text dimColor>{name.padEnd(16)}</Text>
410            <Text>{String(count)}</Text>
411          </Text>
412        ))}
413        {used.length > MAX_TOOL_ROWS && <Text dimColor>{`and ${used.length - MAX_TOOL_ROWS} more tools`}</Text>}
414        {(tally.recent ?? []).length > 0 && <Text dimColor>Last calls</Text>}
415        {[...(tally.recent ?? [])].reverse().map((r, i) => (
416          <Text key={i} wrap="truncate-end" color={r.ok ? 'success' : RED}>
417            {`${r.ok ? '✓' : '✗'} ${r.tool}`}
418            {r.sub ? <Text dimColor>{'  subagent'}</Text> : null}
419          </Text>
420        ))}
421        <Text> </Text>
422        <Text bold color={GOLD}>{heading('Treasury', width)}</Text>
423        {spent !== null && (
424          <Text wrap="truncate-end">
425            <Text color={GOLD}>{'● '}</Text>
426            <Text bold color={GOLD}>{dollars(spent)}</Text>
427            <Text dimColor>{'  this session'}</Text>
428          </Text>
429        )}
430        {windows.map(w => {
431          const left = ringsLeft(w.percentUsed)
432          const reset = w.resetsAt === null ? null : Date.parse(w.resetsAt) - t
433          return (
434            <Box key={w.kind} flexDirection="column">
435              <Text dimColor>{limitName(w.kind)}</Text>
436              <Text>
437                <Text color={GOLD}>{'● '.repeat(left)}</Text>
438                <Text color={TRACK}>{'○ '.repeat(RINGS - left)}</Text>
439              </Text>
440              <Text wrap="truncate-end">
441                {`${w.percentUsed}% used`}
442                {reset !== null && reset > 0 ? `, resets in ${until(reset)}` : ''}
443              </Text>
444            </Box>
445          )
446        })}
447        {spent === null && windows.length === 0 && <Text dimColor>no data yet</Text>}
448        <Text> </Text>
449        <Text bold color={GOLD}>{heading('Cache', width)}</Text>
450        <Text dimColor>Last request</Text>
451        <Text wrap="truncate-end">{lastTrack}</Text>
452        <Text color={hitColor(lastHit)} wrap="truncate-end">{fmtHit(lastHit)}</Text>
453        <Text dimColor>Session</Text>
454        <Text wrap="truncate-end">{totalTrack}</Text>
455        <Text color={hitColor(totalHit)} wrap="truncate-end">{fmtHit(totalHit)}</Text>
456        <Text wrap="truncate-end">
457          <Text dimColor>{'Expires  '}</Text>
458          <Text color={expiryColor}>{expiry}</Text>
459        </Text>
460      </Box>
461    )
462    } catch (err) {
463      // Show the failure in the pane. A silent pane hides the cause.
464      return <Text color={RED}>{`HUD error: ${err instanceof Error ? err.message : String(err)}`}</Text>
465    }
466  })
467}
468
types/index.d.ts 69 lines
1export type HudContext = { percent: number | null; tokens: number | null; window: number | null }
2
3export type HudCache = {
4  /** Input tokens of the last main-loop response, by kind. */
5  lastRead: number
6  lastWrite: number
7  lastUncached: number
8  /** Input tokens of all main-loop responses in this session, by kind. */
9  totalRead: number
10  totalWrite: number
11  totalUncached: number
12  /** Epoch milliseconds of the last main-loop response; null before the first. */
13  lastResponseAt: number | null
14}
15
16export type HudLimit = {
17  /** The window: `five_hour`, `seven_day`, `spend_limit`. */
18  kind: string
19  /** Share of the window used, 0 to 100. */
20  percentUsed: number
21  /** ISO 8601 reset time; null when the engine gives none. */
22  resetsAt: string | null
23}
24
25export type HudAgent = {
26  id: string
27  /** The agent type: `general-purpose`, `Explore`, `teammate`. */
28  type: string
29  /** The few words the Agent call gave for the task. */
30  description: string
31  /** `pending`, `running`, `waiting`, `idle`, `completed`, `failed` or `killed`. */
32  status: string
33}
34
35export type HudCalls = {
36  /** Tool calls of the main loop. */
37  main: { ok: number; failed: number }
38  /** Tool calls of all subagents together. */
39  sub: { ok: number; failed: number }
40  /** Calls of each tool, main loop and subagents together. */
41  tools: Record<string, number>
42  /** The last calls, the newest last. */
43  recent: HudRecentCall[]
44}
45
46export type HudRecentCall = { tool: string; ok: boolean; sub: boolean }
47
48declare module 'claude-code' {
49  interface PluginState {
50    'session-hud': {
51      model: string
52      /** The effort of the last main-loop request; empty before the first, or for a model without effort. */
53      effort: string
54      outputStyle: string
55      context: HudContext
56      cache: HudCache
57      now: number
58      /** Session cost in US dollars; null when the host keeps no ledger. */
59      cost: number | null
60      /** Rate-limit windows; empty off a subscription. */
61      limits: HudLimit[]
62      /** The agents of the session, subagents and teammates, as the last timer tick read them. */
63      agents: HudAgent[]
64      /** Tool calls, counted by who made them and by the result. */
65      calls: HudCalls
66    }
67  }
68}
69