SLOPSHOPPER

status-bar

One colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort.

newspinnertimeragents
★ 1v0.1.0no licenseupdated 2026-10-14ataege/claude-skills/plugins/status-bar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · status-bar
› 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 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ◆ Subscription │ ✻ Opus 5.5 │ ◑ effort … │ ▣ ctx ▰▰▰▰▰▱▱▱▱▱ 49% │ ◷ 7d – │ 5h 69% │ ◎ 0 agents ⟨Claude Code's own drawing⟩

Draws

Prompt hint
◆ Subscription │ ✻ Opus 5.5 │ ◑ effort … │ ▣ ctx ▰▰▰▰▰▱▱▱▱▱ 49% │ ◷ 7d – │ 5h 69% │ ◎ 0 agents ⟨Claude Code's own drawing⟩
README

claude-skills

A Claude Code plugin marketplace.

PluginWhat it does
clean-viewHides tool calls, diffs and command output, and shows a calm checklist of Claude's plan above the prompt, with a row for each helper Claude starts. /simple on or /simple off switches it. Also holds Agent Dock: pick a Team Size from 1 to 100 and every request is split across that many helpers, each shown live in a pane. /dock opens or folds it, /dock 10 sets the size.
status-barOne colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort.
model-routingRoutes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort. Refuses agents with no model, fills in effort by tier, blocks spawns from haiku agents and past two levels, and keeps a routing guide in Claude's instructions.
mods-menuA ☰ Mods button beside the Clean View toggle. It opens a small menu listing your mods with their controls: Clean View on or off, the Agent Dock and its Team Size, and the mods that are always on. /mods opens or closes it.
prompt-upgradeTurns a rough prompt into a great one before it is sent. /better <prompt> opens a pane that asks a few quick multiple-choice questions, rewrites the prompt, and shows your original and the upgrade side by side; nothing runs until you press Run. /better on checks every typed prompt first: one that already has what it needs is sent as typed, one that falls short opens the pane, and the same prompt sent twice goes straight through. Press Enter twice to send a prompt as typed. A Detail setting (Auto, Low, Medium or High; /better level auto) sets the questions and how long and deep the upgrade is: Low, Medium and High ask up to 3, 5 or 7; Auto also turns automatic mode on, asks as many questions as needed (up to 9) and, if you don't answer within 30 seconds or press Do it for me, fills in the rest and sends the upgrade itself. /better history shows the last 20.

Install

At the prompt of a terminal session:

/plugin install clean-view --marketplace ataege/claude-skills
/plugin install status-bar --marketplace ataege/claude-skills
/plugin install model-routing --marketplace ataege/claude-skills
/plugin install mods-menu --marketplace ataege/claude-skills
/plugin install prompt-upgrade --marketplace ataege/claude-skills

Answer y to add the marketplace, then pick a scope with Enter.

From a shell:

claude plugin install clean-view --marketplace ataege/claude-skills
claude plugin install status-bar --marketplace ataege/claude-skills
claude plugin install model-routing --marketplace ataege/claude-skills
claude plugin install mods-menu --marketplace ataege/claude-skills
claude plugin install prompt-upgrade --marketplace ataege/claude-skills

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with ataege/claude-skills, then Add plugin.

Developing

Each plugin lives in plugins/<name>. Check one with:

claude plugin validate plugins/<name>
claude plugin test plugins/<name>
Source 3 files
hooks/register.tsx 9 lines
1import type { Register } from 'claude-code'
2
3import { registerStatusBar } from './status-bar'
4
5// Each mod registers itself here, so more can sit beside the status bar later.
6export const register: Register = on => {
7  registerStatusBar(on)
8}
9
hooks/status-bar.tsx 446 lines
1// Status bar: one colored line under the prompt with plan, model, effort, context, 7-day usage and agents.
2import { atom, read, update } from 'claude-code'
3import type {
4  AgentInfo,
5  Color,
6  ElementConstructor,
7  EngineInterface,
8  On,
9  RenderElement,
10  SessionRateLimit,
11  TextProps,
12} from 'claude-code'
13
14import type { StatusBarRoute, StatusBarSnapshot } from '../types'
15
16// ---------------------------------------------------------------------------
17// Limits and thresholds
18// ---------------------------------------------------------------------------
19
20const CONTEXT_CELLS = 10
21const SEPARATOR = ' │ '
22// Between the routes of the agents segment, like 3 haiku/low · 1 opus/high.
23const ROUTE_SEPARATOR = ' · '
24
25// One single-width symbol per segment, safe in Windows Terminal and most fonts.
26export const ICONS = {
27  plan: '◆',
28  model: '✻',
29  context: '▣',
30  limits: '◷',
31  agents: '◎',
32  betterPrompt: '◇',
33} as const
34
35// Prompt Upgrade's gold, for its segment.
36const BETTER_PROMPT_COLOR = '#F2C46D'
37
38// The effort symbol fills up with the level: ◔ low, ◑ medium, ◕ high, ● xhigh, ◉ max.
39const EFFORT_ICONS: Readonly<Record<string, string>> = {
40  low: '◔',
41  medium: '◑',
42  high: '◕',
43  xhigh: '●',
44  max: '◉',
45}
46// Usage share at which the plan turns amber, then red.
47const NEAR_LIMIT_PERCENT = 75
48const AT_LIMIT_PERCENT = 95
49// A finished subagent can still read as running for a moment: look again after this.
50const AGENT_RECHECK_MS = 1500
51const ACTIVE_AGENT_STATUSES: ReadonlySet<string> = new Set(['pending', 'running', 'waiting'])
52
53const EMPTY_SNAPSHOT: StatusBarSnapshot = {
54  planName: null,
55  model: null,
56  effort: null,
57  contextPercent: null,
58  fiveHourUsedPercent: null,
59  sevenDayUsedPercent: null,
60  isRateLimited: false,
61  activeAgents: 0,
62  agentRoutes: [],
63}
64
65const snapshotAtom = atom({ plugin: 'status-bar', key: 'snapshot' } as const, EMPTY_SNAPSHOT)
66const routesAtom = atom({ plugin: 'status-bar', key: 'routes' } as const, {})
67// Prompt Upgrade's automatic mode, read while drawing so a switch redraws the line; false without that mod.
68const betterPromptAtom = atom({ plugin: 'prompt-upgrade', key: 'isAutoOn' } as const, false)
69// The route table keeps the most recent agents only.
70const MAX_TRACKED_ROUTES = 300
71
72type Engine = EngineInterface
73type TextElement = ElementConstructor<TextProps>
74
75// ---------------------------------------------------------------------------
76// Turning raw figures into words
77// ---------------------------------------------------------------------------
78
79const PLAN_NAMES: Readonly<Record<string, string>> = {
80  claude_free: 'Free',
81  claude_pro: 'Pro',
82  claude_max: 'Max',
83  claude_team: 'Team',
84  claude_enterprise: 'Enterprise',
85}
86
87/** The plan name from the login record's account fields: "Max 20x", "Pro", "API"... */
88export function planLabel(account: unknown): string {
89  if (account === null || typeof account !== 'object') return 'API'
90  const { organizationType, organizationRateLimitTier, userRateLimitTier } = account as Record<string, unknown>
91  const planType = typeof organizationType === 'string' ? organizationType : ''
92  const baseName = PLAN_NAMES[planType] ?? (planType === '' ? 'Subscription' : titleCase(planType.replace(/^claude_/, '')))
93
94  // The tier names the Max multiplier, like "default_claude_max_20x".
95  const tier = [organizationRateLimitTier, userRateLimitTier].find(value => typeof value === 'string') as string | undefined
96  const multiplier = tier?.match(/(\d+)x\b/)?.[1]
97
98  return multiplier !== undefined && baseName === 'Max' ? `Max ${multiplier}x` : baseName
99}
100
101function titleCase(text: string): string {
102  return text
103    .split(/[_\s-]+/)
104    .filter(Boolean)
105    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
106    .join(' ')
107}
108
109/** "claude-opus-5-5" becomes "Opus 5.5"; an alias like "opus" becomes "Opus". */
110export function formatModel(modelId: string): string {
111  const withoutContextTag = modelId.replace(/\[[^\]]*\]$/, '')
112  const hasMillionContext = /\[1m\]$/i.test(modelId)
113  const versioned = withoutContextTag.match(/(?:claude-)?([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?(?:$|[-@:])/i)
114  const name = versioned
115    ? `${titleCase(versioned[1] ?? '')} ${versioned[2]}${versioned[3] !== undefined ? `.${versioned[3]}` : ''}`
116    : titleCase(withoutContextTag.replace(/^claude-/, ''))
117
118  return hasMillionContext ? `${name} 1M` : name
119}
120
121/** The effort level as a word; a numeric budget stays a number. */
122export function formatEffort(effort: unknown): string | null {
123  if (typeof effort === 'string' && effort !== '') return effort
124  if (typeof effort === 'number' && Number.isFinite(effort)) return String(effort)
125
126  return null
127}
128
129/** An agent's route as the line shows it: "haiku/low", or the tier alone when it has no effort. */
130export function routeLabel(modelId: string, effort: unknown): string {
131  const tier = modelId.toLowerCase().match(/haiku|sonnet|opus|fable/)?.[0] ?? formatModel(modelId).toLowerCase()
132  const effortWord = formatEffort(effort)
133
134  return effortWord === null ? tier : `${tier}/${effortWord}`
135}
136
137/** Running agents grouped by route, most first; agents not yet seen making a request count as "agent". */
138export function groupRoutes(agentIds: readonly string[], routes: Readonly<Record<string, string>>): StatusBarRoute[] {
139  const counts = new Map<string, number>()
140  for (const agentId of agentIds) {
141    const label = routes[agentId] ?? 'agent'
142    counts.set(label, (counts.get(label) ?? 0) + 1)
143  }
144
145  return [...counts.entries()]
146    .map(([label, count]) => ({ label, count }))
147    .sort((left, right) => right.count - left.count || left.label.localeCompare(right.label))
148}
149
150/** The effort symbol for a level; a numeric budget or an unknown level shows half full. */
151export function effortIcon(effort: string | null): string {
152  return (effort !== null ? EFFORT_ICONS[effort] : undefined) ?? '◑'
153}
154
155function percentOf(windows: readonly SessionRateLimit[], kind: string): number | null {
156  const window = windows.find(candidate => candidate.kind === kind)
157  return window === undefined ? null : Math.max(0, window.percentUsed)
158}
159
160/** How close the account is to a usage limit, from the busiest window. */
161export function limitStatus(snapshot: StatusBarSnapshot): 'unknown' | 'ok' | 'near' | 'limited' {
162  if (snapshot.isRateLimited) return 'limited'
163  const readings = [snapshot.fiveHourUsedPercent, snapshot.sevenDayUsedPercent].filter(
164    (value): value is number => value !== null,
165  )
166  if (readings.length === 0) return 'unknown'
167  const busiest = Math.max(...readings)
168  if (busiest >= AT_LIMIT_PERCENT) return 'limited'
169  if (busiest >= NEAR_LIMIT_PERCENT) return 'near'
170
171  return 'ok'
172}
173
174const PLAN_COLORS: Readonly<Record<ReturnType<typeof limitStatus>, Color>> = {
175  unknown: 'text',
176  ok: 'success',
177  near: 'warning',
178  limited: 'error',
179}
180
181/** Green while there is room, amber past half, red when nearly full. */
182export function contextColor(percent: number): Color {
183  if (percent >= 80) return 'error'
184  if (percent >= 50) return 'warning'
185
186  return 'success'
187}
188
189/** Green with plenty left, amber when it gets low, red when nearly gone. */
190export function remainingColor(remainingPercent: number): Color {
191  if (remainingPercent < 20) return 'error'
192  if (remainingPercent < 50) return 'warning'
193
194  return 'success'
195}
196
197/** Ten parallelograms, filled to the context share. */
198export function contextMeter(percent: number): { filled: string; empty: string } {
199  const filledCells = Math.min(CONTEXT_CELLS, Math.max(0, Math.round(percent / 10)))
200  return { filled: '▰'.repeat(filledCells), empty: '▱'.repeat(CONTEXT_CELLS - filledCells) }
201}
202
203// ---------------------------------------------------------------------------
204// Keeping the snapshot current
205// ---------------------------------------------------------------------------
206
207async function patchSnapshot($: Engine, patch: Partial<StatusBarSnapshot>): Promise<void> {
208  await update($, snapshotAtom, snapshot => ({ ...snapshot, ...patch }))
209}
210
211/** Reads the plan's name from the login record; only the plan fields are kept. */
212async function refreshPlan($: Engine): Promise<void> {
213  try {
214    const homeDirectory = await $.env.get('HOME')
215    if (homeDirectory === undefined) return
216    const loginRecord = JSON.parse(await $.fs.read(`${homeDirectory}/.claude.json`)) as Record<string, unknown>
217    await patchSnapshot($, { planName: planLabel(loginRecord.oauthAccount) })
218  } catch {
219    // No login record: the plan segment reads "Subscription" until one is found.
220  }
221}
222
223async function refreshModelAndEffort($: Engine): Promise<void> {
224  try {
225    const [modelId, settings] = await Promise.all([$.session.model(), $.settings.read()])
226    const patch: Partial<StatusBarSnapshot> = { model: formatModel(modelId) }
227    const configuredEffort = formatEffort(settings.effortLevel)
228    if (configuredEffort !== null) patch.effort = configuredEffort
229    await patchSnapshot($, patch)
230  } catch {
231    // Keep what the line showed before.
232  }
233}
234
235async function refreshUsage(
236  $: Engine,
237  usage?: { context: { percent?: number }; rateLimits: readonly SessionRateLimit[] },
238): Promise<void> {
239  try {
240    const figures = usage ?? (await $.session.usage())
241    const patch: Partial<StatusBarSnapshot> = {}
242    if (figures.context.percent !== undefined) patch.contextPercent = figures.context.percent
243    if (figures.rateLimits.length > 0) {
244      patch.fiveHourUsedPercent = percentOf(figures.rateLimits, 'five_hour')
245      patch.sevenDayUsedPercent = percentOf(figures.rateLimits, 'seven_day')
246      patch.isRateLimited = false
247    }
248    await patchSnapshot($, patch)
249  } catch {
250    // Keep what the line showed before.
251  }
252}
253
254async function refreshAgents($: Engine): Promise<void> {
255  try {
256    const agents: readonly AgentInfo[] = await $.agent.list()
257    const activeIds = agents.filter(agent => ACTIVE_AGENT_STATUSES.has(agent.status)).map(agent => agent.id)
258    const routes = await read($, routesAtom)
259    await patchSnapshot($, { activeAgents: activeIds.length, agentRoutes: groupRoutes(activeIds, routes) })
260  } catch {
261    // Keep what the line showed before.
262  }
263}
264
265function refreshAgentsSoon($: Engine): void {
266  $.clock.after(AGENT_RECHECK_MS, () => {
267    void refreshAgents($)
268  })
269}
270
271/** Records an agent's route from its own request; recounts when the route is new. */
272async function recordRoute($: Engine, agentId: string, label: string): Promise<void> {
273  const routes = await read($, routesAtom)
274  if (routes[agentId] === label) return
275  await update($, routesAtom, current => {
276    const entries = Object.entries({ ...current, [agentId]: label })
277    return Object.fromEntries(entries.slice(-MAX_TRACKED_ROUTES))
278  })
279  await refreshAgents($)
280}
281
282async function refreshEverything($: Engine): Promise<void> {
283  await Promise.all([refreshPlan($), refreshModelAndEffort($), refreshUsage($), refreshAgents($)])
284}
285
286// ---------------------------------------------------------------------------
287// Drawing the line
288// ---------------------------------------------------------------------------
289
290/** What is left of a usage window, as a whole percent; null with no reading. */
291export function remainingPercent(usedPercent: number | null): number | null {
292  return usedPercent === null ? null : Math.max(0, Math.round(100 - usedPercent))
293}
294
295/** A dim window label ("7d") and its remaining percent, colored by how much is left. */
296function drawLimit(Text: TextElement, label: string, remaining: number | null): RenderElement[] {
297  return [
298    <Text dimColor>{`${label} `}</Text>,
299    remaining === null ? <Text dimColor>–</Text> : <Text color={remainingColor(remaining)}>{`${remaining}%`}</Text>,
300  ]
301}
302
303/** The status line as one Text: ◆ plan │ ✻ model │ ◕ effort │ ▣ ctx │ ◷ 7d 5h │ ◎ agents, then ◇ Better Prompt On while it is. */
304function drawStatusLine(Text: TextElement, snapshot: StatusBarSnapshot, isBetterPromptOn = false): RenderElement {
305  const separator = <Text dimColor>{SEPARATOR}</Text>
306  const planName = snapshot.planName ?? 'Subscription'
307  const contextPercent = snapshot.contextPercent ?? 0
308  const meter = contextMeter(contextPercent)
309  const sevenDayRemaining = remainingPercent(snapshot.sevenDayUsedPercent)
310  const fiveHourRemaining = remainingPercent(snapshot.fiveHourUsedPercent)
311  // The symbol takes the color of whichever window has less left.
312  const knownRemaining = [sevenDayRemaining, fiveHourRemaining].filter((value): value is number => value !== null)
313  const agentWord = snapshot.activeAgents === 1 ? 'agent' : 'agents'
314  const hasKnownRoutes = snapshot.activeAgents > 0 && snapshot.agentRoutes.some(route => route.label !== 'agent')
315
316  const planColor = PLAN_COLORS[limitStatus(snapshot)]
317  const contextTone = snapshot.contextPercent === null ? 'subtle' : contextColor(contextPercent)
318  const limitsTone = knownRemaining.length === 0 ? 'subtle' : remainingColor(Math.min(...knownRemaining))
319  const agentTone = snapshot.activeAgents > 0 ? 'claude' : 'subtle'
320
321  return (
322    <Text wrap="truncate-end">
323      <Text color={planColor}>{`${ICONS.plan} `}</Text>
324      <Text color={planColor} weight="bold">
325        {planName}
326      </Text>
327      {separator}
328      <Text color="claude">{`${ICONS.model} `}</Text>
329      <Text color="claude">{snapshot.model ?? '…'}</Text>
330      {separator}
331      <Text color="suggestion">{`${effortIcon(snapshot.effort)} `}</Text>
332      <Text dimColor>effort </Text>
333      <Text>{snapshot.effort ?? '…'}</Text>
334      {separator}
335      <Text color={contextTone}>{`${ICONS.context} `}</Text>
336      <Text dimColor>ctx </Text>
337      <Text color={contextTone}>{meter.filled}</Text>
338      <Text dimColor>{meter.empty}</Text>
339      <Text> {snapshot.contextPercent === null ? '–' : `${contextPercent}%`}</Text>
340      {separator}
341      <Text color={limitsTone}>{`${ICONS.limits} `}</Text>
342      {drawLimit(Text, '7d', sevenDayRemaining)}
343      {separator}
344      {drawLimit(Text, '5h', fiveHourRemaining)}
345      {separator}
346      <Text color={agentTone}>{`${ICONS.agents} `}</Text>
347      {hasKnownRoutes ? (
348        snapshot.agentRoutes.map((route, index) => [
349          index > 0 ? <Text dimColor>{ROUTE_SEPARATOR}</Text> : null,
350          <Text color="claude">{`${route.count} ${route.label}`}</Text>,
351        ])
352      ) : (
353        <Text color={snapshot.activeAgents > 0 ? 'claude' : undefined} dimColor={snapshot.activeAgents === 0}>
354          {`${snapshot.activeAgents} ${agentWord}`}
355        </Text>
356      )}
357      {isBetterPromptOn ? [
358        separator,
359        <Text color={BETTER_PROMPT_COLOR}>{`${ICONS.betterPrompt} Better Prompt On`}</Text>,
360      ] : null}
361    </Text>
362  )
363}
364
365// ---------------------------------------------------------------------------
366// Hooks
367// ---------------------------------------------------------------------------
368
369export function registerStatusBar(on: On): void {
370  // Session start (and each reload): read every figure once.
371  on('session.start', async ($, e, next) => {
372    await refreshEverything($)
373    return next(e)
374  })
375
376  // Usage figures pushed by the engine after each turn and as limits move.
377  on('session.measure', async ($, e, next) => {
378    await refreshUsage($, e)
379    return next(e)
380  })
381
382  // Each request names the model and effort actually used: the main thread's for the line, an agent's for its route.
383  on('turn.step', async function* ($, e, next) {
384    if (e.agentId === undefined) {
385      const effort = formatEffort(e.effort)
386      const patch = effort === null ? { model: formatModel(e.model) } : { model: formatModel(e.model), effort }
387      // Never hold up or break the model's stream over a display update.
388      void patchSnapshot($, patch).catch(() => undefined)
389    } else {
390      void recordRoute($, e.agentId, routeLabel(e.model, e.effort)).catch(() => undefined)
391    }
392    return yield* next(e)
393  })
394
395  // A new turn: pick up a changed model or effort setting.
396  on('turn.start', async ($, e, next) => {
397    if (e.agentId === undefined) await refreshModelAndEffort($)
398    return next(e)
399  })
400
401  // A subagent's run ended: recount, now and once its task settles.
402  on('turn.complete', async ($, e, next) => {
403    const result = await next(e)
404    if (e.agentId !== undefined) {
405      await refreshAgents($)
406      refreshAgentsSoon($)
407    }
408    return result
409  })
410
411  // An agent started: count it.
412  on('agent.spawn', async ($, e, next) => {
413    const result = await next(e)
414    await refreshAgents($)
415    return result
416  }).catch(($, e, next) => next(e))
417
418  // A usage-limit error turns the plan red until usage moves again.
419  on('classic.StopFailure', async ($, e, next) => {
420    if (e.error === 'rate_limit') await patchSnapshot($, { isRateLimited: true })
421    return next(e)
422  }).catch(($, e, next) => next(e))
423
424  // /model, /effort, /clear and the like: read the figures again afterwards.
425  on('command.run', async ($, e, next) => {
426    const result = await next(e)
427    await Promise.all([refreshModelAndEffort($), refreshUsage($), refreshAgents($)])
428    return result
429  }).catch(($, e, next) => next(e))
430
431  // The line sits right under the prompt, above the engine's own hint line.
432  on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
433    const snapshot = await read($, snapshotAtom)
434    const isBetterPromptOn = (await read($, betterPromptAtom)) === true
435    const engineHint = await next(e)
436    const { Box, Text } = $.ui.resolve(e)
437
438    return (
439      <Box flexDirection="column">
440        {drawStatusLine(Text, snapshot, isBetterPromptOn)}
441        {engineHint}
442      </Box>
443    )
444  })
445}
446
types/index.d.ts 40 lines
1// The status bar's shared state: other mods can read its figures from here.
2
3/** Running agents that share one model and effort, like 3 on "haiku/low". */
4export type StatusBarRoute = { label: string; count: number }
5
6/** What the status bar under the prompt shows; null where nothing is known yet. */
7export type StatusBarSnapshot = {
8  /** The plan, like "Max 20x" or "Pro"; "API" for a key. */
9  planName: string | null
10  /** The model in plain words, like "Opus 5.5". */
11  model: string | null
12  /** The reasoning effort, like "high". */
13  effort: string | null
14  /** How full the context window is, 0 to 100. */
15  contextPercent: number | null
16  /** How much of the 5-hour and 7-day usage windows is used, 0 to 100. */
17  fiveHourUsedPercent: number | null
18  sevenDayUsedPercent: number | null
19  /** True after a turn ended on a usage-limit error, until usage moves again. */
20  isRateLimited: boolean
21  /** Subagents and teammates that are starting, running or waiting. */
22  activeAgents: number
23  /** The running agents grouped by model and effort, most first. */
24  agentRoutes: StatusBarRoute[]
25}
26
27declare module 'claude-code' {
28  interface PluginState {
29    'status-bar': {
30      snapshot: StatusBarSnapshot
31      /** Each agent's route, like "haiku/low", by agent id, from its own requests. */
32      routes: Record<string, string>
33    }
34    // Read only: Prompt Upgrade's automatic mode, shown while it is on.
35    'prompt-upgrade': {
36      isAutoOn: boolean
37    }
38  }
39}
40