Menus
Menus are data-only collections of command-backed actions available through
ctx.menus. The host chooses whether a menu appears as a popover, dropdown,
context menu, or another suitable surface.
Use a menu for short lists of actions, grouped choices, checks, and switches. Use a dialog when the plugin needs a decision. Plugins are currently headless and cannot contribute custom sheet components.
Permission
Section titled “Permission”Request registry.menu to access ctx.menus. Also request
registry.command when the plugin registers item commands or opens and closes
the menu itself:
{ "recall": { "permissions": ["registry.menu", "registry.command"] }}Register a menu
Section titled “Register a menu”Each item points to a command instead of embedding executable behavior:
const registration = await ctx.menus.register({ id: "focus-tools.note-actions", name: "Note actions", entries: [ { type: "item", id: "archive", label: "Archive note", icon: "ArchiveIcon", order: 100, command: { id: "focus-tools.note.archive", payload: { noteId: "note_123" }, }, }, ],});A menu has a stable id, diagnostic name, and an entries array. Recall
assigns ownership and registration time from the active sandbox. Menu and item
ids should be stable and namespaced to the plugin. Duplicate ids are rejected.
Entries
Section titled “Entries”A menu can mix standalone items and groups:
| Entry | Purpose |
|---|---|
| Item | A selectable row that may run a command. |
| Check | An item with control: { type: "check", value }. |
| Switch | An item with control: { type: "switch", value }. |
| Group | A type: "group" entry containing related items. |
Items can also provide description, rightIcon, variant, and
config.disabled. Set config.closeOnSelect when the menu should remain open
or close after selection according to the host’s supported behavior.
Top-level entries and items within a group are sorted by order. Lower values
appear first; ids provide a stable tie-breaker.
const settingsGroup = { type: "group" as const, id: "display", label: "Display", order: 200, items: [ { type: "item" as const, id: "compact", label: "Compact view", icon: "Layers01Icon", order: 100, control: { type: "switch" as const, value: true }, command: { id: "focus-tools.compact.toggle" }, }, ],};Open and close
Section titled “Open and close”Registering a menu creates two commands:
menu.<menu-id>.openmenu.<menu-id>.closeawait ctx.commands.run("menu.focus-tools.note-actions.open");await ctx.commands.run("menu.focus-tools.note-actions.close");Closing a menu that is already closed is safe. A menu item forwards its
command.payload unchanged, so the receiving command must validate it.
Update a menu
Section titled “Update a menu”Use update to change entries without registering a second menu. The returned
menu is normalized again, and an open host surface can react to the update:
await ctx.menus.update("focus-tools.note-actions", (menu) => ({ ...menu, entries: menu.entries.map((entry) => entry.type === "item" && entry.id === "archive" ? { ...entry, config: { ...entry.config, disabled: true } } : entry, ),}));Keep the same menu id when updating it. Treat values returned by get and
list as read-only.
Lifecycle and events
Section titled “Lifecycle and events”Dispose the registration when the plugin unloads. Disposal removes the menu, closes it if necessary, and removes its generated commands:
await registration.dispose();Use on(event, listener) for every menu or pass { id, listener } to observe
one menu:
const subscription = await ctx.menus.on("update", { id: "focus-tools.note-actions", listener({ menu }) { console.log("Menu updated", menu); },});
await subscription.dispose();Supported events are register, update, unregister, listChange, open,
and close. Targeted subscriptions are available for every event except
listChange.
API summary
Section titled “API summary”| Method | Purpose |
|---|---|
register(menu) | Register a menu and return its disposable handle. |
update(id, updater) | Replace an existing menu with the normalized result. |
unregister(id) | Remove a menu by id. |
get(id) | Read one registered menu. |
list() | Read a snapshot of all menus. |
on(event, listener) | Subscribe globally or by menu id. |