SLOPSHOPPER

context-band

A band above the prompt showing the session's context usage, plan quota or cost

newbandcommandtimer
v0.3.0Apache-2.0updated 2026-10-08JinhaoFang/claude-mods/mods/context-band
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · context-band
› 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 › /context-band ⎿ context-band: Context band expanded. claude-opus-5-5 ○ /work/app[ Hide ][ ⟲ Compact ][ ⚙ ][ ▾ ] Context ██████░░░░░░ 49.0% of 200.0k · █░░ 31% ↺1h00m ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
claude-opus-5-5 ○ /work/app[ Hide ][ ⟲ Compact ][ ⚙ ][ ▾ ] Context ██████░░░░░░ 49.0% of 200.0k · █░░ 31% ↺1h00m
README

context-band

English | 中文

A Claude Code mod: a view-aware band above the prompt. An environment row (model, effort, working dot, directory) sits above a readings row (context gauge, plan quota, reset countdown, cost), and the band follows whichever conversation is on screen — the main conversation, or one agent loop's transcript from the tasks list.

Install

From the GitHub marketplace (type in a Claude Code terminal session):

/plugin install context-band --marketplace JinhaoFang/claude-mods

or from a local folder:

/plugin install context-band --marketplace /path/to/mods/context-band

Answer y to add the marketplace, pick the user scope, done — it goes live in the current session.

The band

Two rows:

  • Environment row — who and where: the model name with a five-cell effort scale beside it (filled low → max), a working dot (● running, ○ idle), the session directory in dim colour, and the button group Hide · ⟲ Compact · ⚙ · ▾. In an agent view the row leads with the viewed loop's identity label and shows that loop's model and effort instead.
  • Readings row — everything numeric: the context gauge, the plan quota bars, the reset countdown, and the cost. Nothing in this row truncates; on narrow terminals the directory gives way first — last two segments while they fit, then the last one — and the readings and buttons stay whole.

The context gauge

  • The percentage measures the compaction window — where auto-compact fires, below the model's theoretical limit — so the number reads as "how much conversation is left"
  • Colour thresholds default to <60% green, 60–85% yellow, ≥85% red; tune them in /config or in the band's settings area

Plan quota and cost

No hard-coded login-method check: when the engine reports subscription windows, the band draws the two quota bars — the 5-hour window in the suggestion colour, the 7-day window in remember, told apart by colour, no labels — with ↺ counting down to the nearest reset. When it reports none, the band shows this session's cost instead, with a gateway's spend-limit bar appended when reported.

Agent views

Open one agent loop's transcript from the tasks list and the band follows the view:

  • The gauge shows that loop's own context fill, labelled by its name, else its agent type, else a short id — so you always know whose numbers you are reading
  • Its model and effort scale replace the main conversation's, so a delegate on a different model stands out; the working dot follows that loop's status
  • The plan quota bars, the reset countdown, and the cost stay on the readings row — account-level readings belong to every view
  • A loop with no completed step yet shows a dim placeholder, never a number it does not have
  • The main conversation keeps the engine's native readings throughout; where the per-agent figures come from is recorded in ADR-0004

Teammates with a terminal pane of their own run a separate session with a band of its own; nothing here changes for them.

States

  • Expanded — the two-row band
  • Collapsed — ▾ folds the band into a pill, ◂ 44.4% — the on-screen view's percent, with the viewed loop's short label in an agent view; pressing the pill expands again
  • Hidden — Hide removes the band until you bring it back

Collapsed and hidden persist across sessions. The /context-band command restores the band, expanded.

Compact

⟲ Compact exists in the main conversation only — the engine's compact call has no per-agent form, so agent views draw no compact button at all. The first press turns it into Confirm; the second press compacts; a few seconds without the second press reverts it. A running turn finishes before compaction starts.

Settings

The ⚙ button swaps the readings row for the settings area; Done or a second ⚙ closes it.

  • Steppers: Warn and Error step by five, Refresh seconds steps by one; every value clamps
  • Warning always stays below error: raising warning lifts error out of the way, lowering error drops warning with it
  • Toggles: Model & effort, Reset ↺, Cost — each flips on press
  • A changed value lives in the plugin store and persists across sessions; a /config option applies only until its matching row is changed

A hot reload or a session restart closes the settings area; the values you changed stay.

Configuration

Rows named context-band.* in /config:

optiondefaultdescription
warningThreshold60Percent where colours turn to warning
errorThreshold85Percent where colours turn to error
refreshSeconds7Fallback redraw interval; turn ends update the band at once
showEfforttrueShow the model name and its effort scale
showCosttrueShow the session cost when no plan quota is reported
showResetIntrueShow the nearest plan-window reset countdown

Each option holds until the matching settings row is changed; the settings value then shadows it, and it stays shadowed until you change the row again.

Data source

The main conversation's figures come from the engine-native $.session.usage() — a free local call; turn ends push a redraw, and a fallback clock covers the gaps at refreshSeconds. An agent view's figures are accumulated from that loop's completed turn.step requests, because the engine exposes no per-agent usage — the reasoning is recorded in ADR-0004. The mod spawns no subprocesses and makes no network requests. The cost shown is what this session has spent — account-level totals are not reachable through the plugin API.

Usage

  • ▾ collapses the band to the pill; press the pill to expand
  • Hide hides the band; /context-band brings it back, expanded
  • ⟲ Compact compacts the main conversation, two-step
  • ⚙ opens the settings area
  • Uninstall: claude plugin uninstall context-band

Development

Contributor rules — the mod contract and the inclusion bar — live in CONTRIBUTING.md.

claude plugin validate mods/context-band
claude plugin test mods/context-band

License

Apache-2.0, under the repository's root LICENSE.

Source 2 files
hooks/register.tsx 532 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, PluginOptions, Register, Timer } from 'claude-code'
3
4// Context band: a view-aware band above the prompt (the AbovePrompt site).
5//
6// Layout — an environment row (identity label in agent views, model, effort
7// scale, working dot, dim session directory, shortened path, then the button
8// group Hide · ⟲ Compact · ⚙ · ▾) above a readings row (compaction-window
9// gauge, plan quota bars, reset countdown or session cost). The path is the
10// first element sacrificed; readings never truncate.
11//
12// Render branch order — survey → hidden → collapsed pill → settings open →
13// the full band. The settings area swaps the readings row only; the pill
14// ignores it. /context-band resets only isHidden and isCollapsed.
15//
16// Settings — the `settings` store key holds only explicit values; every
17// effective value is the store's value when set, else the plugin option
18// default (withOverrides), so existing /config setups keep working until a
19// settings row is touched. The pair clamp is directional: raising warning
20// lifts error out of the way, lowering error drops warning with it; warning's
21// ceiling is error's − 5. Refresh seconds re-arm the fallback clock at once —
22// turn ends push a session.measure and redraw anyway, the clock only covers
23// what the engine does not measure. The open/close flag is register-activation
24// scope like the compact arm: a hot reload closes it, a session restart too.
25// The compact confirm timeout rides $.clock.every, one-shot by self-cancel —
26// $.clock.after is not in the public release the repo check runs against.
27//
28// Readings — the gauge measures the compaction window (the summary breakdown,
29// estimated locally, sends nothing), not the model's theoretical limit. The
30// plan form is adaptive, not a login-method check: subscription windows
31// present → quota bars, no cost; otherwise the session cost, with a gateway's
32// spend limit appended when reported. The two subscription windows are told
33// apart by their base colour; past the thresholds the colour turns regardless
34// of the window.
35//
36// Agent views — turn.step fills the per-agent tracker (its writes are wrapped
37// so a refusal never disturbs dispatch; the same catch covers the roster read
38// and the name cache, so a failed agent.list or a refused name write leaves
39// dispatch untouched too); main-loop effort lands in mainEffort. The tracked
40// model decides the window table; identity is name → type → short id, cached
41// before the engine prunes the loop. Compact exists in the main conversation
42// alone, two-step, waiting out a running turn.
43
44const isHidden = atom({ plugin: 'context-band', key: 'isHidden' } as const, false)
45const isCollapsed = atom({ plugin: 'context-band', key: 'isCollapsed' } as const, false)
46const mainEffort = atom({ plugin: 'context-band', key: 'mainEffort' } as const, null)
47const tracker = atom(
48  { plugin: 'context-band', key: 'tracker' } as const,
49  { loops: {}, names: {} },
50)
51const settings = atom(
52  { plugin: 'context-band', key: 'settings' } as const,
53  {} as SettingsValues,
54)
55
56const BAR_CELLS = 12
57const QUOTA_CELLS = 3
58const PATH_CELLS = 12
59const WORKING_DOT_CELLS = 2
60const GROUP_RESERVED_CELLS = 18
61const AGENT_GROUP_RESERVED_CELLS = 7
62const COMPACT_CONFIRM_MS = 5_000
63
64interface ThresholdPair {
65  warnAt: number
66  errorAt: number
67}
68
69interface Config extends ThresholdPair {
70  refreshSeconds: number
71  showEffort: boolean
72  showCost: boolean
73  showResetIn: boolean
74}
75
76interface SettingsValues extends Partial<ThresholdPair> {
77  refreshSeconds?: number
78  showEffort?: boolean
79  showCost?: boolean
80  showResetIn?: boolean
81}
82
83type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
84
85const EFFORT_CELLS: Record<Effort, number> = { low: 1, medium: 2, high: 3, xhigh: 4, max: 5 }
86
87function effortScale(effort: Effort): string {
88  return bar((EFFORT_CELLS[effort] / 5) * 100, 5)
89}
90
91function readConfig(options: PluginOptions): Config {
92  const num = (v: unknown, fallback: number) =>
93    typeof v === 'number' && Number.isFinite(v) ? v : fallback
94  const bool = (v: unknown, fallback: boolean) =>
95    typeof v === 'boolean' ? v : fallback
96  return {
97    warnAt: Math.min(100, Math.max(1, num(options.warningThreshold, 60))),
98    errorAt: Math.min(100, Math.max(1, num(options.errorThreshold, 85))),
99    refreshSeconds: Math.min(600, Math.max(1, num(options.refreshSeconds, 7))),
100    showEffort: bool(options.showEffort, true),
101    showCost: bool(options.showCost, true),
102    showResetIn: bool(options.showResetIn, true),
103  }
104}
105
106function withOverrides(base: Config, s: SettingsValues): Config {
107  return {
108    warnAt: s.warnAt ?? base.warnAt,
109    errorAt: s.errorAt ?? base.errorAt,
110    refreshSeconds: s.refreshSeconds ?? base.refreshSeconds,
111    showEffort: s.showEffort ?? base.showEffort,
112    showCost: s.showCost ?? base.showCost,
113    showResetIn: s.showResetIn ?? base.showResetIn,
114  }
115}
116
117function clamp(n: number, lo: number, hi: number): number {
118  return Math.min(hi, Math.max(lo, n))
119}
120
121function armRefresh($: EngineInterface, seconds: number, previous: Timer | null): Timer {
122  previous?.cancel()
123  return $.clock.every(seconds * 1000, () => $.ui.invalidate('ui.render'))
124}
125
126function formatTokens(n: number): string {
127  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}m`
128  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
129  return String(n)
130}
131
132function formatReset(iso: string, nowMs: number): string {
133  const ms = new Date(iso).getTime() - nowMs
134  if (!(ms > 0)) return 'now'
135  const m = Math.round(ms / 60_000)
136  if (m < 60) return `${m}m`
137  const h = Math.floor(m / 60)
138  if (h < 48) return `${h}h${String(m % 60).padStart(2, '0')}m`
139  return `${Math.floor(h / 24)}d${h % 24}h`
140}
141
142function colorFor(percent: number, config: Config): 'success' | 'warning' | 'error' {
143  if (percent >= config.errorAt) return 'error'
144  if (percent >= config.warnAt) return 'warning'
145  return 'success'
146}
147
148function quotaColor(percent: number, kind: string, config: Config): string {
149  if (percent >= config.errorAt) return 'error'
150  if (percent >= config.warnAt) return 'warning'
151  return kind === 'seven_day' ? 'remember' : 'suggestion'
152}
153
154function bar(percent: number, cells: number): string {
155  const filled = clamp(Math.round((percent / 100) * cells), 0, cells)
156  return '█'.repeat(filled) + '░'.repeat(cells - filled)
157}
158
159function windowFor(model: string | undefined): number {
160  return !!model && /\[1m\]/.test(model) ? 1_000_000 : 200_000
161}
162
163function shortenPath(
164  cwd: string,
165  modelAndScale: string,
166  labelCells: number,
167  bodyColumns: number,
168  reservedCells: number,
169): string {
170  const budget = Math.max(
171    0,
172    bodyColumns - labelCells - WORKING_DOT_CELLS - modelAndScale.length - PATH_CELLS - reservedCells,
173  )
174  if (cwd.length <= budget) return cwd
175  const segments = cwd.split('/').filter(Boolean)
176  const lastTwo = segments.slice(-2).join('/')
177  if (lastTwo.length <= budget) return lastTwo
178  return segments[segments.length - 1] ?? cwd
179}
180
181interface ResolvedView {
182  label: string
183  working: boolean
184  window: number
185  fill: number | undefined
186  model: string | undefined
187  effort: Effort | number | null
188}
189
190async function resolveView(
191  $: EngineInterface,
192  viewed: string | undefined,
193  model: string,
194  contextWindow: number,
195): Promise<ResolvedView | null> {
196  if (viewed === undefined) return null
197  const [tracked, roster] = await Promise.all([read($, tracker), $.agent.list()])
198  const entry = tracked.loops[viewed]
199  const info = roster.find(a => a.id === viewed)
200  const live = info ? info.name || info.type : undefined
201  return {
202    label: live || tracked.names[viewed] || viewed.slice(0, 8),
203    working: info ? info.status === 'running' : false,
204    window: entry && entry.model !== model ? windowFor(entry.model) : contextWindow,
205    fill: entry?.fill,
206    model: entry?.model,
207    effort: entry?.effort ?? null,
208  }
209}
210
211async function tryCompact($: EngineInterface): Promise<boolean> {
212  try {
213    await $.session.compact()
214    return true
215  } catch {
216    return false
217  }
218}
219
220export const register: Register = (on, rawOptions) => {
221  const baseConfig = readConfig(rawOptions)
222
223  let compactArmed = false
224  let confirmTimer: Timer | null = null
225  let compactPending = false
226  let settingsOpen = false
227  let refreshTimer: Timer | null = null
228
229  on('session.start', async ($, e, next) => {
230    await $.command.register({
231      name: 'context-band',
232      description: 'Restore the context band above the prompt, expanded',
233    })
234    const config = withOverrides(baseConfig, await read($, settings))
235    refreshTimer = armRefresh($, config.refreshSeconds, refreshTimer)
236
237    return next(e)
238  })
239
240  on('session.measure', async ($, e, next) => {
241    $.ui.invalidate('ui.render')
242
243    return next(e)
244  })
245
246  on('turn.step', async function* ($, e, next) {
247    const result = yield* next(e)
248    const agentId = e.agentId
249    try {
250      const roster = await $.agent.list()
251      const listed = new Set(roster.map(a => a.id))
252      await update($, tracker, t => {
253        const loops = { ...t.loops }
254        if (agentId && result.usage) {
255          loops[agentId] = {
256            fill:
257              result.usage.input_tokens +
258              result.usage.cache_read_input_tokens +
259              result.usage.cache_creation_input_tokens,
260            model: e.model,
261            effort: e.effort,
262          }
263        }
264        for (const id of Object.keys(loops)) {
265          if (!listed.has(id)) delete loops[id]
266        }
267        const names = { ...t.names }
268        for (const a of roster) {
269          const name = a.name || a.type
270          if (name) names[a.id] = name
271        }
272        return { loops, names }
273      })
274      if (!agentId) await update($, mainEffort, () => e.effort ?? null)
275    } catch {}
276  })
277
278  on('turn.complete', async ($, e, next) => {
279    if (compactPending && e.agentId === undefined) {
280      compactPending = !(await tryCompact($))
281    }
282
283    return next(e)
284  })
285
286  on('command.run', { command: 'context-band' }, async $ => {
287    await update($, isHidden, () => false)
288    await update($, isCollapsed, () => false)
289
290    return { text: 'Context band expanded.' }
291  })
292
293  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
294    if (e.props.hasSurvey || (await read($, isHidden))) {
295      return next(e)
296    }
297
298    const { Box, Text, Button } = $.ui.resolve(e)
299    const viewed = e.props.view.agentId
300    const [usage, model, cwd, mainEffortSeen] = await Promise.all([
301      $.session.usage({ breakdown: 'summary' }),
302      $.session.model(),
303      $.session.cwd(),
304      viewed ? Promise.resolve(null) : read($, mainEffort),
305    ])
306    const { context, rateLimits, cost } = usage
307
308    const bd = context.breakdown
309    let percent = bd ? bd.percentage : (context.percent ?? 0)
310    let window = bd ? bd.rawMaxTokens : context.window
311    let noReading = !bd && context.percent === undefined
312    let label: string | undefined
313    let working = e.props.isWorking
314    let shownModel: string | undefined = model
315    let effort: Effort | number | null = mainEffortSeen
316    const view = await resolveView($, viewed, model, window)
317    if (view) {
318      label = view.label
319      working = view.working
320      window = view.window
321      noReading = view.fill === undefined
322      percent = view.fill === undefined ? 0 : (view.fill / view.window) * 100
323      shownModel = view.model
324      effort = view.effort
325    }
326    let scale = typeof effort === 'string' ? effortScale(effort) : undefined
327
328    if (await read($, isCollapsed)) {
329      const pillLabel = label?.slice(0, 12)
330      return (
331        <Box>
332          <Button
333            key="pill"
334            label={pillLabel ? `◂ ${pillLabel} ${percent.toFixed(1)}%` : `◂ ${percent.toFixed(1)}%`}
335            dimColor={noReading}
336            onPress={() => update($, isCollapsed, () => false)}
337          />
338        </Box>
339      )
340    }
341
342    const config = withOverrides(baseConfig, await read($, settings))
343    if (!config.showEffort) {
344      shownModel = undefined
345      scale = undefined
346    }
347    const modelAndScale = shownModel ? (scale ? `${shownModel} ${scale}` : shownModel) : ''
348    const color = noReading ? 'inactive' : colorFor(percent, config)
349
350    const plans = rateLimits.filter(
351      r => r.kind === 'five_hour' || r.kind === 'seven_day',
352    )
353    const spend = rateLimits.find(r => r.kind === 'spend_limit')
354    const resetsAt = config.showResetIn
355      ? plans.flatMap(p => (p.resetsAt ? [p.resetsAt] : [])).sort()[0]
356      : undefined
357    const resetIn = resetsAt
358      ? ` ↺${formatReset(resetsAt, await $.clock.now())}`
359      : undefined
360
361    const group = [
362      <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />,
363      ...(e.props.view.agentId === undefined
364        ? [
365            <Button
366              key="compact"
367              label={compactArmed ? 'Confirm' : '⟲ Compact'}
368              onPress={() => {
369                if (!compactArmed) {
370                  compactArmed = true
371                  confirmTimer = $.clock.every(COMPACT_CONFIRM_MS, () => {
372                    confirmTimer?.cancel()
373                    confirmTimer = null
374                    compactArmed = false
375                    $.ui.invalidate('ui.render')
376                  })
377                } else {
378                  confirmTimer?.cancel()
379                  confirmTimer = null
380                  compactArmed = false
381                  if (e.props.isWorking) {
382                    compactPending = true
383                  } else {
384                    void tryCompact($).then(compacted => {
385                      compactPending = !compacted
386                    })
387                  }
388                }
389                $.ui.invalidate('ui.render')
390              }}
391            />,
392          ]
393        : []),
394      <Button
395        key="settings"
396        label="⚙"
397        onPress={() => {
398          settingsOpen = !settingsOpen
399          $.ui.invalidate('ui.render')
400        }}
401      />,
402      <Button key="collapse" label="▾" onPress={() => update($, isCollapsed, () => true)} />,
403    ]
404
405    const readingsRow = (
406      <Box key="readings">
407        <Text dimColor>Context </Text>
408        <Text color={color}>{bar(percent, BAR_CELLS)} </Text>
409        <Text color={color} bold={!noReading}>
410          {percent.toFixed(1)}%
411        </Text>
412        <Text dimColor> of {formatTokens(window)}</Text>
413        {plans.map(p => (
414          <Text
415            key={p.kind}
416            color={quotaColor(p.percentUsed, p.kind, config)}
417          >{` · ${bar(p.percentUsed, QUOTA_CELLS)} ${p.percentUsed}%`}</Text>
418        ))}
419        {resetIn && <Text dimColor>{resetIn}</Text>}
420        {config.showCost && plans.length === 0 && cost !== undefined && (
421          <Text dimColor>{` · $${cost.usd.toFixed(2)}`}</Text>
422        )}
423        {spend && (
424          <Text color={quotaColor(spend.percentUsed, 'five_hour', config)}>{` · Limit ${bar(spend.percentUsed, QUOTA_CELLS)} ${spend.percentUsed}%`}</Text>
425        )}
426      </Box>
427    )
428
429    const stepPair = (field: 'warnAt' | 'errorAt', delta: number) => {
430      void update($, settings, s => {
431        const warnFrom = s.warnAt ?? baseConfig.warnAt
432        const errorFrom = s.errorAt ?? baseConfig.errorAt
433        const written: SettingsValues = { ...s }
434        if (field === 'warnAt') {
435          const warn = clamp(warnFrom + delta, 1, 100)
436          const error = Math.min(100, Math.max(errorFrom, warn + 5))
437          written.warnAt = Math.min(warn, error - 5)
438          if (error !== errorFrom) written.errorAt = error
439        } else {
440          const error = clamp(errorFrom + delta, 1, 100)
441          const warn = Math.max(1, Math.min(warnFrom, error - 5))
442          written.errorAt = Math.max(error, warn + 5)
443          if (warn !== warnFrom) written.warnAt = warn
444        }
445        return written
446      })
447    }
448    const stepSeconds = (delta: number) => {
449      const seconds = clamp(config.refreshSeconds + delta, 1, 600)
450      void update($, settings, s => ({ ...s, refreshSeconds: seconds }))
451      refreshTimer = armRefresh($, seconds, refreshTimer)
452    }
453
454    const stepper = (
455      label: string,
456      value: string,
457      minusKey: string,
458      plusKey: string,
459      onPress: (delta: number) => void,
460      by = 1,
461    ) => [
462      <Text key={`${minusKey}v`}>{`${label} ${value}`}</Text>,
463      <Button key={minusKey} label="-" onPress={() => onPress(-by)} />,
464      <Button key={plusKey} label="+" onPress={() => onPress(by)} />,
465    ]
466
467    const numericRow = (
468      <Box key="settings-numeric">
469        {[
470          ...stepper('Warn', String(config.warnAt), 'warn-', 'warn+', delta => stepPair('warnAt', delta), 5),
471          ...stepper('Error', String(config.errorAt), 'error-', 'error+', delta => stepPair('errorAt', delta), 5),
472          ...stepper('Refresh', `${config.refreshSeconds}s`, 'seconds-', 'seconds+', stepSeconds),
473        ]}
474      </Box>
475    )
476
477    const flip = (field: 'showEffort' | 'showCost' | 'showResetIn', value: boolean) => {
478      void update($, settings, s => ({ ...s, [field]: !value }))
479    }
480
481    const togglesRow = (
482      <Box key="settings-toggles">
483        {[
484          <Button
485            key="effort"
486            label={`Model & effort ${config.showEffort ? 'on' : 'off'}`}
487            onPress={() => flip('showEffort', config.showEffort)}
488          />,
489          <Button
490            key="reset"
491            label={`Reset ↺ ${config.showResetIn ? 'on' : 'off'}`}
492            onPress={() => flip('showResetIn', config.showResetIn)}
493          />,
494          <Button
495            key="cost"
496            label={`Cost ${config.showCost ? 'on' : 'off'}`}
497            onPress={() => flip('showCost', config.showCost)}
498          />,
499          <Button
500            key="done"
501            label="Done"
502            onPress={() => {
503              settingsOpen = false
504              $.ui.invalidate('ui.render')
505            }}
506          />,
507        ]}
508      </Box>
509    )
510
511    return (
512      <Box flexDirection="column">
513        <Box>
514          {label && <Text>{label} </Text>}
515          {shownModel && <Text>{shownModel} </Text>}
516          {scale && <Text dimColor>{scale} </Text>}
517          <Text color={working ? 'success' : 'inactive'}>{working ? '●' : '○'}{' '}</Text>
518          <Text dimColor>{shortenPath(
519            cwd,
520            modelAndScale,
521            label ? label.length + 1 : 0,
522            e.props.bodyColumns,
523            viewed ? AGENT_GROUP_RESERVED_CELLS : GROUP_RESERVED_CELLS,
524          )}</Text>
525          {group}
526        </Box>
527        {settingsOpen ? [numericRow, togglesRow] : [readingsRow]}
528      </Box>
529    )
530  })
531}
532
types/index.d.ts 25 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'context-band': {
4      isHidden: boolean
5      isCollapsed: boolean
6      mainEffort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null
7      tracker: {
8        loops: Record<
9          string,
10          { fill: number; model: string; effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | number }
11        >
12        names: Record<string, string>
13      }
14      settings: {
15        warnAt?: number
16        errorAt?: number
17        refreshSeconds?: number
18        showEffort?: boolean
19        showCost?: boolean
20        showResetIn?: boolean
21      }
22    }
23  }
24}
25