Owns /mod-menu, which hides or shows the panel pills above the prompt; each panel mod draws its own entry there

<!-- mod-menu / README.md, by v-amorim --> <!-- Generated from the root README, plugin.json and showcase.json; edit those. -->
Part of [moonlight-claude-mods][doc-root].
The switch that hides or shows the pill row above the prompt, plus its hover tips; each panel mod draws its own pill.
Inside Claude Code:
/plugin marketplace add v-amorim/moonlight-claude-mods
/plugin install mod-menu@claude-mods
/reload-plugins
/plugin install asks where to install the mod. Keep Install for you (user scope) and press Enter, then press Esc to leave the plugin screen before /reload-plugins.
To install every mod at once, see [Install][doc-install]. For sharper icons, see [Icons and Nerd Fonts][doc-icons].
/mod-menu.
| The pill row | Icons alone, with labels set to never |
|---|---|
| ![][img-menu-band] | ![][img-menu-icons] |
| Hovering an entry shows its tip |
|---|
| https://github.com/user-attachments/assets/f0bb82f3-55aa-4e9e-b195-1744ce13e03f |
Change them in the plugin's settings.
| Setting | Default | What it does | ||
|---|---|---|---|---|
| Menu labels | auto | When pills show their names. auto draws icon and name, and drops every pill to its icon when the row is wider than the window. always keeps the names. never shows icons alone. Icons alone need Nerd Font icons, since safe mode has none. /mod-menu labels auto\ | always\ | never sets it too |
Made by [Vinicius Amorim][doc-author], [MIT license][doc-license].
[doc-author]: https://github.com/v-amorim [doc-icons]: https://github.com/v-amorim/moonlight-claude-mods/blob/main/README.md#icons-and-nerd-fonts [doc-install]: https://github.com/v-amorim/moonlight-claude-mods/blob/main/README.md#install [doc-license]: https://github.com/v-amorim/moonlight-claude-mods/blob/main/LICENSE [doc-root]: https://github.com/v-amorim/moonlight-claude-mods/blob/main/README.md
[img-menu-band]: https://github.com/user-attachments/assets/8c2ca805-0d18-4a6d-bb62-6473649540fb [img-menu-icons]: https://github.com/user-attachments/assets/338a7d58-f331-4109-bd80-fd2b7bb2267d
hooks/register.tsx 83 lines1// mod-menu / hooks/register.tsx, by v-amorim: the /mod-menu command, hidden and labels
2import type { EngineInterface, Register } from 'claude-code'
3
4import { fill } from './theme'
5import text from './text'
6
7const LABELS = ['auto', 'always', 'never'] as const
8type Labels = (typeof LABELS)[number]
9const isLabels = (value: unknown): value is Labels => LABELS.some(each => each === value)
10// The mods that draw an entry in the band; each lists its settings rows in config.list once installed and enabled.
11const PANEL_MODS = ['mod-file-tree', 'mod-heads-up', 'mod-manager', 'mod-model-effort', 'mod-progress', 'mod-prompts', 'mod-skill-panel', 'mod-wrap-up']
12const BOOT_KEY = { plugin: 'mod-menu', key: 'booted' } as const
13const LABELS_KEY = { plugin: 'mod-menu', key: 'labels' } as const
14
15// The labels state no longer says the setting: a settings pane saved a new one, which reloads this mod with no session start.
16const isStale = async ($: EngineInterface, labels: Labels) => (await $.state.get(LABELS_KEY)).value !== labels
17
18// A /clear empties $.state and starts no session, so an unset marker means the two values must be written again.
19async function restore($: EngineInterface, labels: Labels) {
20 if ((await $.state.get(BOOT_KEY)).value) {
21 if (!(await isStale($, labels))) return false
22 await $.state.set(LABELS_KEY, labels)
23 return true
24 }
25 await $.state.set(BOOT_KEY, true)
26 await $.state.set({ plugin: 'mod-menu', key: 'hidden' }, (await $.store.get('hidden')) === true)
27 await $.state.set(LABELS_KEY, labels)
28
29 return true
30}
31
32// Panel mods draw their own entries in the band above the prompt and read `hidden` and `labels`; this mod owns both.
33export const register: Register = (on, options) => {
34 const labels = isLabels(options.labels) ? options.labels : 'auto'
35 let restoring = false
36
37 on('session.start', async ($, e, next) => {
38 await $.command.register({ name: 'mod-menu', description: text.commandDescription })
39 await restore($, labels)
40 $.ui.invalidate('ui.render')
41
42 return next(e)
43 })
44
45 // Drawing may not write state, so the first draw after a /clear schedules the restore and the redraw follows it.
46 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
47 if (!restoring && (!(await $.state.get(BOOT_KEY)).value || (await isStale($, labels)))) {
48 restoring = true
49 $.clock.after(0, async () => {
50 if (await restore($, labels).catch(() => false)) $.ui.invalidate('ui.render')
51 restoring = false
52 })
53 }
54
55 return next(e)
56 })
57
58 on('command.run', { command: 'mod-menu' }, async ($, e) => {
59 const [word, mode] = (e.args ?? '').trim().split(/\s+/)
60 if (word === 'labels') {
61 if (!isLabels(mode)) return { text: text.labelsUsage }
62 // The saved setting reloads this mod; the state is set now so the pills redraw at once.
63 await $.config.set({ key: 'mod-menu.labels', value: mode })
64 await $.state.set(LABELS_KEY, mode)
65 $.ui.invalidate('ui.render')
66
67 return { text: fill(text.labelsSet, { mode }) }
68 }
69 if (word) return { text: text.usage }
70
71 const rows = await $.config.list().catch(() => [])
72 if (!rows.some(row => PANEL_MODS.some(mod => row.key.startsWith(`${mod}.`)))) return { text: text.noPanels }
73
74 await restore($, labels)
75 const hidden = (await $.state.get({ plugin: 'mod-menu', key: 'hidden' })).value !== true
76 await $.state.set({ plugin: 'mod-menu', key: 'hidden' }, hidden)
77 await $.store.set('hidden', hidden)
78 $.ui.invalidate('ui.render')
79
80 return { text: text[hidden ? 'menuHidden' : 'menuShown'] }
81 })
82}
83hooks/theme.ts 46 lines1// hooks/theme.ts, by v-amorim
2// Shared theme; every mod carries the same copy. Colors, marks and glyphs live in style.ts.
3import style from './style'
4
5export const THEME = style.colors.theme
6
7// Shell command roles: what runs, its options, quoted text, variables, numbers and the glue between steps.
8export const SYNTAX = style.colors.syntax
9
10// One cell each and never an emoji: ⚠ switches to its emoji form in some fonts and draws two cells wide.
11export const MARK = style.marks
12
13// style.ts writes Nerd glyphs as {U+XXXX}, since private-use characters do not survive many editors and tools.
14const decode = (text: string) => text.replace(/\{U\+([0-9A-F]+)\}/gi, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))
15
16export function decodeAll<T>(value: T): T {
17 if (typeof value === 'string') return decode(value) as T
18 if (Array.isArray(value)) return value.map(decodeAll) as T
19 return Object.fromEntries(Object.entries(value as object).map(([key, inner]) => [key, decodeAll(inner)])) as T
20}
21
22// A pill's rounded ends, drawn only where the font has them.
23export const CAPS = decodeAll(style.caps)
24
25export const statusColor = (note: string) =>
26 note.startsWith(MARK.ok) ? THEME.good : note.startsWith(MARK.warn) ? THEME.alert : THEME.muted
27
28const channels = (hex: string) => [1, 3, 5].map(at => parseInt(hex.slice(at, at + 2), 16))
29const isHex = (color: string) => /^#[0-9a-f]{6}$/i.test(color)
30
31// The color `t` of the way from `from` to `to`; a color that is not #RRGGBB switches halfway.
32export function mix(from: string, to: string, t: number) {
33 if (t <= 0) return from
34 if (t >= 1) return to
35 if (!isHex(from) || !isHex(to)) return t < 0.5 ? from : to
36 const end = channels(to)
37 return `#${channels(from).map((value, at) => Math.round(value + (end[at]! - value) * t).toString(16).padStart(2, '0')).join('')}`
38}
39
40// The share of a hue left in a subpill or count badge, washed into the fill it sits on.
41export const WASH = 0.72
42
43// A text.ts string with its {name} slots filled; a slot with no value stays as written, so a missing one shows.
44export const fill = (template: string, values: Record<string, string | number> = {}) =>
45 template.replace(/\{(\w+)\}/g, (slot, name: string) => (name in values ? String(values[name]) : slot))
46hooks/text.ts 12 lines1// mod-menu / hooks/text.ts, by v-amorim
2// Every string this mod shows. Edit the values to reword or translate; keep the keys and the {slots}.
3export default {
4 "commandDescription": "Hide or show the panel pills above the prompt, or set their labels (labels auto|always|never)",
5 "usage": "Usage: /mod-menu (hides or shows the menu), /mod-menu labels auto|always|never",
6 "labelsUsage": "Usage: /mod-menu labels auto|always|never",
7 "labelsSet": "Menu labels: {mode}",
8 "menuHidden": "Menu hidden",
9 "menuShown": "Menu shown",
10 "noPanels": "No panel mods installed"
11}
12hooks/style.ts 280 lines1// hooks/style.ts, by v-amorim
2// Shared style; every mod carries the same copy. Change values freely, keep the keys. {U+XXXX} is a Nerd Font code point.
3export default {
4 "colors": {
5 "theme": {
6 "bg": "#0D0E17",
7 "surface1": "#141726",
8 "surface2": "#1C2033",
9 "surface3": "#252A42",
10 "lifted": "#2F3654",
11 "trackOff": "#4B5270",
12 "border": "#242839",
13 "onSurface": "#EEEEFA",
14 "muted": "#9BA3C4",
15 "accent": "#7386D0",
16 "secondary": "#B9C6F5",
17 "good": "#49EF95",
18 "ok": "#A2B0EA",
19 "alert": "#CA5F71",
20 "warn": "#FFCB6B",
21 "folder": "#86E1FC",
22 "pill": "#B9C6F5",
23 "addBg": "#1D3B38",
24 "delBg": "#3D2737"
25 },
26 "syntax": {
27 "command": "#7386D0",
28 "option": "#FFAFAF",
29 "text": "#A5CFFF",
30 "variable": "#79C0FF",
31 "number": "#B0C8FF",
32 "operator": "#CDCFE7"
33 },
34 "pills": {
35 "primary": "#252A42",
36 "secondary": "#1C2033",
37 "separator": "#3B446A",
38 "readout": "#7E86A4"
39 },
40 "shimmer": [
41 "#A2B0EA",
42 "#949FCE",
43 "#8993B9"
44 ],
45 "usage": [
46 "#B9C6F5",
47 "#ACB9E6",
48 "#A5A1E9",
49 "#CF95CA",
50 "#CA5FA6",
51 "#CA5F71"
52 ],
53 "statusline": {
54 "pillBorder": "#3A3D4A",
55 "menuBackground": "#1C1D24",
56 "divergence": "#B86CB3",
57 "behind": "#8A9BE0"
58 },
59 "models": {
60 "opus": "#C3A6FF",
61 "sonnet": "#79C0FF",
62 "haiku": "#7CD591",
63 "inherit": "#9BA3C4"
64 },
65 "kinds": {
66 "thinking": { "hue": "#C3A6FF", "wash": "#494267" },
67 "read": { "hue": "#B9C6F5", "wash": "#464C64" },
68 "search": { "hue": "#A5CFFF", "wash": "#404E67" },
69 "edit": { "hue": "#B9C6F5", "wash": "#4C5167" },
70 "shell": { "hue": "#FFCB6B", "wash": "#5B4D3B" },
71 "web": { "hue": "#79C0FF", "wash": "#324A67" },
72 "agent": { "hue": "#F99BD1", "wash": "#593F59" },
73 "tool": { "hue": "#9BA3C4", "wash": "#3D4155" }
74 }
75 },
76 "tags": {
77 "router": {
78 "color": "#F99BD1",
79 "icon": "◆"
80 },
81 "debug": {
82 "color": "#FF9C8B",
83 "icon": "‼"
84 },
85 "interface": {
86 "color": "#EEB154",
87 "icon": "▪"
88 },
89 "writing": {
90 "color": "#C3C55A",
91 "icon": "≡"
92 },
93 "utility": {
94 "color": "#7CD591",
95 "icon": "☼"
96 },
97 "start": {
98 "color": "#2AD7D7",
99 "icon": "»"
100 },
101 "mods": {
102 "color": "#5DCBFF",
103 "icon": "+"
104 },
105 "review": {
106 "color": "#D1A8FF",
107 "icon": "∴"
108 },
109 "personal": {
110 "color": "#9BA3C4",
111 "icon": "●"
112 }
113 },
114 "levels": {
115 "low": { "glyph": "○", "color": "#ACB9E6" },
116 "medium": { "glyph": "◐", "color": "#A5A1E9" },
117 "high": { "glyph": "●", "color": "#CF95CA" },
118 "xhigh": { "glyph": "◉", "color": "#CA5FA6" },
119 "max": { "glyph": "◈", "color": "#CA5F71" }
120 },
121 "statuslineMarks": {
122 "branch": "⑂",
123 "dirty": "●",
124 "diverged": "⇅",
125 "ahead": "↑",
126 "behind": "↓",
127 "pathSep": "›",
128 "menu": "⋯",
129 "close": "×"
130 },
131 "text": {
132 "settings": "Settings",
133 "actions": "Actions",
134 "noSettings": "No settings.",
135 "on": "On",
136 "off": "Off",
137 "locked": "locked",
138 "editInConfig": "edit it in /config",
139 "idleOnly": "available once Claude is idle",
140 "runsCommand": "runs /{command}"
141 },
142 "marks": {
143 "stage": "▸",
144 "cursor": "❯",
145 "bullet": "•",
146 "ok": "✓",
147 "warn": "!",
148 "sep": "·",
149 "stop": "○",
150 "stopOpen": "●",
151 "rail": "│",
152 "rule": "─",
153 "track": "━━",
154 "knob": "●",
155 "spinner": "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏",
156 "open": "↗",
157 "strip": "▎"
158 },
159 "caps": {
160 "left": "{U+E0B6}",
161 "right": "{U+E0B4}"
162 },
163 "glyphs": {
164 "safe": {
165 "on": "[◉]",
166 "off": "[○]",
167 "go": "[↵]",
168 "reveal": "⊞",
169 "cycle": "[↻]",
170 "down": "[−]",
171 "up": "[+]",
172 "locked": "[■]",
173 "close": "[×]",
174 "warn": "!",
175 "ok": "✓",
176 "expanded": "▾",
177 "collapsed": "▸",
178 "more": "▾",
179 "less": "▴",
180 "back": "<",
181 "title": "▸",
182 "gear": "⊛",
183 "copy": "⧉",
184 "icon": { "cost": "$", "arrow": "→" },
185 "mark": {
186 "thinking": "∴",
187 "read": "≡",
188 "search": "?",
189 "edit": "±",
190 "shell": "$",
191 "web": "@",
192 "agent": "◆",
193 "tool": "•"
194 }
195 },
196 "nerd": {
197 "on": " {U+F205} ",
198 "off": " {U+F204} ",
199 "go": " {U+F054} ",
200 "reveal": "{U+EAF7}",
201 "cycle": " {U+F021} ",
202 "down": " {U+F068} ",
203 "up": " {U+F067} ",
204 "locked": " {U+F023} ",
205 "close": " {U+F00D} ",
206 "warn": "{U+EA6C}",
207 "ok": "{U+EAB2}",
208 "expanded": "{U+EAB4}",
209 "collapsed": "{U+EAB6}",
210 "more": "{U+F0D7}",
211 "less": "{U+F0D8}",
212 "back": "{U+F053}",
213 "title": "{U+F054}",
214 "gear": "{U+F0493}",
215 "copy": "{U+F018F}",
216 "icon": {
217 "settings": "{U+F1DE}",
218 "actions": "{U+F0E7}",
219 "wrapup": "{U+F187}",
220 "features": "{U+F005}",
221 "mod": "{U+F12E}",
222 "installed": "{U+F487}",
223 "arrow": "{U+F061}",
224 "not-installed": "{U+F019}",
225 "icons": "{U+F031}",
226 "updates": "{U+F0AA}",
227 "welcome": "{U+F135}",
228 "apply": "{U+F00C}",
229 "cost": "{U+F155}",
230 "skill": "{U+F0D0}",
231 "prompt": "{U+F075}",
232 "fold": "{U+F066}",
233 "files": "{U+EB86}",
234 "replay": "{U+EA82}",
235 "folder": "{U+EA83}",
236 "folder-open": "{U+EAF7}",
237 "file": "{U+EA7B}",
238 "ext-ts": "{U+E628}",
239 "ext-react": "{U+E625}",
240 "ext-js": "{U+E781}",
241 "ext-json": "{U+E60B}",
242 "ext-md": "{U+E609}",
243 "ext-py": "{U+E73C}",
244 "ext-config": "{U+E615}",
245 "ext-sh": "{U+F489}",
246 "Edit": "{U+F040}",
247 "MultiEdit": "{U+F040}",
248 "NotebookEdit": "{U+F040}",
249 "Write": "{U+F15B}",
250 "Read": "{U+F02D}",
251 "Bash": "{U+F120}",
252 "Grep": "{U+F002}",
253 "Glob": "{U+F002}",
254 "Agent": "{U+EBBC}",
255 "Task": "{U+EBBC}",
256 "Skill": "{U+F0D0}",
257 "WebFetch": "{U+F0AC}",
258 "WebSearch": "{U+F0AC}",
259 "TodoWrite": "{U+F0AE}",
260 "tool": "{U+F0AD}",
261 "menu-skills": "{U+F0D0}",
262 "menu-prompts": "{U+F075}",
263 "menu-filetree": "{U+EB86}",
264 "menu-progress": "{U+EBBC}",
265 "menu-headsup": "{U+EA6C}"
266 },
267 "mark": {
268 "thinking": "{U+F09D1}",
269 "read": "{U+F02D}",
270 "search": "{U+F002}",
271 "edit": "{U+F040}",
272 "shell": "{U+F120}",
273 "web": "{U+F0AC}",
274 "agent": "{U+EBBC}",
275 "tool": "{U+F0AD}"
276 }
277 }
278 }
279}
280types/index.d.ts 7 lines1// mod-menu / types/index.d.ts, by v-amorim: plugin state shape
2declare module 'claude-code' {
3 interface PluginState {
4 'mod-menu': { hidden: boolean; labels: 'auto' | 'always' | 'never'; booted: boolean }
5 }
6}
7