SLOPSHOPPER

cost-visibility

Live cost, context, and plan-quota meters above the Claude Code prompt (Nerd Font required for icons). Type /spend for the detail pane. Optional cost guards…

newpanebandspinnerguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cost-visibility
│ ┃ Cost & usage ✕ › fix the failing auth test and add an audit log call │ ┃ │ ┃ Money this session ⏺ Read(src/auth.ts) │ ┃ ⎿ Read 6 lines │ ┃ Estimated API list price (same yardstick as ⏺ Update(src/auth.ts) │ ┃ cost.usd). On a Team subscription this is a ⎿ Added 2 lines, removed 1 line │ ┃ meter, not your invoice. ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ | | USD | │ ┃ |---|---:| ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ | Whole session | $0.42 | │ ┃ | Main thread | $0.00 | ✻ Worked for 42s · done 4:20 PM │ ┃ | Subagents (sum) | $0.00 | │ ┃ | Subagents spawned | 0 | › /spend │ ┃ | Tool calls | 9 | ⎿ cost-visibility:  5h ██░░░░ 31% now · this chat $0.42 · agents │ ┃ ⎿ cost-visibility: Guards OFF · pane open · /spend close · /spend │ ┃ Context window │ ┃ │ ┃ • 49% filled — 97k / 200k tokens │ ┃ • Soft target: stay under ~70–85% (auto-com │ ┃ │ ┃ Plan / rate limits │ ┃ │ ┃ •  5-hour plan window — ok at 31% ███░░░░░ │ ┃ refreshes in now (10/9/2025, 9:53:20 AM) │ ┃ ✻ Thinking ·  $0.42 ·  49% ·  31%…  5h ██░░░░░░ 31% · now  $0.42 │  main $0.00 │  0 agents │  49% ██░░░ 97k/200k │  9 /spend ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
 5h ██░░░░░░ 31% · now  $0.42 │  main $0.00 │  0 agents │  49% ██░░░ 97k/200k │  9 /spend
Pane · Cost & usage
Money this session Estimated API list price (same yardstick as /cost / cost.usd). On a Team subscription this is a burn meter, not your invoice. | | USD | |---|---:| | Whole session | $0.42 | | Main thread | $0.00 | | Subagents (sum) | $0.00 | | Subagents spawned | 0 | | Tool calls | 9 | Context window • 49% filled — 97k / 200k tokens • Soft target: stay under ~70–85% (auto-compact ~150k) Plan / rate limits •  5-hour plan window — ok at 31% ███░░░░░░░ refreshes in now (10/9/2025, 9:53:20 AM) Subagents No subagents spawned this session. Cost guards OFF (default) — HUD only, no blocks from this mod. Toggle: /spend caps on · /spend caps off · /config Org **monthly** spend: Team Owners → claude.ai → Usage. • Session [object Promise] • Model [object Promise] [ Refresh ] [ Close ] Tip: dollars are list-price estimates. Shell `claude-cost` has multi-session history.
README

cost-visibility

Plugin package for claude-mod-cost-visibility.

Requires a Nerd Font as your terminal font for icons (meters work without it).

# from repo root
./scripts/check-nerd-font.sh
./install.sh
claude plugin validate .

See the root README for full install and /spend.

Source 1 files
hooks/register.js 705 lines
1// Cost / context visibility mod for Claude Code (UI band + optional pane).
2// Requires Claude Code >= 2.1.287.
3// Note: "/cost" is reserved (alias of built-in /usage) — use /spend for the pane.
4
5const PANE_ID = 'cost-visibility'
6const CAPS_CONFIG_KEY = 'cost-visibility.costGuards'
7const CAPS_STORE_KEY = 'costGuards'
8
9const LIMIT_LABEL = {
10  five_hour: '5h',
11  seven_day: 'week',
12  spend_limit: 'spend',
13}
14
15const LIMIT_LABEL_LONG = {
16  five_hour: '5-hour plan window',
17  seven_day: '7-day week window',
18  spend_limit: 'spend cap',
19}
20
21// Nerd Fonts (Font Awesome). Needs a Nerd Font in the terminal.
22// Avoid fa-memory (\uf538) — missing in some NF builds (renders as tofu).
23const I = {
24  clock: '\uf017', // fa-clock-o
25  calendar: '\uf073', // fa-calendar
26  usd: '\uf155', // fa-usd
27  users: '\uf0c0', // fa-users
28  ctx: '\uf1c0', // fa-database (context window)
29  wrench: '\uf0ad', // fa-wrench
30  bolt: '\uf0e7', // fa-bolt
31  warn: '\uf071', // fa-warning
32  circle: '\uf111', // fa-circle
33}
34
35function fmtUsd(n) {
36  if (n == null || Number.isNaN(n)) return '—'
37  if (n === 0) return '$0.00'
38  if (n < 0.01) return '<$0.01'
39  if (n < 10) return '$' + n.toFixed(2)
40  return '$' + n.toFixed(1)
41}
42
43function fmtTok(n) {
44  if (n == null) return '—'
45  if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'
46  if (n >= 1e3) return (n / 1e3).toFixed(0) + 'k'
47  return String(n)
48}
49
50function costUsd(usage) {
51  const c = usage?.cost
52  if (typeof c === 'number') return c
53  if (c && typeof c.usd === 'number') return c.usd
54  if (c && typeof c.total === 'number') return c.total
55  return null
56}
57
58function limitLabel(kind) {
59  return LIMIT_LABEL[kind] || kind || 'limit'
60}
61
62function limitLabelLong(kind) {
63  return LIMIT_LABEL_LONG[kind] || kind || 'limit'
64}
65
66function limitIcon(kind) {
67  if (kind === 'seven_day') return I.calendar
68  if (kind === 'spend_limit') return I.usd
69  return I.clock
70}
71
72function barParts(pct, width) {
73  const w = width || 8
74  const raw = Number(pct) || 0
75  const capped = Math.max(0, Math.min(100, raw))
76  const filled = Math.round((capped / 100) * w)
77  return {
78    filled: '█'.repeat(filled),
79    empty: '░'.repeat(w - filled),
80    over: raw > 100,
81  }
82}
83
84function bar(pct, width) {
85  const { filled, empty } = barParts(pct, width)
86  return filled + empty
87}
88
89function untilReset(iso) {
90  if (!iso) return null
91  const ms = new Date(iso).getTime() - Date.now()
92  if (Number.isNaN(ms)) return null
93  if (ms <= 0) return 'now'
94  const m = Math.floor(ms / 60000)
95  const h = Math.floor(m / 60)
96  const d = Math.floor(h / 24)
97  if (d >= 1) return d + 'd' + (h % 24 ? h % 24 + 'h' : '')
98  if (h >= 1) return h + 'h' + (m % 60 ? m % 60 + 'm' : '')
99  return m + 'm'
100}
101
102/** Soft label for the pane only — never shouted in the band. */
103function statusWord(pct) {
104  if (pct == null) return 'unknown'
105  if (pct >= 100) return 'full'
106  if (pct >= 85) return 'tight'
107  if (pct >= 70) return 'warm'
108  return 'ok'
109}
110
111function pctColor(pct) {
112  if (pct == null) return 'subtle'
113  if (pct >= 100) return 'error'
114  if (pct >= 85) return 'error'
115  if (pct >= 70) return 'warning'
116  return 'success'
117}
118
119function formatLimitShort(lim) {
120  const p = Math.round(lim.percentUsed ?? 0)
121  const reset = untilReset(lim.resetsAt)
122  const { filled, empty } = barParts(p, 6)
123  return (
124    limitIcon(lim.kind) +
125    ' ' +
126    limitLabel(lim.kind) +
127    ' ' +
128    filled +
129    empty +
130    ' ' +
131    p +
132    '%' +
133    (reset ? ' ' + reset : '')
134  )
135}
136
137/** Colored meter chip: icon + label + bar + % + countdown */
138function meterChip(Text, Box, lim, barWidth) {
139  const p = Math.round(lim.percentUsed ?? 0)
140  const color = pctColor(p)
141  const { filled, empty, over } = barParts(p, barWidth || 8)
142  const reset = untilReset(lim.resetsAt)
143  const kids = [
144    el(Text, { color }, (over ? I.bolt + ' ' : '') + limitIcon(lim.kind)),
145    el(Text, { color }, ' ' + limitLabel(lim.kind) + ' '),
146    el(Text, { color }, filled),
147    el(Text, { color: 'inactive' }, empty),
148    el(Text, { color, bold: p >= 85 }, ' ' + p + '%'),
149  ]
150  if (reset) kids.push(el(Text, { color: 'subtle' }, ' · ' + reset))
151  return el(Box, null, ...kids)
152}
153
154function sep(Text) {
155  return el(Text, { color: 'inactive' }, '  │  ')
156}
157
158function burnChip(Text, Box, icon, color, label) {
159  return el(
160    Box,
161    null,
162    el(Text, { color: color || 'subtle' }, icon + ' '),
163    el(Text, { color: color || 'text' }, label),
164  )
165}
166
167async function readUsage($) {
168  try {
169    return await $.session.usage()
170  } catch {
171    return null
172  }
173}
174
175function el(factory, props, ...children) {
176  const p = props ? { ...props } : {}
177  if (children.length === 1) p.children = children[0]
178  else if (children.length > 1) p.children = children
179  return factory(p)
180}
181
182async function openPane($) {
183  const result = await $.ui.open({
184    id: PANE_ID,
185    title: 'Cost & usage',
186    placement: 'dock',
187    rows: 24,
188  })
189  $.ui.invalidate('ui.render')
190  return result
191}
192
193/** Persist costGuards via userConfig; store is a same-session fallback. */
194async function writeCapsConfig($, enabled) {
195  try {
196    $.store.set(CAPS_STORE_KEY, enabled === true)
197  } catch {
198    /* optional */
199  }
200  try {
201    const result = await $.config.set({
202      key: CAPS_CONFIG_KEY,
203      value: enabled === true,
204    })
205    if (result?.deny) {
206      return { ok: false, reason: String(result.deny), persisted: 'denied' }
207    }
208    return { ok: true, persisted: 'config' }
209  } catch (err) {
210    return {
211      ok: true,
212      persisted: 'session-store',
213      reason: err?.message || 'config.set unavailable; kept for this session via store',
214    }
215  }
216}
217
218export function register(on, raw = {}) {
219  let lastUsage = null
220  let toolCalls = 0
221  let agentSpawns = 0
222  let lastCostSeen = 0
223  let mainCostUsd = 0
224  let agentCostUsd = 0
225  /** Live flag — default off (HUD only). Synced from userConfig / /spend caps / /config. */
226  let capsOn = raw?.costGuards === true
227  /** @type {Map<string, { type: string, description: string, usd: number, turns: number }>} */
228  const agents = new Map()
229
230  function noteCostDelta(usage, agentId) {
231    const total = costUsd(usage)
232    if (total == null) return
233    const delta = Math.max(0, total - lastCostSeen)
234    lastCostSeen = total
235    if (delta === 0) return
236    if (agentId) {
237      agentCostUsd += delta
238      const row = agents.get(agentId)
239      if (row) {
240        row.usd += delta
241        row.turns += 1
242      }
243    } else {
244      mainCostUsd += delta
245    }
246  }
247
248  on('session.start', async ($, e, next) => {
249    try {
250      await $.command.register({
251        name: 'spend',
252        description:
253          'Cost HUD pane. /spend · /spend close · /spend caps [on|off]',
254        argumentHint: '[close|caps on|caps off]',
255      })
256    } catch {
257      /* optional */
258    }
259
260    // Options from manifest win; store covers in-session toggle before reload.
261    try {
262      if (raw?.costGuards === true) capsOn = true
263      else if (raw?.costGuards === false) capsOn = false
264      else if ($.store.get(CAPS_STORE_KEY) === true) capsOn = true
265    } catch {
266      /* keep capsOn */
267    }
268
269    lastUsage = await readUsage($)
270    const total = costUsd(lastUsage)
271    if (total != null) lastCostSeen = total
272
273    // Icons need a Nerd Font in the terminal — remind once per machine.
274    try {
275      const key = 'nerdFontHintShown'
276      if (!$.store.get(key)) {
277        $.store.set(key, true)
278        await $.ui.toast(
279          'cost-visibility: set a Nerd Font as your terminal font for icons (meters work either way). See README.',
280          { timeoutMs: 8000 },
281        )
282      }
283    } catch {
284      /* toast/store unavailable — non-fatal */
285    }
286
287    $.ui.invalidate('ui.render')
288    return next(e)
289  })
290
291  // Keep live flag in sync when the person toggles via /config UI.
292  on('config.set', { key: CAPS_CONFIG_KEY }, async ($, e, next) => {
293    const result = await next(e)
294    capsOn = (result?.value ?? e.value) === true
295    try {
296      $.store.set(CAPS_STORE_KEY, capsOn)
297    } catch {
298      /* optional */
299    }
300    $.ui.invalidate('ui.render')
301    return result
302  }).catch((_$, e, next) => next(e))
303
304  on('session.measure', async ($, e, next) => {
305    lastUsage = await readUsage($)
306    $.ui.invalidate('ui.render')
307    return next(e)
308  })
309
310  on('turn.complete', async ($, e, next) => {
311    lastUsage = await readUsage($)
312    noteCostDelta(lastUsage, e.agentId)
313    $.ui.invalidate('ui.render')
314    return next(e)
315  })
316
317  on('tool.call', async ($, e, next) => {
318    toolCalls += 1
319    $.ui.invalidate('ui.render')
320    return next(e)
321  }).catch(() => {})
322
323  on('agent.spawn', async ($, e, next) => {
324    agentSpawns += 1
325    if (capsOn) {
326      const desc = String(e.description || e.prompt || '').toLowerCase()
327      const bg =
328        e.runInBackground === true ||
329        e.run_in_background === true ||
330        e.background === true
331      if (
332        desc.includes('code-review max') ||
333        desc.includes('5+5 angles') ||
334        desc.includes('multi-angle') ||
335        /angle [a-e]:/.test(desc)
336      ) {
337        return {
338          deny: 'cost-visibility: multi-angle / code-review max agents banned (cost guards on). Use /code-review medium|high, or /spend caps off.',
339        }
340      }
341      if (bg) {
342        return {
343          deny: 'cost-visibility: background agents disabled (cost guards on). /spend caps off to allow.',
344        }
345      }
346    }
347    const result = await next(e)
348    const id = result?.agentId || result?.id
349    if (id) {
350      agents.set(id, {
351        type: String(e.subagentType || e.type || 'agent'),
352        description: String(e.description || '').slice(0, 80),
353        usd: 0,
354        turns: 0,
355      })
356    }
357    $.ui.invalidate('ui.render')
358    return result
359  }).catch(($, e, next) => {
360    if (capsOn) {
361      return {
362        deny: 'cost-visibility: agent.spawn hook error — denying spawn (cost guards on, fail-closed)',
363      }
364    }
365    return next(e)
366  })
367
368  on('prompt.submit', async ($, e, next) => {
369    if (!capsOn) return next(e)
370    const text = String(e.text || '').toLowerCase()
371    if (
372      (text.includes('/code-review') || text.includes('/review')) &&
373      (/\bxhigh\b/.test(text) ||
374        /\bultra\b/.test(text) ||
375        text.includes('/code-review max') ||
376        /(^|[\s])max([\s]|$)/.test(text) ||
377        text.includes('braba') ||
378        text.includes('5+5'))
379    ) {
380      return {
381        drop: 'cost-visibility: /code-review max|xhigh|ultra|braba blocked (cost guards on). Try /code-review medium, or /spend caps off.',
382      }
383    }
384    return next(e)
385  }).catch((_$, _e, next) => next)
386
387  on('command.run', { command: 'spend' }, async ($, e) => {
388    lastUsage = await readUsage($)
389    const args = String(e.args || '').trim()
390    const words = args.split(/\s+/).filter(Boolean)
391
392    if (words[0] === 'close') {
393      await $.ui.close(PANE_ID)
394      return { text: 'Closed cost pane.' }
395    }
396
397    if (words[0] === 'caps' || words[0] === 'guards') {
398      const action = (words[1] || 'status').toLowerCase()
399      if (action === 'status' || action === '') {
400        return {
401          text:
402            'Cost guards: ' +
403            (capsOn ? 'ON' : 'OFF') +
404            ' (default OFF = HUD only).\n' +
405            'Toggle: /spend caps on · /spend caps off\n' +
406            'Also: /config → cost-visibility → Cost guards',
407        }
408      }
409      if (action === 'on' || action === 'true' || action === '1') {
410        capsOn = true
411        const r = await writeCapsConfig($, true)
412        $.ui.invalidate('ui.render')
413        return {
414          text:
415            'Cost guards ON — blocking /code-review max|xhigh|ultra|braba, background agents, multi-angle fan-out.' +
416            (r.persisted === 'config' ? '' : '\n(' + (r.reason || 'session-only until config persists') + ')'),
417        }
418      }
419      if (action === 'off' || action === 'false' || action === '0') {
420        capsOn = false
421        const r = await writeCapsConfig($, false)
422        $.ui.invalidate('ui.render')
423        return {
424          text:
425            'Cost guards OFF — HUD only, no blocks from this mod.' +
426            (r.persisted === 'config' ? '' : '\n(' + (r.reason || 'session-only until config persists') + ')'),
427        }
428      }
429      return {
430        text: 'Usage: /spend caps [on|off|status]',
431      }
432    }
433
434    await openPane($)
435    return {
436      text:
437        summarize(lastUsage) +
438        '\nGuards ' +
439        (capsOn ? 'ON' : 'OFF') +
440        ' · pane open · /spend close · /spend caps on|off',
441    }
442  })
443
444  function summarize(usage) {
445    const total = costUsd(usage)
446    const ctx = usage?.context || {}
447    const parts = []
448    for (const lim of usage?.rateLimits || []) {
449      parts.push(formatLimitShort(lim))
450    }
451    parts.push('this chat ' + fmtUsd(total))
452    parts.push('agents ' + fmtUsd(agentCostUsd) + '×' + agentSpawns)
453    if (ctx.percent != null) {
454      parts.push('ctx ' + Math.round(ctx.percent) + '%')
455    }
456    return parts.join(' · ')
457  }
458
459  // Primary UX: always-on two-line band with colors + Nerd Font icons + bars.
460  // Line 1 = plan meters. Line 2 = this-chat burn + details.
461  on('ui.render', { component: 'AbovePrompt' }, async ($, e) => {
462    if (e.surface && e.surface !== 'terminal') return undefined
463    const { Text, Box } = $.ui.resolve(e)
464    const usage = lastUsage || (await readUsage($))
465    lastUsage = usage
466    const total = costUsd(usage)
467    const ctx = usage?.context || {}
468    const ctxPct = ctx.percent != null ? Math.round(ctx.percent) : null
469    const limits = usage?.rateLimits || []
470    const width = Math.max(40, e.props.bodyColumns || 80)
471    const narrow = width < 90
472    const barW = narrow ? 6 : 8
473
474    const five = limits.find((l) => l.kind === 'five_hour')
475    const week = limits.find((l) => l.kind === 'seven_day')
476    const spend = limits.find((l) => l.kind === 'spend_limit')
477    const ordered = [five, week, spend].filter(Boolean)
478
479    const planRow = ordered.length
480      ? el(
481          Box,
482          { width, paddingRight: 5 },
483          ...ordered.flatMap((lim, i) =>
484            i === 0
485              ? [meterChip(Text, Box, lim, barW)]
486              : [sep(Text), meterChip(Text, Box, lim, barW)],
487          ),
488        )
489      : el(
490          Text,
491          { color: 'subtle' },
492          I.clock + ' plan meters after the first reply',
493        )
494
495    const ctxColor = pctColor(ctxPct)
496    const ctxLabel =
497      ctxPct != null
498        ? ctxPct + '% ' + bar(ctxPct, 5) + ' ' + fmtTok(ctx.tokens) + '/' + fmtTok(ctx.window)
499        : '—'
500    const moneyColor = (total ?? 0) >= 5 ? 'warning' : (total ?? 0) > 0 ? 'claude' : 'subtle'
501    const agentColor = agentSpawns > 0 ? 'warning' : 'subtle'
502
503    const burnRow = el(
504      Box,
505      { justifyContent: 'space-between', paddingRight: 5, width },
506      el(
507        Box,
508        null,
509        burnChip(Text, Box, I.usd, moneyColor, fmtUsd(total ?? 0)),
510        sep(Text),
511        burnChip(
512          Text,
513          Box,
514          I.circle,
515          'subtle',
516          'main ' + fmtUsd(mainCostUsd),
517        ),
518        sep(Text),
519        burnChip(
520          Text,
521          Box,
522          I.users,
523          agentColor,
524          agentSpawns > 0
525            ? fmtUsd(agentCostUsd) + ' ×' + agentSpawns
526            : '0 agents',
527        ),
528        sep(Text),
529        burnChip(Text, Box, I.ctx, ctxColor, ctxLabel),
530        sep(Text),
531        burnChip(Text, Box, I.wrench, 'subtle', String(toolCalls)),
532      ),
533      el(Text, { color: 'suggestion' }, '/spend'),
534    )
535
536    return el(
537      Box,
538      { flexDirection: 'column', width, gap: 0 },
539      planRow,
540      burnRow,
541    )
542  })
543
544  on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
545    const usage = lastUsage
546    const total = costUsd(usage)
547    const pct = usage?.context?.percent
548    const five = usage?.rateLimits?.find((l) => l.kind === 'five_hour')
549    const suffix =
550      ' · ' +
551      I.usd +
552      ' ' +
553      fmtUsd(total) +
554      (pct != null ? ' · ' + I.ctx + ' ' + Math.round(pct) + '%' : '') +
555      (five
556        ? ' · ' + I.clock + ' ' + Math.round(five.percentUsed ?? 0) + '%'
557        : '') +
558      (agentSpawns ? ' · ' + I.users + ' ' + agentSpawns : '')
559    return next({ ...e, props: { ...e.props, suffix } })
560  })
561
562  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
563    if (e.requestId !== PANE_ID) return next(e)
564    const { Text, Box, Button, Markdown } = $.ui.resolve(e)
565    const usage = lastUsage || (await readUsage($))
566    lastUsage = usage
567    const ctx = usage?.context || {}
568    const limits = usage?.rateLimits || []
569    const total = costUsd(usage)
570    const ctxPct = ctx.percent != null ? Math.round(ctx.percent) : null
571
572    const limitMd = limits.length
573      ? limits
574          .map((l) => {
575            const p = Math.round(l.percentUsed ?? 0)
576            const resetAbs = l.resetsAt ? new Date(l.resetsAt).toLocaleString() : '?'
577            const resetRel = untilReset(l.resetsAt)
578            return (
579              '- ' +
580              limitIcon(l.kind) +
581              ' **' +
582              limitLabelLong(l.kind) +
583              '** — ' +
584              statusWord(p) +
585              ' at **' +
586              p +
587              '%** `' +
588              bar(p, 10) +
589              '`\n' +
590              '  refreshes in **' +
591              (resetRel || '?') +
592              '** (' +
593              resetAbs +
594              ')'
595            )
596          })
597          .join('\n')
598      : '_No plan-window reading yet (needs a reply on a Claude subscription)._'
599
600    const agentRows = [...agents.entries()]
601    const agentMd = agentRows.length
602      ? agentRows
603          .map(([id, a]) => {
604            return (
605              '- `' +
606              id.slice(0, 8) +
607              '` **' +
608              a.type +
609              '**: ' +
610              fmtUsd(a.usd) +
611              ' · ' +
612              a.turns +
613              ' turn(s)' +
614              (a.description ? ' — ' + a.description : '')
615            )
616          })
617          .join('\n')
618      : '_No subagents spawned this session._'
619
620    const md =
621      '## Money this session\n\n' +
622      'Estimated **API list price** (same yardstick as `/cost` / `cost.usd`). ' +
623      'On a Team subscription this is a burn meter, not your invoice.\n\n' +
624      '| | USD |\n|---|---:|\n' +
625      '| **Whole session** | **' +
626      fmtUsd(total) +
627      '** |\n' +
628      '| Main thread | ' +
629      fmtUsd(mainCostUsd) +
630      ' |\n' +
631      '| Subagents (sum) | ' +
632      fmtUsd(agentCostUsd) +
633      ' |\n' +
634      '| Subagents spawned | ' +
635      agentSpawns +
636      ' |\n' +
637      '| Tool calls | ' +
638      toolCalls +
639      ' |\n\n' +
640      '## Context window\n\n' +
641      (ctxPct != null
642        ? '- **' +
643          ctxPct +
644          '%** filled — ' +
645          fmtTok(ctx.tokens) +
646          ' / ' +
647          fmtTok(ctx.window) +
648          ' tokens\n- Soft target: stay under ~70–85% (auto-compact ~150k)\n'
649        : '_Waiting for the first model reply._\n') +
650      '\n## Plan / rate limits\n\n' +
651      limitMd +
652      '\n\n## Subagents\n\n' +
653      agentMd +
654      '\n\n## Cost guards\n\n' +
655      (capsOn
656        ? '**ON** — blocking `/code-review` max|xhigh|ultra|braba, background agents, multi-angle fan-out.\n'
657        : '**OFF** (default) — HUD only, no blocks from this mod.\n') +
658      'Toggle: `/spend caps on` · `/spend caps off` · `/config`\n\n' +
659      '_Org **monthly** spend: Team Owners → claude.ai → Usage._\n\n' +
660      '- Session `' +
661      $.session.id() +
662      '`\n' +
663      '- Model `' +
664      $.session.model() +
665      '`'
666
667    return el(
668      Box,
669      {
670        flexDirection: 'column',
671        gap: 1,
672        width: e.props.bodyColumns,
673        padding: 1,
674      },
675      el(Markdown, { text: md }),
676      el(
677        Box,
678        { gap: 1 },
679        el(Button, {
680          key: 'refresh',
681          label: 'Refresh',
682          hotkey: 'r',
683          onPress: async () => {
684            lastUsage = await readUsage($)
685            $.ui.invalidate('ui.render')
686          },
687        }),
688        el(Button, {
689          key: 'close',
690          label: 'Close',
691          hotkey: 'c',
692          onPress: async () => {
693            await $.ui.close(PANE_ID)
694          },
695        }),
696      ),
697      el(
698        Text,
699        { dimColor: true },
700        'Tip: dollars are list-price estimates. Shell `claude-cost` has multi-session history.',
701      ),
702    )
703  })
704}
705