SLOPSHOPPER

toolbox

Toolbox: an orange button that opens a panel to switch model and effort, with rows other mods can add.

newbandspinnercommandtoast
v0.1.0MITupdated 2026-10-05schmas/claude-plugins/plugins/toolbox
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · toolbox
› 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 › /toolbox ⎿ toolbox: Toolbox panel toggled. ⟨Claude Code's own drawing⟩ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ ◆ T O O L B O X Opus 5.5 [ − ] │ ────────────────────────────────────────────────────────────────────────── │ │ MODEL Haiku 4.5 Sonnet 5.5 Opus 5.5 Fabl │ EFFORT Low Medium High XHigh Max │ │ ─ S E T T I N G S ─ │ │ ● Toolbox button where it sits In footer ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⟨Claude Code's own drawing⟩ & ◆ Toolbox ▲

Draws

Band
⟨Claude Code's own drawing⟩ ╭─────────────────────────────────────────────────────────────────────────────────────────────────── │ ◆ T O O L B O X Opus 5.5 [ − ] │ ────────────────────────────────────────────────────────────────────────── │ │ MODEL Haiku 4.5 Sonnet 5.5 Opus 5.5 │ EFFORT Low Medium High XHigh Max │ │ ─ S E T T I N G S ─ │ │ ● Toolbox button where it sits In footer ╰───────────────────────────────────────────────────────────────────────────────────────────────────
README

claude-plugins

Public Claude Code plugins and mods by schmas.

PluginScopeContents
clean-viewCalm view for non-technical users (mod)Hides tool calls, diffs and command output; shows one plain-English checklist with progress meters above the prompt. Three modes: on hides details, both shows details and the checklist, off. Switch with the band button or `/simple on\both\off`. With Toolbox installed, the switch is a row in the Toolbox panel
toolboxModel and effort switcher (mod)◆ Toolbox button sits in the prompt footer, next to the mode labels, and opens a panel last above the prompt. A SETTINGS row moves the button above the prompt too. One click sets the session model or effort. Other mods add rows to it through $.toolbox. Toggle with the button or /toolbox

Install

Add the marketplace once, then install the plugins you want.

/plugin marketplace add schmas/claude-plugins
/plugin install clean-view@schmas
/plugin install toolbox@schmas
/reload-plugins

Repo layout

claude-plugins/
├── .claude-plugin/
│   └── marketplace.json        # Lists 2 plugins
├── plugins/
│   ├── clean-view/
│   │   ├── .claude-plugin/plugin.json
│   │   ├── hooks/              # Function-hook mod + tests
│   │   └── types/              # Shared checklist state contract
│   └── toolbox/
│       ├── .claude-plugin/plugin.json
│       ├── hooks/              # Function-hook mod + tests
│       └── types/              # $.toolbox add-on API + state contract
└── README.md

License

MIT. See LICENSE.

Source 4 files
hooks/register.tsx 8 lines
1import type { Register } from 'claude-code'
2
3import { registerPanel } from './panel'
4
5export const register: Register = on => {
6  registerPanel(on)
7}
8
hooks/panel.tsx 390 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, On, RenderElement, RenderInput } from 'claude-code'
3
4import type { ToolboxEffort, ToolboxLaunch, ToolboxPosition, ToolboxSetting } from '../types'
5import {
6  DEFAULT_POSITION,
7  EFFORTS,
8  POSITION_SETTING_ID,
9  activeModel,
10  buttonLabel,
11  controlText,
12  isEffort,
13  isOn,
14  isPosition,
15  modelChoices,
16  nextValue,
17  parseEffortArg,
18  positionFromLabel,
19  positionSetting,
20  spaced,
21  summary,
22  upsert,
23} from './panel-logic'
24import type { EffortChoice, ModelChoice } from './panel-logic'
25
26type Engine = EngineInterface
27
28const ORANGE = '#E07B39'
29const PURPLE = '#7C5CD6'
30const GREEN = '#2E9E5B'
31const GRAY = '#555555'
32/** The rule under the title: darker than dim text, like a shadow. */
33const SHADOW = '#3A3A3A'
34const STORE_KEY = 'isOpen'
35const EFFORT_STORE_KEY = 'effort'
36const POSITION_STORE_KEY = 'position'
37const LABEL_WIDTH = 10
38const PANEL_WIDTH = 78
39
40const IS_OPEN = { plugin: 'toolbox', key: 'isOpen' } as const
41const EFFORT = { plugin: 'toolbox', key: 'effort' } as const
42const isOpen = atom(IS_OPEN, false)
43const effort = atom(EFFORT, null as ToolboxEffort | null)
44const POSITION = { plugin: 'toolbox', key: 'position' } as const
45const position = atom(POSITION, DEFAULT_POSITION as ToolboxPosition)
46const sync = atom({ plugin: 'toolbox', key: 'sync' } as const, 0)
47const press = atom({ plugin: 'toolbox', key: 'press' } as const, null)
48
49/**
50 * Rows other mods add through `$.toolbox`. The noun's methods have no `$` to
51 * write state with, so the rows live here; a hot reload of Toolbox drops them
52 * until each add-on sends its row again.
53 */
54let settings: ToolboxSetting[] = []
55let launches: ToolboxLaunch[] = []
56
57async function redraw($: Engine): Promise<void> {
58  await update($, sync, n => (n ?? 0) + 1)
59}
60
61// A /clear starts the session's state over, and no event this mod can count on
62// fires after it. So a value not written yet this session comes from the store.
63
64async function openNow($: Engine): Promise<boolean> {
65  const { value } = await $.state.get(IS_OPEN)
66
67  return value === undefined ? (await $.store.get(STORE_KEY)) === true : value
68}
69
70async function effortNow($: Engine): Promise<ToolboxEffort | null> {
71  const { value } = await $.state.get(EFFORT)
72
73  if (value !== undefined && value !== null) {
74    return value
75  }
76
77  const stored = await $.store.get(EFFORT_STORE_KEY)
78
79  return isEffort(stored) ? stored : null
80}
81
82async function positionNow($: Engine): Promise<ToolboxPosition> {
83  const { value } = await $.state.get(POSITION)
84
85  if (value !== undefined) {
86    return value
87  }
88
89  const stored = await $.store.get(POSITION_STORE_KEY)
90
91  return isPosition(stored) ? stored : DEFAULT_POSITION
92}
93
94async function setPosition($: Engine, value: ToolboxPosition): Promise<void> {
95  await update($, position, () => value)
96  await $.store.set(POSITION_STORE_KEY, value)
97}
98
99async function setOpen($: Engine, value: boolean): Promise<void> {
100  await update($, isOpen, () => value)
101  await $.store.set(STORE_KEY, value)
102}
103
104async function seeEffort($: Engine, level: ToolboxEffort | null): Promise<void> {
105  if (level !== null && (await read($, effort)) !== level) {
106    await update($, effort, () => level)
107    await $.store.set(EFFORT_STORE_KEY, level)
108  }
109}
110
111async function chooseModel($: Engine, choice: ModelChoice): Promise<void> {
112  const result = await $.config.set({ key: 'model', value: choice.alias })
113  await redraw($)
114
115  if ('deny' in result) {
116    $.ui.toast(`Model did not change: ${String(result.deny)}`)
117
118    return
119  }
120
121  $.ui.toast(`Model changed to ${choice.label}`)
122}
123
124async function chooseEffort($: Engine, choice: EffortChoice): Promise<void> {
125  await $.command.run({ command: 'effort', args: choice.level })
126  await seeEffort($, choice.level)
127  $.ui.toast(`Effort changed to ${choice.label}`)
128}
129
130/** Tells the add-on that owns `id`: it hooks `state.set` on `toolbox.press`. */
131async function pressAddon($: Engine, id: string, value: ToolboxSetting['value'] | null): Promise<void> {
132  await update($, press, last => ({ id, value, count: (last?.count ?? 0) + 1 }))
133  await redraw($)
134}
135
136async function drawButton($: Engine, e: RenderInput<'AbovePrompt' | 'SessionMode'>): Promise<RenderElement> {
137  const { Button } = $.ui.resolve(e)
138  const open = await openNow($)
139
140  return <Button key="toolbox" plain label={buttonLabel(open)} onPress={() => setOpen($, !open)} />
141}
142
143async function drawPanel($: Engine, e: RenderInput<'AbovePrompt'>): Promise<RenderElement> {
144  const { Box, Text, Button } = $.ui.resolve(e)
145  await read($, sync)
146  const level = await effortNow($)
147  const rows = await $.config.list()
148  const modelId = await $.session.model()
149  const models = modelChoices(rows.find(row => row.key === 'model'))
150  const model = activeModel(models, modelId)
151  const width = Math.max(30, Math.min(PANEL_WIDTH, e.props.bodyColumns))
152  // border (2) + padding (2)
153  const inner = width - 4
154
155  const chip = (key: string, label: string, isActive: boolean, color: string, onPress: () => Promise<void>) =>
156    isActive ? (
157      <Box key={`${key}-chip`} backgroundColor={color} marginRight={1}>
158        <Button key={key} plain label={` ${label} `} onPress={onPress} />
159      </Box>
160    ) : (
161      <Box key={`${key}-chip`} marginRight={1}>
162        <Button key={key} plain label={` ${label} `} onPress={onPress} />
163      </Box>
164    )
165
166  const line = (key: string, name: string, children: RenderElement[]) => (
167    <Box key={key} flexDirection="row" flexWrap="wrap">
168      <Text dimColor>{name.padEnd(LABEL_WIDTH)}</Text>
169      {children}
170    </Box>
171  )
172
173  // A blank row above and below the title, as between the header and MODEL.
174  const section = (key: string, title: string) => (
175    <Box key={key} flexDirection="row" marginTop={1} marginBottom={1}>
176      <Text dimColor>─ {spaced(title)} ─</Text>
177    </Box>
178  )
179
180  const settingRows: RenderElement[] = []
181
182  for (const setting of [positionSetting(await positionNow($)), ...settings]) {
183    const { value } = setting
184    const onPress = () =>
185      setting.id === POSITION_SETTING_ID
186        ? setPosition($, positionFromLabel(nextValue(setting, value)))
187        : pressAddon($, setting.id, nextValue(setting, value))
188    const control =
189      setting.kind === 'toggle' && value !== true ? (
190        <Button key={`set-${setting.id}`} plain dimColor label={` ${controlText(setting, value)} `} onPress={onPress} />
191      ) : (
192        <Box key={`set-${setting.id}-chip`} backgroundColor={setting.kind === 'toggle' ? GREEN : GRAY}>
193          <Button key={`set-${setting.id}`} plain label={` ${controlText(setting, value)} `} onPress={onPress} />
194        </Box>
195      )
196
197    settingRows.push(
198      <Box key={`row-${setting.id}`} flexDirection="row" justifyContent="space-between">
199        <Text wrap="truncate">
200          {isOn(value) ? <Text color={GREEN}>●</Text> : <Text dimColor>○</Text>} {setting.label}
201          {setting.hint === undefined ? '' : <Text dimColor>   {setting.hint}</Text>}
202        </Text>
203        {control}
204      </Box>,
205    )
206  }
207
208  const launchButtons = launches.map(launch =>
209    chip(`launch-${launch.id}`, `${launch.icon ?? '◆'} ${launch.label}`, false, ORANGE, () => pressAddon($, launch.id, null)),
210  )
211
212  return (
213    <Box key="toolbox-panel" flexDirection="column" borderStyle="round" borderColor={ORANGE} paddingLeft={1} paddingRight={1} width={width}>
214      <Box flexDirection="row" justifyContent="space-between">
215        <Text bold color={ORANGE}>
216          ◆  {spaced('Toolbox')}
217        </Text>
218        <Box flexDirection="row">
219          <Text key="summary" dimColor>
220            {summary(model, modelId, level)}{' '}
221          </Text>
222          <Button key="close" label="−" role="dismiss" onPress={() => setOpen($, false)} />
223        </Box>
224      </Box>
225      <Text color={SHADOW}>{'─'.repeat(inner)}</Text>
226      <Text> </Text>
227      {line(
228        'models',
229        'MODEL',
230        models.map(choice =>
231          chip(`model-${choice.alias}`, choice.label, choice === model, ORANGE, () => chooseModel($, choice)),
232        ),
233      )}
234      {line(
235        'efforts',
236        'EFFORT',
237        EFFORTS.map(choice => chip(`effort-${choice.level}`, choice.label, choice.level === level, PURPLE, () => chooseEffort($, choice))),
238      )}
239      {settingRows.length > 0 ? section('settings-title', 'Settings') : null}
240      {settingRows}
241      {launchButtons.length > 0 ? section('launch-title', 'Launch') : null}
242      {launchButtons.length > 0 ? (
243        <Box key="launches" flexDirection="row" flexWrap="wrap">
244          {launchButtons}
245        </Box>
246      ) : null}
247    </Box>
248  )
249}
250
251export function registerPanel(on: On): void {
252  on('engine.create', async ($, e, next) => {
253    const built = await next(e)
254
255    return {
256      ...built,
257      toolbox: {
258        addSetting: (setting: ToolboxSetting) => {
259          settings = upsert(settings, setting)
260        },
261        addLaunch: (launch: ToolboxLaunch) => {
262          launches = upsert(launches, launch)
263        },
264      },
265    }
266  })
267
268  on('session.start', async ($, e, next) => {
269    await $.command.register({ name: 'toolbox', description: 'Open or close the Toolbox panel' })
270    const stored = await $.store.get(STORE_KEY)
271    await update($, isOpen, () => stored === true)
272    const storedPosition = await $.store.get(POSITION_STORE_KEY)
273    await update($, position, () => (isPosition(storedPosition) ? storedPosition : DEFAULT_POSITION))
274    const storedEffort = await $.store.get(EFFORT_STORE_KEY)
275
276    if ((await read($, effort)) === null && isEffort(storedEffort)) {
277      await update($, effort, () => storedEffort)
278    }
279
280    const result = await next(e)
281    // Mods beneath this one add their rows during next(e); draw them.
282    await redraw($)
283
284    return result
285  })
286
287  on('command.run', { command: 'toolbox' }, ($, e) => {
288    try {
289      void (async () => {
290        try {
291          await setOpen($, !(await openNow($)))
292        } catch (error) {
293          $.ui.toast(`Toolbox could not toggle: ${String(error)}`)
294        }
295      })()
296
297      return { text: 'Toolbox panel toggled.' }
298    } catch (error) {
299      return { text: `Toolbox could not toggle: ${String(error)}` }
300    }
301  })
302
303  // /model, /effort and other mods' commands change what the panel shows.
304  on('command.run', async ($, e, next) => {
305    const result = await next(e)
306
307    if (e.command === 'effort') {
308      await seeEffort($, parseEffortArg(e.args))
309    }
310
311    if (e.command !== 'toolbox') {
312      await redraw($)
313    }
314
315    return result
316  })
317
318  on('config.set', async ($, e, next) => {
319    const result = await next(e)
320    await redraw($)
321
322    return result
323  })
324
325  on('turn.step', async function* ($, e, next) {
326    if (e.agentId === undefined && typeof e.effort === 'string') {
327      await seeEffort($, e.effort)
328    }
329
330    return yield* next(e)
331  })
332
333  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
334    if ((await positionNow($)) !== 'SessionMode') {
335      return next(e)
336    }
337
338    const { Box, Text } = $.ui.resolve(e)
339    const beneath = await next(e)
340
341    // A plain label in `modes` cannot be pressed, so the button sits after the
342    // engine's own labels, joined the way the engine joins them.
343    return (
344      <Box flexDirection="row">
345        {beneath}
346        {e.props.modes.length > 0 ? <Text dimColor> & </Text> : null}
347        {await drawButton($, e)}
348      </Box>
349    )
350  })
351
352  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
353    if (e.props.hasSurvey) {
354      return next(e)
355    }
356
357    const open = await openNow($)
358    const hasButton = (await positionNow($)) === 'AbovePrompt'
359
360    if (!open && !hasButton) {
361      return next(e)
362    }
363
364    const { Box } = $.ui.resolve(e)
365    // Mods beneath draw first: an add-on may send its row while it draws.
366    const beneath = await next(e)
367    const panel = open ? await drawPanel($, e) : null
368
369    // The band cannot paint over the transcript, so the panel and the button
370    // take their own rows at the right end, last, next to the prompt.
371    // Only those rows have a set width: the engine refuses its own node
372    // under a Box with one, and `beneath` may hold that node.
373    return (
374      <Box flexDirection="column">
375        {beneath}
376        {panel === null ? null : (
377          <Box key="panel-row" flexDirection="row" justifyContent="flex-end" width={e.props.bodyColumns}>
378            {panel}
379          </Box>
380        )}
381        {hasButton ? (
382          <Box key="button-row" flexDirection="row" justifyContent="flex-end" width={e.props.bodyColumns}>
383            {await drawButton($, e)}
384          </Box>
385        ) : null}
386      </Box>
387    )
388  })
389}
390
hooks/panel-logic.ts 162 lines
1import type { ToolboxEffort, ToolboxPosition, ToolboxSetting, ToolboxSettingValue } from '../types'
2
3export type ModelChoice = { alias: string; label: string }
4
5export type EffortChoice = { level: ToolboxEffort; label: string }
6
7export type ModelRow = { options?: readonly string[] } | undefined
8
9export const EFFORTS: readonly EffortChoice[] = [
10  { level: 'low', label: 'Low' },
11  { level: 'medium', label: 'Medium' },
12  { level: 'high', label: 'High' },
13  { level: 'xhigh', label: 'XHigh' },
14  { level: 'max', label: 'Max' },
15]
16
17/** Used only when Claude Code lists no model options. */
18export const FALLBACK_MODELS: readonly ModelChoice[] = [
19  { alias: 'haiku', label: 'Haiku 4.5' },
20  { alias: 'sonnet', label: 'Sonnet 5.5' },
21  { alias: 'opus', label: 'Opus 5.5' },
22  { alias: 'fable', label: 'Fable 5.1' },
23]
24
25const KNOWN_LABELS: Record<string, string> = Object.fromEntries(FALLBACK_MODELS.map(one => [one.alias, one.label]))
26
27/** Options of the `/model` row that are not one plain model. */
28const NOT_A_MODEL = new Set(['default', 'best', 'opusplan'])
29
30function capitalize(text: string): string {
31  return text === '' ? text : text[0]!.toUpperCase() + text.slice(1)
32}
33
34/** The model chips: the `/model` row's plain model options, with friendly labels. */
35export function modelChoices(row: ModelRow): ModelChoice[] {
36  const aliases = (row?.options ?? []).filter(option => !NOT_A_MODEL.has(option) && !option.includes('['))
37
38  if (aliases.length === 0) {
39    return [...FALLBACK_MODELS]
40  }
41
42  // Known models from small to large, then any others in Claude Code's order.
43  const rank = (alias: string) => {
44    const at = FALLBACK_MODELS.findIndex(one => one.alias === alias)
45
46    return at === -1 ? FALLBACK_MODELS.length : at
47  }
48
49  return [...aliases]
50    .sort((a, b) => rank(a) - rank(b))
51    .map(alias => ({ alias, label: KNOWN_LABELS[alias] ?? capitalize(alias) }))
52}
53
54/** The chip that matches the session's model id (`claude-opus-5-5` matches `opus`). */
55export function activeModel(choices: readonly ModelChoice[], modelId: string): ModelChoice | null {
56  const id = modelId.toLowerCase()
57
58  return choices.find(choice => id === choice.alias || id.includes(`-${choice.alias}-`) || id.startsWith(`${choice.alias}-`)) ?? null
59}
60
61export function effortLabel(level: ToolboxEffort): string {
62  return EFFORTS.find(one => one.level === level)?.label ?? level
63}
64
65export function isEffort(value: unknown): value is ToolboxEffort {
66  return EFFORTS.some(one => one.level === value)
67}
68
69/** The effort an `/effort <arg>` sets, or null when the argument names no level. */
70export function parseEffortArg(args: string): ToolboxEffort | null {
71  const arg = args.trim().toLowerCase()
72
73  return isEffort(arg) ? arg : null
74}
75
76/** The dim summary at the top right: `Opus 5.5 · Max`. */
77export function summary(model: ModelChoice | null, modelId: string, effort: ToolboxEffort | null): string {
78  const name = model?.label ?? modelId
79
80  return effort === null ? name : `${name} · ${effortLabel(effort)}`
81}
82
83/** `TOOLBOX` → `T O O L B O X`. */
84export function spaced(text: string): string {
85  return text.toUpperCase().split('').join(' ')
86}
87
88/** The value a press moves a setting to. */
89export function nextValue(setting: Pick<ToolboxSetting, 'kind' | 'options'>, current: ToolboxSettingValue): ToolboxSettingValue {
90  if (setting.kind === 'toggle') {
91    return current !== true
92  }
93
94  const options = setting.options ?? []
95
96  if (options.length === 0) {
97    return current
98  }
99
100  const at = options.indexOf(String(current))
101
102  return options[(at + 1) % options.length]!
103}
104
105/** A setting is on unless it is false or reads `off`. */
106export function isOn(value: ToolboxSettingValue): boolean {
107  return typeof value === 'boolean' ? value : value.trim().toLowerCase() !== 'off'
108}
109
110/** What the control at the right end of a setting row reads. */
111export function controlText(setting: Pick<ToolboxSetting, 'kind'>, value: ToolboxSettingValue): string {
112  if (setting.kind === 'toggle') {
113    return value === true ? '● On' : '○ Off'
114  }
115
116  return String(value)
117}
118
119export const DEFAULT_POSITION: ToolboxPosition = 'SessionMode'
120
121export const POSITION_SETTING_ID = 'toolbox.position'
122
123const POSITION_LABELS: Record<ToolboxPosition, string> = {
124  AbovePrompt: 'Above prompt',
125  SessionMode: 'In footer',
126}
127
128export function isPosition(value: unknown): value is ToolboxPosition {
129  return value === 'AbovePrompt' || value === 'SessionMode'
130}
131
132/** Toolbox's own row under SETTINGS: where the button sits. */
133export function positionSetting(position: ToolboxPosition): ToolboxSetting {
134  return {
135    id: POSITION_SETTING_ID,
136    label: 'Toolbox button',
137    hint: 'where it sits',
138    kind: 'choice',
139    options: Object.values(POSITION_LABELS),
140    value: POSITION_LABELS[position],
141  }
142}
143
144/** The position a row label like `Under prompt` names. */
145export function positionFromLabel(label: ToolboxSettingValue): ToolboxPosition {
146  const found = (Object.keys(POSITION_LABELS) as ToolboxPosition[]).find(one => POSITION_LABELS[one] === label)
147
148  return found ?? DEFAULT_POSITION
149}
150
151export function buttonLabel(isOpen: boolean): string {
152  return isOpen ? ' ◆ Toolbox ▲ ' : ' ◆ Toolbox ▼ '
153}
154
155/** Puts a new entry in place of one with the same id, or at the end. */
156export function upsert<T extends { id: string }>(list: readonly T[], entry: T): T[] {
157  const at = list.findIndex(one => one.id === entry.id)
158
159  return at === -1 ? [...list, entry] : list.map((one, index) => (index === at ? entry : one))
160}
161
162
types/index.d.ts 78 lines
1/** A reasoning effort level, as `/effort` takes it. */
2export type ToolboxEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
3
4/** Where the Toolbox button sits: the band above the prompt, or the mode labels in the prompt footer. */
5export type ToolboxPosition = 'AbovePrompt' | 'SessionMode'
6
7export type ToolboxSettingValue = boolean | string
8
9/**
10 * A row another mod adds under SETTINGS. Plain data: a mod sends it again
11 * with the new `value` each time the value changes.
12 */
13export type ToolboxSetting = {
14  /** Unique across mods, led by the mod's name, like `clean-view.mode`. */
15  id: string
16  label: string
17  /** Short dim text after the label. */
18  hint?: string
19  /** `toggle` flips a boolean; `choice` steps through `options`. */
20  kind: 'toggle' | 'choice'
21  /** The values a `choice` row steps through, in order. */
22  options?: readonly string[]
23  /** The current value. */
24  value: ToolboxSettingValue
25}
26
27/** A button another mod adds under LAUNCH. Plain data. */
28export type ToolboxLaunch = {
29  /** Unique across mods, led by the mod's name. */
30  id: string
31  label: string
32  /** One glyph before the label, like `✦`. */
33  icon?: string
34}
35
36/**
37 * What Toolbox writes to its `press` state when the person clicks an add-on
38 * row or button. The add-on hooks `state.set` on
39 * `{ plugin: 'toolbox', key: 'press' }` and acts on its own `id`.
40 */
41export type ToolboxPress = {
42  id: string
43  /** The value a setting row moves to; null for a LAUNCH button. */
44  value: ToolboxSettingValue | null
45  /** Counts presses, so two presses of one row are two writes. */
46  count: number
47}
48
49/**
50 * What Toolbox adds to `$`. Arguments cross between mods as plain data, so
51 * they hold no functions. A second add with the same id replaces the first.
52 */
53export type Toolbox = {
54  addSetting: (setting: ToolboxSetting) => void
55  addLaunch: (launch: ToolboxLaunch) => void
56}
57
58declare module 'claude-code' {
59  interface EngineInterface {
60    toolbox: Toolbox
61  }
62
63  interface PluginState {
64    toolbox: {
65      /** True while the panel shows above the prompt. */
66      isOpen: boolean
67      /** The main loop's effort as last seen; null until known. */
68      effort: ToolboxEffort | null
69      /** Where the button sits; the person picks it under SETTINGS. */
70      position: ToolboxPosition
71      /** Bumped to redraw after a change Toolbox cannot subscribe to. */
72      sync: number
73      /** The last add-on row or button pressed. */
74      press: ToolboxPress | null
75    }
76  }
77}
78