SLOPSHOPPER

tpl-panel

Band above the prompt with this turn's tool calls, and a pane listing the recent ones.

newpanebandguardcommandprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tpl-panel
│ ┃ Tool calls ✕ › fix the failing auth test and add an audit log call │ ┃ c: clear · ctrl+x tab focuses · esc closes │ ┃ ✓ Read ⏺ Read(src/auth.ts) │ ┃ ✓ Grep ⎿ Read 6 lines │ ┃ ✓ Edit ⏺ Update(src/auth.ts) │ ┃ ✓ Write ⎿ Added 2 lines, removed 1 line │ ┃ ✓ Write ⏺ Bash(bun test) │ ┃ ✓ Bash ⎿ 3 pass, 1 fail │ ┃ ✓ Bash │ ┃ ✓ Bash ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ✓ Bash │ ✻ Worked for 42s · done 4:20 PM │ │ › /tpl-panel │ ⎿ tpl-panel: Pane opened. │ │ 9 tool calls this turn · last: Bash o: Open ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
9 tool calls this turn · last: Bash o: Open
Pane · Tool calls
c: clear · ctrl+x tab focuses · esc closes ✓ Read ✓ Grep ✓ Edit ✓ Write ✓ Write ✓ Bash ✓ Bash ✓ Bash ✓ Bash
README

plugin-marketplace

Béla's Claude Code plugins: mods (function-hook plugins that draw panes, bands and status lines), the skill that builds them, and personal skills grouped by theme.

PluginWhat it is
mods/mods opens a keyboard-driven pane that lists your mods, shows what each one does, and switches it on/off now or at startup.
mod-builderA skill that builds new mods to a fixed bar: four tested templates, a mandatory `/<mod> on\off, a bare /<mod>` that toggles what it opened, keyboard-first panes, and a validate + type-check + test gate.
briefingsai-, tech-, geo-, cyber-, econ-, space-briefing, and morning-briefing, which runs them all into one digest.
decksmarp-deck (Marp slides on a tech topic), feature-deck (researches a release and hands it to marp-deck), obsidian-canvas. Decks go to $MARP_DECKS_DIR, else ~/Documents/Presentations.
dev-toolscleanup, deps-audit, docgen, explain, find-bug, justify, package-scout, quick-commit, review-pr, scaffold, summarize-changes.
skill-toolsskill-writer, skill-refactor, skill-guide, agent-retro.

Install

At the prompt of a terminal Claude Code session:

/plugin install mods --marketplace B314-N03/plugin-marketplace
/plugin install mod-builder --marketplace B314-N03/plugin-marketplace
/plugin install briefings --marketplace B314-N03/plugin-marketplace
/plugin install decks --marketplace B314-N03/plugin-marketplace
/plugin install dev-tools --marketplace B314-N03/plugin-marketplace
/plugin install skill-tools --marketplace B314-N03/plugin-marketplace

A plugin's skills are called by their namespaced name, e.g. /briefings:ai-briefing.

Answer y to add the marketplace the first time, then choose the user scope. Get updates with claude plugin update <name>, then /reload-plugins.

Developing here

Add your clone as a folder marketplace instead. Installed plugins are then read straight from the folder, so an edit reaches a session after /reload-plugins, with no version bump:

claude plugin marketplace add ~/Coding/plugin-marketplace
claude plugin install mods@b314
claude plugin install mod-builder@b314
# and briefings@b314, decks@b314, dev-tools@b314, skill-tools@b314

Gate a plugin before committing (the types file path is the one the built-in plugin-authoring skill names when it loads):

bash plugins/mod-builder/skills/mod-builder/scripts/check.sh plugins/mods <types-file>

It must end with == PASS. claude plugin validate . checks the marketplace file.

Layout

.claude-plugin/marketplace.json   the plugin list
plugins/mods/                     hooks module, pure logic, tests, types contract
plugins/mod-builder/skills/mod-builder/
  SKILL.md                        the build procedure
  reference/conventions.md        the rules (off switch, keyboard, tests)
  templates/                      status-line, band-pane, tool-hook, poller
  scripts/                        new-mod.sh, check.sh, promote.sh
Source 3 files
hooks/register.tsx 110 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { addCall, bandText, markDone, paneAction, parseToggle } from './logic'
5
6const NAME = 'tpl-panel'
7const PANE = 'tpl-panel'
8const OFF_KEY = 'isOff'
9const calls = atom({ plugin: 'tpl-panel', key: 'calls' } as const, [])
10const turn = atom({ plugin: 'tpl-panel', key: 'turn' } as const, 0)
11const isOff = atom({ plugin: 'tpl-panel', key: 'isOff' } as const, false)
12
13// A status pane: Esc closes it once the person has given it the keyboard (ctrl+x tab).
14// A pane the person acts in (a picker, a manager) also passes `focus: true`, and
15// calls this again after any `$.command.run`, which hands the keys to the prompt.
16async function openPane($: EngineInterface) {
17  return $.ui.open({ id: PANE, title: 'Tool calls', closeOnEscape: true })
18}
19
20async function setOff($: EngineInterface, value: boolean) {
21  await $.store.set(OFF_KEY, value)
22  await update($, isOff, () => value)
23  if (value) await $.ui.close({ id: PANE })
24}
25
26export const register: Register = on => {
27  on('session.start', async ($, e, next) => {
28    await $.command.register({
29      name: 'tpl-panel',
30      description: `Tool-call band and pane. /${NAME} opens or closes the pane, /${NAME} off hides both, /${NAME} on brings them back`,
31      argumentHint: '[on|off]',
32    })
33    const wasOff = (await $.store.get(OFF_KEY)) === true
34    await update($, isOff, () => wasOff)
35    return next(e)
36  })
37
38  on('prompt.submit', async ($, e, next) => {
39    await update($, turn, n => n + 1)
40    return next(e)
41  })
42
43  on('tool.call', async ($, e, next) => {
44    if (e.agentId !== undefined || (await read($, isOff))) return next(e)
45    const id = e.tool_use_id ?? `${Date.now()}`
46    const currentTurn = await read($, turn)
47    await update($, calls, list => addCall(list, { id, tool: String(e.tool), isDone: false, turn: currentTurn }))
48    const ran = await next(e)
49    await update($, calls, list => markDone(list, id))
50    return ran
51  })
52
53  on('command.run', { command: 'tpl-panel' }, async ($, e) => {
54    const toggle = parseToggle(e.args)
55    if (toggle === 'invalid') return { text: `Use /${NAME}, /${NAME} on or /${NAME} off.` }
56    if (toggle === 'off') {
57      await setOff($, true)
58      return { text: `${NAME} off: band hidden, pane closed (also in new sessions). /${NAME} on to bring it back.` }
59    }
60    if (toggle === 'on') await setOff($, false)
61    if (await read($, isOff)) return { text: `${NAME} is off. /${NAME} on first.` }
62    // A bare command toggles the pane: the way out is the way in.
63    if (toggle === 'show' && paneAction(await $.ui.panes(), PANE) === 'close') {
64      await $.ui.close({ id: PANE })
65      return { text: `Pane closed. /${NAME} opens it again.` }
66    }
67    await openPane($)
68    return { text: toggle === 'on' ? `${NAME} on.` : 'Pane opened.' }
69  })
70
71  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
72    if (e.props.hasSurvey || (await read($, isOff))) return next(e)
73    const text = bandText(await read($, calls), await read($, turn))
74    if (text === null) return next(e)
75
76    const { Box, Button, Text } = $.ui.resolve(e)
77    return (
78      <Box flexDirection="row">
79        <Text dimColor wrap="truncate">
80          {text}{' '}
81        </Text>
82        <Button key="open" label="Open" plain hotkey="o" onPress={() => void openPane($)} />
83      </Box>
84    )
85  })
86
87  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
88    const { Box, Button, Text } = $.ui.resolve(e)
89    const list = await read($, calls)
90    const room = Math.max(1, (e.viewport?.rows ?? 24) - 5)
91
92    return (
93      <Box flexDirection="column">
94        <Box gap={1}>
95          {/* Every Button gets a hotkey: the main screen has no pointer, so a Button without one is unreachable. */}
96          <Button key="clear" label="clear" plain hotkey="c" onPress={() => void update($, calls, () => [])} />
97          <Text dimColor>· ctrl+x tab focuses · esc closes</Text>
98        </Box>
99        {list.length === 0 && <Text dimColor>No tool calls yet.</Text>}
100        {list.slice(-room).map(call => (
101          <Text wrap="truncate" dimColor={call.isDone}>
102            <Text color={call.isDone ? 'success' : 'warning'}>{call.isDone ? '✓' : '…'} </Text>
103            {call.tool}
104          </Text>
105        ))}
106      </Box>
107    )
108  })
109}
110
hooks/logic.ts 29 lines
1import type { TplPanelCall } from '../types'
2
3export const KEEP = 200
4
5export const addCall = (list: readonly TplPanelCall[], call: TplPanelCall) => [...list, call].slice(-KEEP)
6
7export const markDone = (list: readonly TplPanelCall[], id: string) =>
8  list.map(call => (call.id === id ? { ...call, isDone: true } : call))
9
10export const bandText = (list: readonly TplPanelCall[], turn: number) => {
11  const thisTurn = list.filter(call => call.turn === turn)
12  if (thisTurn.length === 0) return null
13  const running = thisTurn.filter(call => !call.isDone).length
14  const last = thisTurn[thisTurn.length - 1]!.tool
15  return `${thisTurn.length} tool calls this turn${running > 0 ? ` · ${running} running` : ''} · last: ${last}`
16}
17
18export type Toggle = 'on' | 'off' | 'show' | 'invalid'
19
20export const parseToggle = (args: string): Toggle => {
21  const arg = args.trim().toLowerCase()
22  if (arg === '') return 'show'
23  return arg === 'on' || arg === 'off' ? arg : 'invalid'
24}
25
26/** What a bare `/<mod>` does to its pane: closes it while the person is looking at it, else opens it (or brings it to the front). */
27export const paneAction = (panes: readonly { id: string; isShown: boolean }[], id: string): 'open' | 'close' =>
28  panes.some(pane => pane.id === id && pane.isShown) ? 'close' : 'open'
29
types/index.d.ts 8 lines
1export type TplPanelCall = { id: string; tool: string; isDone: boolean; turn: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'tpl-panel': { calls: TplPanelCall[]; turn: number; isOff: boolean }
6  }
7}
8