SLOPSHOPPER

mod-menu

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

newbandcommandtimer
★ 2v1.0.3MITupdated 2026-10-26v-amorim/moonlight-claude-mods/mod-menu
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-menu
› 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 › /mod-menu ⎿ mod-menu: No panel mods installed ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

<!-- mod-menu / README.md, by v-amorim --> <!-- Generated from the root README, plugin.json and showcase.json; edit those. -->

mod-menu

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.

Install

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].

How to open

/mod-menu.

See it

The pill rowIcons 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

Settings

Change them in the plugin's settings.

SettingDefaultWhat it does
Menu labelsautoWhen 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

License

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

Source 5 files
hooks/register.tsx 83 lines
1// 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}
83
hooks/theme.ts 46 lines
1// 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))
46
hooks/text.ts 12 lines
1// 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}
12
hooks/style.ts 280 lines
1// 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}
280
types/index.d.ts 7 lines
1// 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