SLOPSHOPPER

mod-tools

A Mod Tools button in the prompt footer that opens a panel for picking the model and effort, showing or hiding mod panes, and switching your installed mods on…

newbandspinnercommandtoastprocess
v0.4.0no licenseupdated 2026-10-08jidhra/claude-mods/plugins/mod-tools
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-tools
› 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 › /tools ⎿ mod-tools: Mod Tools is open above the prompt. ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ◆ M O D T O O L S Default model · Default effort │ │ │ │ MODEL Haiku 4.5 Sonnet 5.5 Opus 5.5 Fable 5.1 │ │ EFFORT Low Medium High XHigh Max │ │ │ │ M O D S │ │ │ │ No other mods installed │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ──────────────────────────────────────────────────────────────────────────────── Mod Control Panel ─ ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts auto-accept edits ◆ Mod Tools ▴

Draws

Band
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ ◆ M O D T O O L S Default model · Default effort │ │ │ │ MODEL Haiku 4.5 Sonnet 5.5 Opus 5.5 Fable 5.1 │ │ EFFORT Low Medium High XHigh Max │ │ │ │ M O D S │ │ │ │ No other mods installed │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ──────────────────────────────────────────────────────────────────────────────── Mod Control Panel ─ ⟨Claude Code's own drawing⟩
README

Mod Tools

A Claude Code mod that puts a ◆ Mod Tools ▾ button at the bottom right of the prompt footer. Click it to open a small control panel above the prompt:

╭──────────────────────────────────────────────────────────────╮
│ ◆ M O D   T O O L S                          Opus 5.5 · High │
│                                                              │
│ MODEL   Haiku 4.5  Sonnet 5.5 [Opus 5.5] Fable 5.1           │
│ EFFORT  Low  Medium [High] XHigh  Max                        │
│                                                              │
│   M O D S   All on   All off                  Show  Enabled  │
│                                                              │
│ ○ Buffer Pane  text snippets pane                     ○ Off  │
│ ● Clean View  simple checklist                        ● On   │
│ ● Flightdeck  agent dashboard           [● Shown ]    ● On   │
│ ● Pinboard  decisions, tasks & links pa  ○ Hidden     ● On   │
│ ● Secret Redactor  hides secrets & PII                ● On   │
│ ● Usage Pane  plan limits & session us   ○ Hidden     ● On   │
╰──────────────────────────────────────────────────────────────╯
───────────────────────────────────────────── Mod Control Panel ─
  • Model: click a model to switch to it (runs /model with its full ID; Opus keeps the 1M context window).
  • Effort: click a level to set it (runs /effort). Haiku doesn't use effort, so the row says so.
  • Mods: every installed mod (a plugin whose hooks/hooks.json loads function-hook modules) gets an On/Off switch. Plugins that aren't mods, such as Codex, aren't listed, and neither is Mod Tools itself.
  • Switching a mod runs claude plugin enable|disable <id> --scope user, then /reload-plugins.
  • Clean View is the exception: it stays loaded and its switch runs /simple on|off. If the Clean View plugin is disabled, its switch enables it.
  • All on / All off switch every listed mod at once, with a single reload.
  • Show: mods that have a pane (Flightdeck, Pinboard, Buffer Pane, Usage Pane) get a Show/Hide button next to their On/Off switch. ● Shown on a green background means the pane is open, and pressing it hides the pane. ○ Hidden, with no background, means it's closed, and pressing it opens the pane. The button runs the mod's own command (/flightdeck open|close, /pinboard, /pinboard close, /buffer-pane, /usage-pane open|close). A mod that's switched off, or that has no pane, leaves the cell empty.
  • The engine only lists a plugin's own panes, so each of those mods publishes a paneOpen value in its $.state, and Mod Tools reads it.

Colors come from your Claude Code theme, so the panel follows whatever theme you've picked in /config.

If you can't click the footer, type /tools to open or close the panel.

Install

claude plugin marketplace add jidhra/claude-mods
claude plugin install mod-tools@claude-mods --scope user

Or, inside Claude Code: /plugin install mod-tools --marketplace jidhra/claude-mods.

Develop

claude plugin validate .
claude plugin test .
Source 3 files
hooks/register.tsx 8 lines
1import type { Register } from 'claude-code'
2
3import { registerModTools } from './mod-tools'
4
5export const register: Register = on => {
6  registerModTools(on)
7}
8
hooks/mod-tools.tsx 511 lines
1import { atom, read, update } from 'claude-code'
2import type { ConfigRow, EngineInterface, On } from 'claude-code'
3
4import type { ModToolsMod } from '../types'
5
6const COMMAND = 'tools'
7const FALLBACK_COMMAND = 'mod-tools'
8const TITLE = '◆ M O D   T O O L S'
9const FOOTER_LABEL = 'Mod Tools'
10const RULE_LABEL = ' Mod Control Panel ─'
11const LABEL_CELLS = 8
12const MODS_HEADING = 'M O D S'
13
14type Engine = EngineInterface
15
16type ModelChoice = {
17  id: string
18  label: string
19  family: string
20  hasEffort: boolean
21}
22
23type EffortChoice = {
24  value: string
25  label: string
26}
27
28/** The models the panel offers, in the order drawn. Opus keeps the 1M context window. */
29export const MODELS: readonly ModelChoice[] = [
30  { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', family: 'haiku', hasEffort: false },
31  { id: 'claude-sonnet-5-5', label: 'Sonnet 5.5', family: 'sonnet', hasEffort: true },
32  { id: 'claude-opus-5-5[1m]', label: 'Opus 5.5', family: 'opus', hasEffort: true },
33  { id: 'claude-fable-5-1', label: 'Fable 5.1', family: 'fable', hasEffort: true },
34]
35
36export const EFFORTS: readonly EffortChoice[] = [
37  { value: 'low', label: 'Low' },
38  { value: 'medium', label: 'Medium' },
39  { value: 'high', label: 'High' },
40  { value: 'xhigh', label: 'XHigh' },
41  { value: 'max', label: 'Max' },
42]
43
44const isOpenAtom = atom({ plugin: 'mod-tools', key: 'isOpen' } as const, false)
45const modelAtom = atom({ plugin: 'mod-tools', key: 'model' } as const, null)
46const effortAtom = atom({ plugin: 'mod-tools', key: 'effort' } as const, null)
47const modsAtom = atom({ plugin: 'mod-tools', key: 'mods' } as const, [])
48const cleanViewAtom = atom({ plugin: 'clean-view', key: 'cleanViewEnabled' } as const, true)
49
50/** Each mod with a pane publishes whether it is open; the engine lists a plugin's panes only to that plugin. */
51const flightdeckOpenAtom = atom({ plugin: 'flightdeck', key: 'paneOpen' } as const, false)
52const pinboardOpenAtom = atom({ plugin: 'pinboard', key: 'paneOpen' } as const, false)
53const bufferPaneOpenAtom = atom({ plugin: 'buffer-pane', key: 'paneOpen' } as const, false)
54const usagePaneOpenAtom = atom({ plugin: 'usage-pane', key: 'paneOpen' } as const, false)
55
56type PaneCommand = { command: string; args: string }
57
58/** The mods that have a pane: where Show reads whether it is open, and the commands that show and hide it. */
59export const PANES: Readonly<Record<string, { show: PaneCommand; hide: PaneCommand }>> = {
60  flightdeck: { show: { command: 'flightdeck', args: 'open' }, hide: { command: 'flightdeck', args: 'close' } },
61  pinboard: { show: { command: 'pinboard', args: '' }, hide: { command: 'pinboard', args: 'close' } },
62  // /buffer-pane toggles
63  'buffer-pane': { show: { command: 'buffer-pane', args: '' }, hide: { command: 'buffer-pane', args: '' } },
64  'usage-pane': { show: { command: 'usage-pane', args: 'open' }, hide: { command: 'usage-pane', args: 'close' } },
65}
66
67async function readPaneOpen($: Engine): Promise<Record<string, boolean>> {
68  return {
69    flightdeck: await read($, flightdeckOpenAtom),
70    pinboard: await read($, pinboardOpenAtom),
71    'buffer-pane': await read($, bufferPaneOpenAtom),
72    'usage-pane': await read($, usagePaneOpenAtom),
73  }
74}
75
76/** The bare plugin name: `pinboard@claude-mods` → `pinboard`. */
77const baseName = (id: string) => id.split('@')[0] ?? id
78
79async function setShown($: Engine, mod: ModToolsMod, isShown: boolean) {
80  const pane = PANES[baseName(mod.id)]
81  if (pane === undefined) {
82    return
83  }
84  await $.command.run(isShown ? pane.show : pane.hide)
85}
86
87// Cells of the two switch columns, so every row's buttons line up under their headings
88const SHOW_CELLS = 10
89const ENABLED_CELLS = 8
90
91/** Clean View stays loaded when switched off: its row runs /simple instead of disabling the plugin. */
92export const CLEAN_VIEW_ID = 'clean-view@claude-mods'
93const SELF_ID = 'mod-tools@claude-mods'
94
95/** Short captions for the mods this panel knows; any other mod shows its name alone. */
96const CAPTIONS: Readonly<Record<string, string>> = {
97  'clean-view': 'simple checklist',
98  flightdeck: 'agent dashboard',
99  'buffer-pane': 'text snippets pane',
100  pinboard: 'decisions, tasks & links pane',
101  'usage-pane': 'plan limits & session usage pane',
102  'secret-redactor': 'hides secrets & PII',
103}
104
105/** `clean-view@claude-mods` → `Clean View`. */
106export function modName(id: string): string {
107  return (id.split('@')[0] ?? id)
108    .split(/[-_]/)
109    .filter(word => word !== '')
110    .map(word => word[0]!.toUpperCase() + word.slice(1))
111    .join(' ')
112}
113
114type ListedPlugin = { id: string; enabled: boolean; installPath: string }
115
116/** A mod is a plugin whose hooks/hooks.json loads function-hook modules; classic-hook plugins (Codex) are not. */
117async function isMod($: Engine, installPath: string): Promise<boolean> {
118  try {
119    const manifest = asRecord(JSON.parse(String(await $.fs.read(`${installPath}/hooks/hooks.json`))))
120
121    return Array.isArray(manifest.modules) && manifest.modules.length > 0
122  } catch {
123    return false
124  }
125}
126
127/** Lists the installed mods (not this one) from `claude plugin list --json`. */
128async function refreshMods($: Engine) {
129  let listed: ListedPlugin[]
130  try {
131    const { exitCode, stdout } = await $.process.run(['claude', 'plugin', 'list', '--json'])
132    if (exitCode !== 0) {
133      return
134    }
135    listed = JSON.parse(stdout) as ListedPlugin[]
136  } catch {
137    return
138  }
139
140  const mods: ModToolsMod[] = []
141  for (const plugin of listed) {
142    if (plugin.id !== SELF_ID && (await isMod($, plugin.installPath))) {
143      mods.push({ id: plugin.id, name: modName(plugin.id), enabled: plugin.enabled })
144    }
145  }
146  mods.sort((a, b) => a.name.localeCompare(b.name))
147  await update($, modsAtom, () => mods)
148}
149
150/** Enables or disables each plugin, then reloads once. Returns false if a switch failed (its error is toasted). */
151async function switchPlugins($: Engine, ids: readonly string[], isOn: boolean): Promise<boolean> {
152  let isOk = true
153  for (const id of ids) {
154    const { exitCode, stderr, stdout } = await $.process.run(['claude', 'plugin', isOn ? 'enable' : 'disable', id, '--scope', 'user'])
155    if (exitCode !== 0) {
156      isOk = false
157      $.ui.toast(`${modName(id)}: ${(stderr || stdout).trim().split('\n')[0] ?? 'could not switch'}`)
158    }
159  }
160  if (ids.length > 0) {
161    await refreshMods($)
162    await $.command.run({ command: 'reload-plugins', args: '' })
163  }
164
165  return isOk
166}
167
168async function setMod($: Engine, mod: ModToolsMod, isOn: boolean) {
169  if (mod.id === CLEAN_VIEW_ID && mod.enabled) {
170    await setCleanView($, isOn)
171    return
172  }
173  if (await switchPlugins($, [mod.id], isOn)) {
174    $.ui.toast(`${mod.name}: ${isOn ? 'On' : 'Off'}`)
175  }
176}
177
178async function setAllMods($: Engine, isOn: boolean) {
179  const mods = await read($, modsAtom)
180  const cleanView = mods.find(mod => mod.id === CLEAN_VIEW_ID)
181  // Clean View switches by /simple; it is only enabled as a plugin when All on finds it disabled.
182  const ids = mods
183    .filter(mod => mod.enabled !== isOn && (mod.id !== CLEAN_VIEW_ID || isOn))
184    .map(mod => mod.id)
185  if (cleanView?.enabled && !isOn) {
186    await setCleanView($, false)
187  }
188  await switchPlugins($, ids, isOn)
189  if (cleanView !== undefined && isOn) {
190    await setCleanView($, true)
191  }
192  $.ui.toast(isOn ? 'All mods on' : 'All mods off')
193}
194
195/** Finds which offered model a model name (an id, an alias, a /config value) is. */
196export function modelFor(name: string | null): ModelChoice | null {
197  if (name === null) {
198    return null
199  }
200  const lower = name.toLowerCase()
201
202  return MODELS.find(model => lower.includes(model.family)) ?? null
203}
204
205export function effortFor(name: string | null): EffortChoice | null {
206  if (name === null) {
207    return null
208  }
209  const lower = name.toLowerCase()
210
211  return EFFORTS.find(effort => effort.value === lower) ?? null
212}
213
214function findRow(rows: readonly ConfigRow[], name: 'model' | 'effort'): ConfigRow | undefined {
215  const matches = (text: string) => (name === 'model' ? /^model$/i.test(text) : /effort/i.test(text))
216
217  return rows.find(row => row.provider.plugin === 'engine' && (matches(row.key) || matches(row.label)))
218}
219
220function asText(value: unknown): string | null {
221  return typeof value === 'string' && value !== '' ? value : null
222}
223
224function asRecord(value: unknown): Record<string, unknown> {
225  return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {}
226}
227
228/** The effort settings hold for a model: its own `modelSettings` entry, else the global `effortLevel`. */
229function savedEffort(settings: Record<string, unknown>, model: string | null): string | null {
230  const choice = modelFor(model)
231  if (choice !== null) {
232    const perModel = asRecord(settings.modelSettings)
233    const bareId = choice.id.replace('[1m]', '')
234    const key = bareId in perModel ? bareId : Object.keys(perModel).find(name => modelFor(name)?.family === choice.family)
235    const effort = key === undefined ? null : asText(asRecord(perModel[key]).effortLevel)
236    if (effort !== null) {
237      return effort
238    }
239  }
240
241  return asText(settings.effortLevel)
242}
243
244async function readSettings($: Engine): Promise<Record<string, unknown>> {
245  try {
246    return asRecord(await $.settings.read())
247  } catch {
248    return {}
249  }
250}
251
252/** Reads the session's model from `/config` (or settings) and its effort from `/config` or settings. */
253async function refresh($: Engine) {
254  let model: string | null = null
255  let effort: string | null = null
256  try {
257    const rows = await $.config.list()
258    model = asText(findRow(rows, 'model')?.value)
259    effort = asText(findRow(rows, 'effort')?.value)
260  } catch {
261    // The settings below are the fallback.
262  }
263
264  const settings = await readSettings($)
265  model ??= asText(settings.model)
266  effort ??= savedEffort(settings, model)
267
268  if (model !== null) {
269    await update($, modelAtom, () => model)
270  }
271  if (effort !== null) {
272    await update($, effortAtom, () => effort)
273  }
274}
275
276async function chooseModel($: Engine, model: ModelChoice) {
277  await update($, modelAtom, () => model.id)
278  await $.command.run({ command: 'model', args: model.id })
279  const effort = savedEffort(await readSettings($), model.id)
280  if (effort !== null) {
281    await update($, effortAtom, () => effort)
282  }
283  $.ui.toast(`Model: ${model.label}`)
284}
285
286/** Sets effort through its `/config` row where this build has one, else `/effort`. */
287async function chooseEffort($: Engine, effort: EffortChoice) {
288  await update($, effortAtom, () => effort.value)
289  const row = findRow(await $.config.list(), 'effort')
290  const fits = row !== undefined && !row.isLocked && (row.kind === 'text' || (row.options ?? []).includes(effort.value))
291  const answer = fits ? await $.config.set({ key: row.key, value: effort.value }).catch(() => undefined) : undefined
292  if (answer === undefined || answer.deny !== undefined) {
293    await $.command.run({ command: 'effort', args: effort.value })
294  }
295  $.ui.toast(`Effort: ${effort.label}`)
296}
297
298async function setCleanView($: Engine, isOn: boolean) {
299  await $.command.run({ command: 'simple', args: isOn ? 'on' : 'off' })
300}
301
302async function setOpen($: Engine, isOpen: boolean) {
303  await update($, isOpenAtom, () => isOpen)
304  if (isOpen) {
305    await refresh($)
306    await refreshMods($)
307  }
308}
309
310async function toggleFromCommand($: Engine) {
311  const isOpen = !(await read($, isOpenAtom))
312  await setOpen($, isOpen)
313
314  return isOpen ? 'Mod Tools is open above the prompt.' : 'Mod Tools is closed.'
315}
316
317export function registerModTools(on: On) {
318  on('session.start', async ($, e, next) => {
319    try {
320      await $.command.register({ name: COMMAND, description: 'Open or close Mod Tools' })
321    } catch {
322      await $.command.register({ name: FALLBACK_COMMAND, description: 'Open or close Mod Tools' })
323    }
324
325    await refresh($)
326    await refreshMods($)
327
328    return next(e)
329  })
330
331  on('command.run', { command: 'tools' }, async $ => ({ text: await toggleFromCommand($) }))
332  on('command.run', { command: 'mod-tools' }, async $ => ({ text: await toggleFromCommand($) }))
333
334  on('turn.start', async ($, e, next) => {
335    if (await read($, isOpenAtom)) {
336      await refresh($)
337    }
338
339    return next(e)
340  })
341
342  on('ui.render', { component: 'SessionMode' }, async ($, e) => {
343    const { Box, Text, Button } = $.ui.resolve(e)
344    const isOpen = await read($, isOpenAtom)
345
346    return (
347      <Box flexDirection="row" gap={2}>
348        {e.props.modes.length > 0 && <Text dimColor>{e.props.modes.join(' & ')}</Text>}
349        <Button
350          key="open"
351          plain
352          label={`◆ ${FOOTER_LABEL} ${isOpen ? '▴' : '▾'}`}
353          onPress={() => setOpen($, !isOpen)}
354        />
355      </Box>
356    )
357  })
358
359  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
360    if (e.props.hasSurvey || !(await read($, isOpenAtom))) {
361      return next(e)
362    }
363
364    const { Box, Text, Button } = $.ui.resolve(e)
365    const below = await next(e)
366    const width = Math.max(32, e.props.bodyColumns)
367    const model = modelFor(await read($, modelAtom))
368    const effort = effortFor(await read($, effortAtom))
369    const hasEffort = model === null || model.hasEffort
370    const isCleanViewOn = await read($, cleanViewAtom)
371    const mods = await read($, modsAtom)
372    const paneOpen = await readPaneOpen($)
373    // A mod reads On when its plugin is enabled; Clean View also needs /simple on.
374    const isOn = (mod: ModToolsMod) => mod.enabled && (mod.id !== CLEAN_VIEW_ID || isCleanViewOn)
375    const summary = [model?.label ?? 'Default model', hasEffort ? (effort?.label ?? 'Default effort') : null]
376      .filter(part => part !== null)
377      .join(' · ')
378
379    const modelRow = MODELS.map(choice =>
380      choice.family === model?.family ? (
381        <Text backgroundColor="claude" color="inverseText" bold>
382          {` ${choice.label} `}
383        </Text>
384      ) : (
385        <Button key={`model:${choice.family}`} plain label={` ${choice.label} `} onPress={() => chooseModel($, choice)} />
386      ),
387    )
388
389    const effortRow = hasEffort ? (
390      EFFORTS.map(choice =>
391        choice.value === effort?.value ? (
392          <Text backgroundColor="permission" color="inverseText" bold>
393            {` ${choice.label} `}
394          </Text>
395        ) : (
396          <Button key={`effort:${choice.value}`} plain label={` ${choice.label} `} onPress={() => chooseEffort($, choice)} />
397        ),
398      )
399    ) : (
400      <Text dimColor> {model?.label} doesn't use an effort setting</Text>
401    )
402
403    const modRow = (mod: ModToolsMod) => {
404      const isModOn = isOn(mod)
405      const caption = CAPTIONS[baseName(mod.id)]
406      const hasPane = mod.enabled && PANES[baseName(mod.id)] !== undefined
407      const isShown = paneOpen[baseName(mod.id)] === true
408
409      return (
410        <Box key={`row:${mod.id}`} flexDirection="row" justifyContent="space-between" gap={2}>
411          <Box flexDirection="row" gap={1} flexShrink={1}>
412            <Text color={isModOn ? 'success' : 'inactive'}>{isModOn ? '●' : '○'}</Text>
413            <Text bold>{mod.name}</Text>
414            {caption !== undefined && (
415              <Text dimColor wrap="truncate-end">
416                {caption}
417              </Text>
418            )}
419          </Box>
420          <Box flexDirection="row" flexShrink={0}>
421            <Box width={SHOW_CELLS} flexShrink={0}>
422              {hasPane &&
423                (isShown ? (
424                  <Box backgroundColor="success">
425                    <Button key={`show:${mod.id}`} plain label=" ● Shown " onPress={() => setShown($, mod, false)} />
426                  </Box>
427                ) : (
428                  <Button key={`show:${mod.id}`} plain label=" ○ Hidden " onPress={() => setShown($, mod, true)} />
429                ))}
430            </Box>
431            <Box width={ENABLED_CELLS} flexShrink={0}>
432              {isModOn ? (
433                <Box backgroundColor="success">
434                  <Button key={`mod:${mod.id}`} plain label=" ● On " onPress={() => setMod($, mod, false)} />
435                </Box>
436              ) : (
437                <Button key={`mod:${mod.id}`} plain label=" ○ Off " onPress={() => setMod($, mod, true)} />
438              )}
439            </Box>
440          </Box>
441        </Box>
442      )
443    }
444
445    const panel = (
446      <Box flexDirection="column" width={width}>
447        <Box flexDirection="column" borderStyle="round" borderColor="subtle" paddingX={1}>
448          <Box flexDirection="row" justifyContent="space-between" gap={2}>
449            <Text color="claude" bold wrap="truncate-end">
450              {TITLE}
451            </Text>
452            <Text wrap="truncate-end">{summary}</Text>
453          </Box>
454
455          <Box flexDirection="row" marginTop={1}>
456            <Box width={LABEL_CELLS} flexShrink={0}>
457              <Text dimColor>MODEL</Text>
458            </Box>
459            <Box flexDirection="row" flexWrap="wrap" flexShrink={1}>
460              {modelRow}
461            </Box>
462          </Box>
463          <Box flexDirection="row">
464            <Box width={LABEL_CELLS} flexShrink={0}>
465              <Text dimColor>EFFORT</Text>
466            </Box>
467            <Box flexDirection="row" flexWrap="wrap" flexShrink={1}>
468              {effortRow}
469            </Box>
470          </Box>
471
472          <Box key="heading:mods" flexDirection="row" justifyContent="space-between" gap={2} marginTop={1} marginBottom={1}>
473            <Box flexDirection="row" gap={2} paddingLeft={2} flexShrink={1}>
474              <Text dimColor>{MODS_HEADING}</Text>
475              {mods.length > 0 && (
476                <Box flexDirection="row" gap={1}>
477                  <Button key="mods:allOn" plain label=" All on " onPress={() => setAllMods($, true)} />
478                  <Button key="mods:allOff" plain label=" All off " onPress={() => setAllMods($, false)} />
479                </Box>
480              )}
481            </Box>
482            {mods.length > 0 && (
483              <Box flexDirection="row" flexShrink={0}>
484                <Box width={SHOW_CELLS} flexShrink={0}>
485                  <Text dimColor> Show</Text>
486                </Box>
487                <Box width={ENABLED_CELLS} flexShrink={0}>
488                  <Text dimColor> Enabled</Text>
489                </Box>
490              </Box>
491            )}
492          </Box>
493          {mods.length > 0 ? mods.map(modRow) : <Text dimColor>No other mods installed</Text>}
494        </Box>
495        <Text dimColor wrap="truncate-end">
496          {'─'.repeat(Math.max(0, width - RULE_LABEL.length))}
497          {RULE_LABEL}
498        </Text>
499      </Box>
500    )
501
502    // No size props on this wrapper: the engine refuses its own band node under a sized Box.
503    return (
504      <Box flexDirection="column">
505        {panel}
506        {below}
507      </Box>
508    )
509  })
510}
511
types/index.d.ts 18 lines
1export type ModToolsModel = string | null
2
3export type ModToolsEffort = string | null
4
5/** An installed mod the panel can switch: its plugin id, display name and whether the plugin is enabled. */
6export type ModToolsMod = { id: string; name: string; enabled: boolean }
7
8declare module 'claude-code' {
9  interface PluginState {
10    'mod-tools': {
11      isOpen: boolean
12      model: ModToolsModel
13      effort: ModToolsEffort
14      mods: ModToolsMod[]
15    }
16  }
17}
18