SLOPSHOPPER

switchboard

/mods control panel: list every mod, auto-discover new ones, toggle them, hide their panels.

newpanecommandtoasttimer
v0.1.0no licenseupdated 2026-10-25ikentrock/starwars-claude-mods/switchboard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · switchboard
│ ┃ Switchboard ✕ › fix the failing auth test and add an audit log call │ ┃ Mods │ ┃ No mods found yet. ⏺ Read(src/auth.ts) │ ┃ Panels ⎿ Read 6 lines │ ┃ No other panels open. ⏺ 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 │ │ › /mods │ ⎿ switchboard: Switchboard open. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Switchboard
Mods No mods found yet. Panels No other panels open.
README

starwars-claude-mods

Nine local Claude Code mods (UI and hook plugins) with a light Star Wars flavor.

ModWhat it does
brain-dumpIntercepts long, unstructured prompts, asks clarifying questions, rewrites them into a structured prompt. !raw prefix skips it.
helper-deskProgress bar from the to-do list, plus a side panel for sub-agents (/helper-desk).
cache-clockPrompt-cache countdown, Keep Warm button, /keep-warm.
handoffGenerates a handoff prompt at 250k tokens, on cold cache, or on /handoff.
reflectOffers to save rules/preferences to memory, CLAUDE.md, or a second brain.
chat-inventoryZero-token "In This Chat" panel (/inventory).
switchboard/mods panel to toggle mods and hide panels.
power-cellsGauges above the prompt for context, the 5-hour limit, and a weekly pacing plan: a daily ration, work-day weighting, over/under-plan warnings. /power plan shows the week.
droid-consoleRestyles the whole UI as a droid console: personality palettes, transmission headers, droid spinner words, status band. /droid switches personality.

Status: experimental. They pass claude plugin validate and tsc, but have not been tested at runtime. Expect rough edges.

Install

Add the marketplace once, then install the mods you want (user scope makes them load in every session):

claude plugin marketplace add ikentrock/starwars-claude-mods
claude plugin install brain-dump@starwars-claude-mods --scope user
claude plugin install cache-clock@starwars-claude-mods --scope user
claude plugin install chat-inventory@starwars-claude-mods --scope user
claude plugin install droid-console@starwars-claude-mods --scope user
claude plugin install handoff@starwars-claude-mods --scope user
claude plugin install helper-desk@starwars-claude-mods --scope user
claude plugin install power-cells@starwars-claude-mods --scope user
claude plugin install reflect@starwars-claude-mods --scope user
claude plugin install switchboard@starwars-claude-mods --scope user

Or one at a time from a session prompt:

/plugin install <mod> --marketplace ikentrock/starwars-claude-mods

Update later with claude plugin marketplace update starwars-claude-mods.

Source 2 files
hooks/register.tsx 151 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { SwitchMod } from '../types'
5
6const PANE = 'switchboard'
7const STORE_KEY = 'off'
8
9const mods = atom({ plugin: 'switchboard', key: 'mods' } as const, [], { shape: 'mods-1' })
10const off = atom({ plugin: 'switchboard', key: 'off' } as const, [], { shape: 'off-1' })
11const panes = atom({ plugin: 'switchboard', key: 'panes' } as const, [], { shape: 'panes-1' })
12
13const seen = new Map<string, SwitchMod>()
14
15// Mods found on disk (this session's mod folders, and installed plugins) are
16// merged with every plugin the engine admits, so a new mod appears by itself.
17async function refresh($: EngineInterface) {
18  const home = (await $.env.get('HOME')) ?? ''
19  const roots = [`${home}/.claude/dev-mods`]
20  for (const top of roots) {
21    if (!(await $.fs.exists(top))) continue
22    for (const session of await $.fs.list(top)) {
23      if (session.kind !== 'dir') continue
24      for (const mod of await $.fs.list(`${top}/${session.name}`)) {
25        if (mod.kind !== 'dir') continue
26        const dir = `${top}/${session.name}/${mod.name}`
27        const manifest = `${dir}/.claude-plugin/plugin.json`
28        if (!(await $.fs.exists(manifest))) continue
29        try {
30          const json = JSON.parse(await $.fs.read(manifest)) as {
31            name?: string
32            description?: string
33          }
34          const name = json.name ?? mod.name
35          if (!seen.has(name)) seen.set(name, { name, description: json.description ?? '', root: dir })
36        } catch {
37          // a manifest still being written
38        }
39      }
40    }
41  }
42  const installed = `${home}/.claude/plugins/installed_plugins.json`
43  if (await $.fs.exists(installed)) {
44    try {
45      const json = JSON.parse(await $.fs.read(installed)) as { plugins?: Record<string, unknown> }
46      for (const key of Object.keys(json.plugins ?? {})) {
47        const name = key.split('@')[0] ?? key
48        if (!seen.has(name)) seen.set(name, { name, description: `installed (${key})`, root: '' })
49      }
50    } catch {
51      // unreadable registry: skip it
52    }
53  }
54  const sorted = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name))
55  await update($, mods, () => sorted)
56  const shown = await $.ui.panes()
57  await update($, panes, () => shown.map(p => ({ id: p.id, title: p.title })))
58}
59
60async function open($: EngineInterface) {
61  await refresh($)
62  await $.ui.open({ id: PANE, title: 'Switchboard', focus: true })
63}
64
65
66export const register: Register = on => {
67  on('session.start', async ($, e, next) => {
68    await $.command.register({ name: 'mods', description: 'Open the Switchboard: toggle your mods' })
69    const stored = await $.store.get(STORE_KEY)
70    if (typeof stored === 'string') await update($, off, () => JSON.parse(stored) as string[])
71    $.clock.every(5000, () => refresh($))
72    await refresh($)
73
74    return next(e)
75  })
76
77  on('command.run', { command: 'mods' }, async $ => {
78    await open($)
79
80    return { text: 'Switchboard open.' }
81  })
82
83  // Every plugin the engine admits joins the list; one switched off is refused.
84  on('plugin.register', async ($, e, next) => {
85    if (e.name !== $.plugin.name && !seen.has(e.name)) {
86      seen.set(e.name, { name: e.name, description: e.provenance, root: e.root })
87    }
88    const stored = await $.store.get(STORE_KEY)
89    const list = typeof stored === 'string' ? (JSON.parse(stored) as string[]) : []
90    if (e.name !== $.plugin.name && list.includes(e.name)) {
91      return { refuse: `Switchboard: ${e.name} is switched off` }
92    }
93
94    return next(e)
95  }).catch(($, e, next) => next(e))
96
97  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
98    const { Box, Button, Text } = $.ui.resolve(e)
99    const list = await read($, mods)
100    const disabled = await read($, off)
101    const open_ = await read($, panes)
102    const width = Math.max(20, (e.props.bodyColumns ?? 50) - 14)
103
104    const toggle = async (name: string) => {
105      const next = (await read($, off)).includes(name)
106        ? (await read($, off)).filter(n => n !== name)
107        : [...(await read($, off)), name]
108      await update($, off, () => next)
109      await $.store.set(STORE_KEY, JSON.stringify(next))
110      $.ui.toast(
111        `Switchboard: ${name} ${next.includes(name) ? 'off' : 'on'}. Mod hooks apply on the next reload.`,
112      )
113    }
114
115    return (
116      <Box flexDirection="column">
117        <Text bold>Mods</Text>
118        {list.length === 0 && <Text dimColor>No mods found yet.</Text>}
119        {list.map(m => (
120          <Box key={m.name}>
121            <Button
122              key={`t-${m.name}`}
123              plain
124              onPress={() => toggle(m.name)}
125              label={`${disabled.includes(m.name) ? '[ off ]' : '[ on  ]'} ${m.name}`}
126            />
127            <Text dimColor> {m.description.slice(0, width)}</Text>
128          </Box>
129        ))}
130        <Text bold>Panels</Text>
131        {open_.length === 0 && <Text dimColor>No other panels open.</Text>}
132        {open_
133          .filter(p => p.id !== PANE)
134          .map(p => (
135            <Box key={p.id}>
136              <Button
137                key={`h-${p.id}`}
138                label="Hide"
139                onPress={async () => {
140                  await $.ui.close({ id: p.id })
141                  await refresh($)
142                }}
143              />
144              <Text> {p.title}</Text>
145            </Box>
146          ))}
147      </Box>
148    )
149  })
150}
151
types/index.d.ts 12 lines
1export type SwitchMod = { name: string; description: string; root: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    switchboard: {
6      mods: Shaped<SwitchMod[]>
7      off: Shaped<string[]>
8      panes: Shaped<{ id: string; title: string }[]>
9    }
10  }
11}
12