SLOPSHOPPER

barometer

Context and plan-limit gauges in the band above the prompt

newbandtoastprocesstimer
v0.2.0MITupdated 2026-10-07Alchez/claude-mods/barometer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · barometer
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ 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 ⟨Claude Code's own drawing⟩ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Opus 5.5 | ? effort │ │ Context ━━━━━━━━━━━━━━━─────────╂────── 49% 97k/200k │ │ 5h ━━━━━━━━━╾───────────────────── 31% │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Opus 5.5 | ? effort │ │ Context ━━━━━━━━━━━━━━━─────────╂────── 49% 97k/200k │ │ 5h ━━━━━━━━━╾───────────────────── 31% │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
README

claude-mods

  • Claude Code mods, installable as a plugin marketplace.

barometer

  • Context and plan-limit gauges above the prompt.
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Context ━━━━━━━━━━━━╋───────  63%  630k/1M [ Compact ]                       you@example.com · Claude Max │
│ 5h      ━━━━━━━╾─╂──────────  38%  resets in 2h35m                                               Opus 5.5 │
│ Week    ━━━━━╾───╂──────────  27%  resets in 3d14h                             xhigh effort · thinking on │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  • Context fills green to red; past the red line, a Compact button appears between replies.
  • 5h and Week show plan usage; the notch is how far through the window you are, and turns orange once usage passes it.

Install

claude plugin marketplace add Alchez/claude-mods
claude plugin install barometer@alchez
  • Then start a new session, or run /reload-plugins.
  • Needs a Claude Code version with mods (tested on 2.1.289).

Settings

  • Set these in /config.
KeyDefaultWhat it does
redLine1M60Red line, as a context %, for 1M-token windows; 0 turns it off
redLine200k80Red line for smaller windows; 0 turns it off
alwaysShowCompactfalseShow the Compact button at any time, not only past the red line

Update or remove

claude plugin marketplace update alchez && claude plugin update barometer@alchez
claude plugin uninstall barometer@alchez

License

Source 2 files
hooks/register.tsx 471 lines
1import { atom, read, update } from 'claude-code'
2import type { ConfigRow, EngineInterface, PluginOptions, Register, RenderElement, SessionCompactResult, SessionRateLimit, SessionUsage } from 'claude-code'
3
4import type { Account, Effort, RememberedLimit } from '../types'
5
6const account = atom({ plugin: 'barometer', key: 'account' } as const, null as Account)
7const isCompacting = atom({ plugin: 'barometer', key: 'isCompacting' } as const, false)
8const compactedTokens = atom({ plugin: 'barometer', key: 'compactedTokens' } as const, null as number | null)
9const effort = atom({ plugin: 'barometer', key: 'effort' } as const, null as Effort)
10const lastLimits = atom({ plugin: 'barometer', key: 'lastLimits' } as const, [] as RememberedLimit[])
11
12const MINUTE = 60_000
13const HOUR = 60 * MINUTE
14const DAY = 24 * HOUR
15
16// xterm-256 colors 46, 118, 226, 208, 196, 117 and 250, as hex
17const GREEN = '#00ff00'
18const YELLOW_GREEN = '#87ff00'
19const YELLOW = '#ffff00'
20const ORANGE = '#ff8700'
21const RED = '#ff0000'
22const ACCOUNT = '#87d7ff'
23const ELAPSED = '#bcbcbc'
24
25const FILLED = '━'
26// Heavy left half, light right half: a fill that ends mid-cell
27const HALF_FILLED = '╾'
28const EMPTY = '─'
29const MARK_ON_FILLED = '╋'
30const MARK_ON_EMPTY = '╂'
31
32const REFRESH_MS = 5_000
33const LABEL_COLUMNS = 8
34const PERCENT_COLUMNS = 5
35const COMPACT_BELOW_COLUMNS = 60
36const FOLDED_GAP_COLUMNS = 3
37const PAD_FROM_ROWS = 5
38const FRAME_FROM_ROWS = 6
39const FRAME_ROWS = 2
40const FRAME_MARGIN_LEFT = 2
41const FRAME_PADDING_X = 1
42const FRAME_COLUMNS = FRAME_MARGIN_LEFT + 2 + 2 * FRAME_PADDING_X
43const IDENTITY_COLUMN_FROM = 100
44const PROJECT_AFTER_ELAPSED_PERCENT = 5
45
46const PLAN_WINDOWS = [
47  { kind: 'five_hour', label: '5h', lengthMs: 5 * HOUR },
48  { kind: 'seven_day', label: 'Week', lengthMs: 7 * DAY },
49] as const
50
51type Style = { color?: string; dimColor?: true }
52type Cell = Style & { glyph: string }
53type Marker = { atPercent: number; color: string }
54type Pace = { resetsInMs?: number; elapsedPercent?: number; runsOutInMs?: number }
55type Plan = (typeof PLAN_WINDOWS)[number] & { limit: SessionRateLimit; pace: Pace }
56// null: the person set 0, which turns that red line off
57type RedLines = { million: number | null; standard: number | null }
58
59const GRADIENT = [GREEN, YELLOW_GREEN, YELLOW, ORANGE]
60const PLAN_LIMITS = [50, 70, 85, 94] as const
61// Each context colour ends at this share of the red line: 60 gives 30/45/52/60, 80 gives 40/60/70/80
62const CONTEXT_STEPS_OF_RED_LINE = [0.5, 0.75, 0.875, 1] as const
63
64function gradient(percent: number, limits: readonly number[]): string {
65  const step = limits.findIndex(limit => Math.round(percent) <= limit)
66  return GRADIENT[step] ?? RED
67}
68
69function redLineOption(options: PluginOptions, name: string): number | null {
70  const value = options[name]
71  if (typeof value !== 'number') throw new Error(`barometer: option ${name} should be a number, got ${JSON.stringify(value)}`)
72  return value === 0 ? null : value
73}
74
75function booleanOption(options: PluginOptions, name: string): boolean {
76  const value = options[name]
77  if (typeof value !== 'boolean') throw new Error(`barometer: option ${name} should be true or false, got ${JSON.stringify(value)}`)
78  return value
79}
80
81const planColor = (percent: number) => gradient(percent, PLAN_LIMITS)
82const redLine = (window: number, redLines: RedLines) => (window >= 1_000_000 ? redLines.million : redLines.standard)
83// undefined with the red line off: the bar and percent keep the terminal's own text colour
84function contextColor(percent: number, window: number, redLines: RedLines): string | undefined {
85  const line = redLine(window, redLines)
86  if (line === null) return undefined
87  return gradient(
88    percent,
89    CONTEXT_STEPS_OF_RED_LINE.map(share => Math.floor(line * share)),
90  )
91}
92const colorOf = (color: string | undefined) => (color === undefined ? {} : { color })
93const clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value))
94
95function duration(ms: number): string {
96  const days = Math.floor(ms / DAY)
97  const hours = Math.floor((ms % DAY) / HOUR)
98  const minutes = Math.floor((ms % HOUR) / MINUTE)
99  if (days > 0) return `${days}d${hours}h`
100  if (hours > 0) return `${hours}h${String(minutes).padStart(2, '0')}m`
101  return `${minutes}m`
102}
103
104function tokenCount(count: number): string {
105  if (count >= 1_000_000) return `${Number((count / 1_000_000).toFixed(1))}M`
106  if (count >= 1_000) return `${Math.round(count / 1_000)}k`
107  return String(count)
108}
109
110function modelName(raw: string): string {
111  if (/\s/.test(raw)) return raw
112  const [family, ...version] = raw
113    .replace(/^claude-/, '')
114    .replace(/\[1m\]$/i, '')
115    .split('-')
116    .filter(part => !/^\d{8}$/.test(part))
117  if (!family) return raw
118  const name = family.charAt(0).toUpperCase() + family.slice(1)
119  return version.length > 0 ? `${name} ${version.join('.')}` : name
120}
121
122function reasoningLine(reading: Effort, isThinking: boolean | undefined): string | null {
123  const parts: string[] = []
124  if (reading === null) parts.push('? effort')
125  else if (reading.level !== null) parts.push(`${reading.level} effort`)
126  if (isThinking !== undefined) parts.push(`thinking ${isThinking ? 'on' : 'off'}`)
127  return parts.length > 0 ? parts.join(' · ') : null
128}
129
130function pace(limit: SessionRateLimit, lengthMs: number, now: number): Pace {
131  const resetsAt = limit.resetsAt === undefined ? Number.NaN : Date.parse(limit.resetsAt)
132  if (!Number.isFinite(resetsAt) || resetsAt <= now) return {}
133  const startedAt = resetsAt - lengthMs
134  const elapsedMs = Math.max(0, now - startedAt)
135  const elapsedPercent = Math.min(100, (elapsedMs / lengthMs) * 100)
136  const known = { resetsInMs: resetsAt - now, elapsedPercent }
137  const canProject = elapsedPercent >= PROJECT_AFTER_ELAPSED_PERCENT && limit.percentUsed > 0 && limit.percentUsed < 100
138  if (!canProject) return known
139  const runsOutAt = startedAt + (elapsedMs / limit.percentUsed) * 100
140  return runsOutAt < resetsAt ? { ...known, runsOutInMs: runsOutAt - now } : known
141}
142
143function meterCells(columns: number, percent: number | undefined, color: string | undefined, marker?: Marker): Cell[] {
144  const filledHalves = percent === undefined ? 0 : Math.round((clamp(percent, 0, 100) / 100) * columns * 2)
145  const markerAt = marker === undefined ? -1 : Math.min(columns - 1, Math.floor((marker.atPercent / 100) * columns))
146  return Array.from({ length: columns }, (_, index): Cell => {
147    const halvesHere = clamp(filledHalves - index * 2, 0, 2)
148    if (marker !== undefined && index === markerAt) {
149      return { glyph: halvesHere > 0 ? MARK_ON_FILLED : MARK_ON_EMPTY, color: marker.color }
150    }
151    if (halvesHere === 2) return { glyph: FILLED, ...colorOf(color) }
152    if (halvesHere === 1) return { glyph: HALF_FILLED, ...colorOf(color) }
153    return { glyph: EMPTY, dimColor: true }
154  })
155}
156
157function runs(cells: readonly Cell[]): Array<{ style: Style; glyphs: string }> {
158  const grouped: Array<{ style: Style; glyphs: string }> = []
159  for (const { glyph, ...style } of cells) {
160    const last = grouped.at(-1)
161    const isSameStyle = last !== undefined && last.style.color === style.color && last.style.dimColor === style.dimColor
162    if (isSameStyle) last.glyphs += glyph
163    else grouped.push({ style, glyphs: glyph })
164  }
165  return grouped
166}
167
168function nextReset(resetsAt: number, lengthMs: number, now: number): number {
169  let next = resetsAt
170  while (next <= now) next += lengthMs
171  return next
172}
173
174// A window past its reset is drawn as the fresh one, starting where the old one ended: what a new
175// session reads from Claude Code then. A long-running session stops reporting the window instead.
176function plansOf(usage: SessionUsage, remembered: readonly RememberedLimit[], now: number): Plan[] {
177  return PLAN_WINDOWS.flatMap(window => {
178    const reported = usage.rateLimits.find(candidate => candidate.kind === window.kind)
179    const known = reported ?? remembered.find(candidate => candidate.kind === window.kind)
180    if (known === undefined) return []
181    const resetsAt = known.resetsAt === undefined ? Number.NaN : Date.parse(known.resetsAt)
182    const hasReset = Number.isFinite(resetsAt) && resetsAt <= now
183    if (reported === undefined && !hasReset) return []
184    const limit: SessionRateLimit = hasReset
185      ? { kind: known.kind, percentUsed: 0, resetsAt: new Date(nextReset(resetsAt, window.lengthMs, now)).toISOString() }
186      : known
187    return [{ ...window, limit, pace: pace(limit, window.lengthMs, now) }]
188  })
189}
190
191async function rememberLimits($: EngineInterface, reported: readonly SessionRateLimit[]) {
192  if (reported.length === 0) return
193  const known = await read($, lastLimits)
194  const merged = [...known.filter(limit => !reported.some(fresh => fresh.kind === limit.kind)), ...reported]
195  if (JSON.stringify(merged) !== JSON.stringify(known)) await update($, lastLimits, () => merged)
196}
197
198async function rememberCompaction($: EngineInterface, result: SessionCompactResult) {
199  if (result.skip !== undefined) return
200  await update($, compactedTokens, () => result.tokensAfter ?? null)
201}
202
203async function refreshAccount($: EngineInterface) {
204  try {
205    const status = await $.process.run(['claude', 'auth', 'status', '--text'], { timeoutMs: 10_000 })
206    const email = /^Email: (.+)$/m.exec(status.stdout)?.[1]?.trim() ?? null
207    // "Login method: Claude Max account" names the plan as Claude Code's welcome banner does ("Claude Max")
208    const plan = /^Login method: (.+?)(?: account)?$/m.exec(status.stdout)?.[1]?.trim() ?? null
209    await update($, account, () => ({ email, plan }))
210  } catch {
211    // claude missing from PATH or the call timed out: the band leaves the account out
212  }
213}
214
215async function compactNow($: EngineInterface) {
216  if (await read($, isCompacting)) return
217  await update($, isCompacting, () => true)
218  try {
219    await rememberCompaction($, await $.session.compact())
220  } catch (error) {
221    $.ui.toast(`Compact failed: ${error instanceof Error ? error.message : String(error)}`)
222  } finally {
223    await update($, isCompacting, () => false)
224  }
225}
226
227export const register: Register = (on, options) => {
228  const redLines: RedLines = { million: redLineOption(options, 'redLine1M'), standard: redLineOption(options, 'redLine200k') }
229  const alwaysShowCompact = booleanOption(options, 'alwaysShowCompact')
230
231  on('session.start', async ($, e, next) => {
232    $.clock.after(0, () => void refreshAccount($))
233    $.clock.every(REFRESH_MS, async () => {
234      await rememberLimits($, (await $.session.usage()).rateLimits)
235      $.ui.invalidate('ui.render')
236    })
237    return next(e)
238  })
239
240  on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
241    $.clock.after(0, () => void refreshAccount($))
242    return next(e)
243  })
244
245  on('session.measure', async ($, e, next) => {
246    if (e.context.tokens !== undefined && (await read($, compactedTokens)) !== null) {
247      await update($, compactedTokens, () => null)
248    }
249    $.ui.invalidate('ui.render')
250    return next(e)
251  })
252
253  // Effort comes off the main conversation's requests: it has no /config row, and $.process.run's env carries no CLAUDE_EFFORT
254  on('turn.step', async function* ($, e, next) {
255    if (e.agentId === undefined) {
256      const level = e.effort === undefined ? null : typeof e.effort === 'number' ? tokenCount(e.effort) : e.effort
257      const known = await read($, effort)
258      if (known === null || known.level !== level) await update($, effort, () => ({ level }))
259    }
260    return yield* next(e)
261  })
262
263  on('command.run', { command: ['effort', 'model'] }, async ($, e, next) => {
264    const result = await next(e)
265    await update($, effort, () => null)
266    return result
267  })
268
269  on('session.compact', async ($, e, next) => {
270    const result = await next(e)
271    if (e.agentId === undefined && e.trigger !== 'precompute') await rememberCompaction($, result)
272    return result
273  })
274
275  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
276    const below = await next(e)
277    if (e.props.hasSurvey) return below
278
279    const [usage, rawModel, identity, compacting, compacted, effortReading, remembered, configRows, now] = await Promise.all([
280      $.session.usage(),
281      $.session.model(),
282      read($, account),
283      read($, isCompacting),
284      read($, compactedTokens),
285      read($, effort),
286      read($, lastLimits),
287      $.config.list().catch((): ConfigRow[] => []),
288      $.clock.now(),
289    ])
290    const { Box, Button, Text } = $.ui.resolve(e)
291    const { bodyColumns, isWorking, maxRows } = e.props
292    const model = modelName(rawModel)
293    const email = identity?.email ?? null
294    const plan = identity?.plan ?? null
295    const accountLength = (email?.length ?? 0) + (plan === null ? 0 : plan.length + (email === null ? 0 : 3))
296    const accountTexts: RenderElement[] = []
297    if (email !== null) accountTexts.push(<Text color={ACCOUNT}>{email}</Text>)
298    if (plan !== null) accountTexts.push(<Text dimColor>{email === null ? plan : ` · ${plan}`}</Text>)
299    const thinkingValue = configRows.find(row => row.key === 'thinking')?.value
300    const reasoning = reasoningLine(effortReading, typeof thinkingValue === 'boolean' ? thinkingValue : undefined)
301    const contextWindow = usage.context.window
302    const contextTokens = usage.context.tokens ?? compacted ?? undefined
303    const contextPercent =
304      usage.context.percent ?? (compacted === null ? undefined : Math.round((compacted / contextWindow) * 100))
305    const plans = plansOf(usage, remembered, now)
306    const showHint = plans.length === 0 && contextPercent === undefined
307    const isFramed = maxRows >= FRAME_FROM_ROWS
308    const columns = isFramed ? bodyColumns - FRAME_COLUMNS : bodyColumns
309    const isCompact = columns < COMPACT_BELOW_COLUMNS || maxRows < 3
310    const topPadding = maxRows - (isFramed ? FRAME_ROWS : 0) >= PAD_FROM_ROWS ? 1 : 0
311    // No bottom margin: Claude Code already leaves a blank row between the band and the prompt
312    const frame = (content: RenderElement) =>
313      isFramed ? (
314        <Box flexDirection="column" borderStyle="round" borderDimColor paddingX={FRAME_PADDING_X} marginLeft={FRAME_MARGIN_LEFT}>
315          {content}
316        </Box>
317      ) : (
318        content
319      )
320
321    // flexShrink 0 on the bar and its label and percent columns: only the detail text gives way when a row runs long
322    const strip = (cells: readonly Cell[]) => (
323      <Box flexDirection="row" flexShrink={0}>
324        {runs(cells).map(run => (
325          <Text {...run.style}>{run.glyphs}</Text>
326        ))}
327      </Box>
328    )
329    const separator = <Text dimColor>{' | '}</Text>
330    const hint = <Text dimColor>Rate: populates after first API call</Text>
331    const contextRedLine = redLine(contextWindow, redLines)
332    const isPastRedLine = contextPercent !== undefined && contextRedLine !== null && contextPercent >= contextRedLine
333    const compactButton =
334      (alwaysShowCompact || isPastRedLine) && !isWorking && !compacting ? [<Button key="compact" label="Compact" onPress={() => void compactNow($)} />] : []
335
336    if (isCompact) {
337      const parts: RenderElement[][] = []
338      if (columns >= IDENTITY_COLUMN_FROM) {
339        if (accountTexts.length > 0) parts.push(accountTexts)
340        parts.push([<Text dimColor>{model}</Text>])
341        if (reasoning !== null) parts.push([<Text dimColor>{reasoning}</Text>])
342      }
343      if (contextPercent !== undefined) {
344        parts.push([
345          <Text {...colorOf(contextColor(contextPercent, contextWindow, redLines))}>{`Context: ${contextPercent}%`}</Text>,
346          ...(compactButton.length > 0 ? [<Text> </Text>, ...compactButton] : []),
347        ])
348      } else if (compactButton.length > 0) {
349        parts.push(compactButton)
350      }
351      for (const plan of plans) {
352        const used = Math.round(plan.limit.percentUsed)
353        const reset = plan.pace.resetsInMs === undefined ? [] : [<Text dimColor>{` (resets in ${duration(plan.pace.resetsInMs)})`}</Text>]
354        parts.push([<Text color={planColor(used)}>{`${plan.label}: ${used}%`}</Text>, ...reset])
355      }
356      if (showHint) parts.push([hint])
357      return (
358        <Box flexDirection="column" paddingTop={topPadding}>
359          {below}
360          {frame(
361            // Spaced, not " | "-separated: a separator ends up dangling at the end of a wrapped line
362            <Box flexDirection="row" flexWrap="wrap" columnGap={FOLDED_GAP_COLUMNS}>
363              {parts.map(part => (
364                <Box flexDirection="row" flexShrink={0}>
365                  {part}
366                </Box>
367              ))}
368            </Box>,
369          )}
370        </Box>
371      )
372    }
373
374    const showIdentityColumn = columns >= IDENTITY_COLUMN_FROM
375    const identityColumns = showIdentityColumn ? Math.max(accountLength, model.length, reasoning?.length ?? 0) + 2 : 0
376    const available = columns - LABEL_COLUMNS - PERCENT_COLUMNS - 2 - identityColumns
377    const barColumns = clamp(Math.floor(available * 0.4), 10, 40)
378
379    const gauge = (label: string, cells: readonly Cell[], percent: RenderElement, detail: RenderElement[]) => (
380      <Box flexDirection="row">
381        <Box width={LABEL_COLUMNS} flexShrink={0}>
382          <Text>{label}</Text>
383        </Box>
384        {strip(cells)}
385        <Box width={PERCENT_COLUMNS} justifyContent="flex-end" flexShrink={0}>
386          {percent}
387        </Box>
388        <Box marginLeft={2} flexGrow={1} flexDirection="row" columnGap={1} overflow="hidden">
389          {detail}
390        </Box>
391      </Box>
392    )
393
394    const contextFill = contextColor(contextPercent ?? 0, contextWindow, redLines)
395    const tokensShown = contextTokens === undefined ? '?' : tokenCount(contextTokens)
396    const contextRow = gauge(
397      'Context',
398      meterCells(barColumns, contextPercent, contextFill, contextRedLine === null ? undefined : { atPercent: contextRedLine, color: RED }),
399      contextPercent === undefined ? <Text dimColor>–</Text> : <Text {...colorOf(contextFill)}>{`${contextPercent}%`}</Text>,
400      [<Text dimColor wrap="truncate-end">{`${tokensShown}/${tokenCount(contextWindow)}`}</Text>, ...compactButton],
401    )
402
403    const planRows = plans.map(plan => {
404      const used = Math.round(plan.limit.percentUsed)
405      const color = planColor(used)
406      const elapsed = plan.pace.elapsedPercent
407      const marker = elapsed === undefined ? undefined : { atPercent: elapsed, color: plan.limit.percentUsed > elapsed ? ORANGE : ELAPSED }
408      const detail: RenderElement[] = []
409      if (plan.pace.resetsInMs !== undefined) {
410        detail.push(
411          <Box flexShrink={0}>
412            <Text dimColor>{`resets in ${duration(plan.pace.resetsInMs)}`}</Text>
413          </Box>,
414        )
415      }
416      if (plan.pace.runsOutInMs !== undefined) {
417        detail.push(<Text dimColor>·</Text>)
418        detail.push(<Text color={ORANGE} wrap="truncate-end">{`out in ~${duration(plan.pace.runsOutInMs)} at this pace`}</Text>)
419      }
420      return gauge(plan.label, meterCells(barColumns, plan.limit.percentUsed, color, marker), <Text color={color}>{`${used}%`}</Text>, detail)
421    })
422
423    const gauges = (
424      <Box flexDirection="column" flexGrow={1}>
425        {contextRow}
426        {planRows}
427        {showHint ? hint : null}
428      </Box>
429    )
430    const identityColumn = (
431      <Box flexDirection="column" alignItems="flex-end" marginLeft={2} flexShrink={0}>
432        {accountTexts.length === 0 ? null : <Box flexDirection="row">{accountTexts}</Box>}
433        <Text dimColor>{model}</Text>
434        {reasoning === null ? null : <Text dimColor>{reasoning}</Text>}
435      </Box>
436    )
437    // One row, never two: the band's row budget counts it as one, so the tail truncates instead of wrapping
438    const identityRow =
439      !showIdentityColumn && maxRows >= 4 ? (
440        <Box flexDirection="row" overflow="hidden">
441          {accountTexts.length === 0 ? null : (
442            <Box flexDirection="row" flexShrink={0}>
443              {accountTexts}
444              {separator}
445            </Box>
446          )}
447          <Box flexDirection="row" flexShrink={0}>
448            <Text dimColor>{model}</Text>
449            {reasoning === null ? null : separator}
450          </Box>
451          {reasoning === null ? null : <Text dimColor wrap="truncate-end">{reasoning}</Text>}
452        </Box>
453      ) : null
454
455    return (
456      <Box flexDirection="column" paddingTop={topPadding}>
457        {below}
458        {frame(
459          <Box flexDirection="column">
460            {identityRow}
461            <Box flexDirection="row">
462              {gauges}
463              {showIdentityColumn ? identityColumn : null}
464            </Box>
465          </Box>,
466        )}
467      </Box>
468    )
469  })
470}
471
types/index.d.ts 21 lines
1// From `claude auth status --text`; null until it answers
2export type Account = { email: string | null; plan: string | null } | null
3
4// null until the main conversation's next model request; level null for a model that takes no effort
5export type Effort = { level: string | null } | null
6
7// The last reading of each plan window, kept because Claude Code stops reporting a window once it resets
8export type RememberedLimit = { kind: string; percentUsed: number; resetsAt?: string }
9
10declare module 'claude-code' {
11  interface PluginState {
12    barometer: {
13      account: Account
14      isCompacting: boolean
15      compactedTokens: number | null
16      effort: Effort
17      lastLimits: RememberedLimit[]
18    }
19  }
20}
21