An optional /mods menu that lists the installed mods (status line, clean screen, mode) and opens each one's page in a pane.

An optional /mods menu. It opens a small pane that lists the mods you have installed from this collection (status line, clean screen, mode, permissions), each with a one-line summary. Pick one to open its settings page; "back" returns to the list.
◆ mods button sits in the footer under the prompt (the status line draws it when it is on).1 to 4 open the rows; b goes back; esc closes.Install:
/plugin install menu@malka-claude-mods
Use: /mods, or /mods statusline, /mods cleanscreen, /mods mode, /mods permissions to open straight on a page.
Uninstall: /plugin uninstall menu@malka-claude-mods. The other mods keep working; they simply stop showing the ◆ mods button.
Known limits: the open/closed state is kept in memory, so a hot reload can leave it out of step with the pane; if the menu is disabled mid-session, the other mods still think it is present until you restart.
Um menu /mods opcional. Abre um painel pequeno que lista os mods desta coleção que você instalou (status line, clean screen, mode, permissions), cada um com um resumo de uma linha. Escolha um para abrir a página de configurações; "back" volta à lista.
◆ mods fica no rodapé, sob o prompt (a status line o desenha quando está ligada).1 a 4 abrem as linhas; b volta; esc fecha.Instalar:
/plugin install menu@malka-claude-mods
Usar: /mods, ou /mods statusline, /mods cleanscreen, /mods mode, /mods permissions para abrir direto numa página.
Desinstalar: /plugin uninstall menu@malka-claude-mods. Os outros mods continuam funcionando; só deixam de mostrar o botão ◆ mods.
Limites conhecidos: o estado aberto/fechado fica em memória, então um hot reload pode deixá-lo fora de sincronia com o painel; se o menu for desativado no meio da sessão, os outros mods ainda o veem como presente até reiniciar.
hooks/register.tsx 173 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type {} from '../types/statusline'
5import type {} from '../types/cleanscreen'
6import type {} from '../types/mode'
7import type {} from '../types/permissions'
8
9const panelAtom = atom({ plugin: 'menu', key: 'panel' } as const, 'main')
10const presentAtom = atom({ plugin: 'menu', key: 'present' } as const, false)
11
12// Members known statically (state refs must be source literals). Each publishes `menuEntry` (apiVersion 1).
13const statuslineEntry = { plugin: 'statusline', key: 'menuEntry' } as const
14const cleanscreenEntry = { plugin: 'cleanscreen', key: 'menuEntry' } as const
15const modeEntry = { plugin: 'mode', key: 'menuEntry' } as const
16const permissionsEntry = { plugin: 'permissions', key: 'menuEntry' } as const
17
18const PANE = 'mods-menu'
19const NAMES = ['statusline', 'cleanscreen', 'mode', 'permissions']
20
21type Row = { name: string; title: string; summary: string; order: number }
22
23// The installed members' entries: missing, malformed or another apiVersion are skipped; sorted by `order`.
24async function entries($: EngineInterface): Promise<Row[]> {
25 const none = () => ({ value: undefined })
26 const found = [
27 (await $.state.get(statuslineEntry).catch(none)).value,
28 (await $.state.get(cleanscreenEntry).catch(none)).value,
29 (await $.state.get(modeEntry).catch(none)).value,
30 (await $.state.get(permissionsEntry).catch(none)).value,
31 ]
32 const rows: Row[] = []
33 found.forEach((v, i) => {
34 if (v && v.apiVersion === 1 && typeof v.title === 'string') rows.push({ name: NAMES[i]!, title: v.title, summary: String(v.summary), order: Number(v.order) || 0 })
35 })
36 return rows.sort((a, b) => a.order - b.order)
37}
38
39// The engine's hint on one row: it can carry a second line holding only a separator, which drew as a stray dot.
40const hintText = (hint: string) =>
41 hint
42 .split(/\r?\n/)
43 .map(line => line.trim())
44 .filter(line => /[\p{L}\p{N}]/u.test(line))
45 .join(' · ')
46
47let isOpen = false
48
49async function toggle($: EngineInterface, panel = 'main') {
50 if (isOpen) {
51 isOpen = false
52 await $.ui.close({ id: PANE })
53 return
54 }
55 await update($, panelAtom, () => panel)
56 isOpen = true
57 await $.ui.open({ id: PANE, title: 'mods', focus: true, closeOnEscape: true, rows: 16, columns: 56 })
58}
59
60const goTo = ($: EngineInterface, panel: string) => update($, panelAtom, () => panel)
61
62// A member page only when its entry is present now; otherwise main, with a toast.
63async function goToMember($: EngineInterface, name: string) {
64 if ((await entries($)).some(r => r.name === name)) return goTo($, name)
65 $.ui.toast(`${name} is not installed`)
66 return goTo($, 'main')
67}
68
69async function back($: EngineInterface, e: any, next: any) {
70 await goTo($, 'main')
71 return next(e)
72}
73
74function mainPage({ Box, Text, Button }: { Box: any; Text: any; Button: any }, rows: Row[], onOpen: (name: string) => unknown) {
75 return (
76 <Box flexDirection="column" paddingX={1}>
77 <Text bold color="claude">
78 ◆ mods
79 </Text>
80 {rows.length === 0 && (
81 <Box flexDirection="column" marginTop={1}>
82 <Text>No mods installed</Text>
83 <Text dimColor>Install one with /plugin install statusline@malka-claude-mods</Text>
84 <Text dimColor>(also cleanscreen, mode or permissions).</Text>
85 </Box>
86 )}
87 {rows.map((r, i) => (
88 <Box key={`row-${r.name}`} flexDirection="column" marginTop={1}>
89 <Button
90 key={`open-${r.name}`}
91 plain
92 hotkey={String(i + 1)}
93 autoFocus={i === 0 ? true : undefined}
94 label={`${r.title} ›`}
95 onPress={() => onOpen(r.name)}
96 />
97 <Text dimColor> {r.summary}</Text>
98 </Box>
99 ))}
100 <Box marginTop={1}>
101 <Text dimColor>↑↓ move · enter select · esc close</Text>
102 </Box>
103 </Box>
104 )
105}
106
107export const register: Register = on => {
108 on('session.start', async ($, e, next) => {
109 await update($, presentAtom, () => true)
110 await update($, panelAtom, () => 'main')
111 isOpen = false
112 await $.command
113 .register({ name: 'mods', description: 'Open the mods menu', argumentHint: '[statusline|cleanscreen|mode|permissions]', immediate: true })
114 .catch(() => undefined)
115 return next(e)
116 })
117
118 on('command.run', { command: 'mods' }, async ($, e) => {
119 try {
120 const arg = e.args.trim().toLowerCase()
121 const known = NAMES.includes(arg)
122 const panel = known && (await entries($)).some(r => r.name === arg) ? arg : 'main'
123 if (known && panel === 'main') $.ui.toast(`${arg} is not installed`)
124 if (isOpen) await goTo($, panel)
125 else await toggle($, panel)
126 return { text: 'The mods menu is open.' }
127 } catch (error) {
128 return { text: `The mods menu could not open: ${String(error)}` }
129 }
130 })
131
132 // Main page only: a member draws its own page when the panel is its name (the hooks beneath).
133 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
134 const rows = await entries($)
135 const panel = await read($, panelAtom)
136 // a member page whose entry is gone falls back to main
137 if (panel !== 'main' && rows.some(r => r.name === panel)) return next(e)
138 return mainPage($.ui.resolve(e), rows, name => goToMember($, name))
139 })
140
141 // However the pane closes (esc, its mark, the footer button), the menu resets.
142 on('ui.close', { id: PANE }, async ($, e, next) => {
143 isOpen = false
144 await update($, panelAtom, () => 'main').catch(() => undefined)
145 return next(e)
146 }).catch(() => undefined)
147
148 on('ui.press', { plugin: 'statusline', element: 'mods-back' }, back)
149 on('ui.press', { plugin: 'cleanscreen', element: 'mods-back' }, back)
150 on('ui.press', { plugin: 'mode', element: 'mods-back' }, back)
151 on('ui.press', { plugin: 'permissions', element: 'mods-back' }, back)
152
153 on('ui.press', { plugin: 'statusline', element: 'mods-open' }, async ($, e, next) => {
154 await toggle($)
155 return next(e)
156 })
157
158 // Footer, disjoint with the status line: when it is on, it draws the row and the ◆ mods button itself.
159 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
160 const { value } = await $.state.get(statuslineEntry).catch(() => ({ value: undefined }))
161 if (value?.summary === 'on') return next(e)
162 const { Box, Text, Button } = $.ui.resolve(e)
163 return (
164 <Box justifyContent="space-between" flexGrow={1} gap={1}>
165 <Text dimColor wrap="truncate">
166 {hintText(e.props.hint)}
167 </Text>
168 <Button key="mods-open" plain label="◆ mods" onPress={() => toggle($)} />
169 </Box>
170 )
171 })
172}
173types/index.d.ts 13 lines1// Menu contract, apiVersion 1.
2export type Panel = string
3declare module 'claude-code' {
4 interface PluginState {
5 menu: {
6 // 'main' or the name of the member whose page the pane shows
7 panel: Panel
8 // true while this mod is loaded (set at session.start)
9 present: boolean
10 }
11 }
12}
13