Files

Command System

The command system is the single source of truth for what the app can do and the wiring that lets a keyboard shortcut, an application/context menu item, or a button all trigger the same behavior.

It replaces three previously independent systems (keyboard shortcuts, the native application menu, and adhoc context menus), each of which used to maintain its own definitions, keyformatting, and dispatch wiring.

  • command-usage.md — how to register handlers, contribute menus, render commandbacked UI, and add a new command.

Commands, shortcuts, and menus — the relationship

A command is what the app does; a shortcut is one way to ask for it. They are deliberately separate concepts:

  • A command is the unit of behavior, identified by a CommandId (e.g. topic.create, app.zoom.in, chat.message.search). It owns the behavior and knows nothing about how it was triggered.
  • A shortcut is a key binding for a command. A menu item is a menu entry for a command. A button invokes a command. All of them are just triggers that resolve to a CommandId and run its handler.
 keyboard shortcut ─┐
 menu item ─────────┼──▶  CommandId  ──▶  handler (renderer or main)
 button / palette ──┘

Two consequences fall out of this split:

  • There are no freefloating shortcuts. Every shortcut, menu item, and button resolves to a command. You never bind a key to an inline callback — you bind it to a CommandId, and a surface registers the handler separately. Adding a new way to trigger something never touches the behavior, and changing the behavior never touches its triggers.
  • A command can have zero, one, or several triggers. A command may be menuonly (no default key), keyboardonly, or both; the keybinding even allows additionalBindings (e.g. numpad zoom). The command is the same either way.

How a command relates to its shortcut

Concept Where it lives Example for topic.create
Command definition COMMAND_DEFINITIONS (src/shared/command/definitions.ts) { id: 'topic.create', scope: 'renderer', keybinding: { defaultBinding: ['CommandOrControl','N'] } }
Default key binding the command's keybinding.defaultBinding Cmd/Ctrl + N
User override the preference shortcut.<commandId> shortcut.topic.create{ binding, enabled }
Handler a surface via useCommandHandler (renderer) or a builtin (main) useCommandHandler('topic.create', addNewTopic)
Menu entry (optional) a MENU_CONTRIBUTIONS entry { location: 'chat.input.tools.context', command: 'topic.create' }

So there is exactly one shortcut preference key per command (shortcut.<id>): the command's default binding comes from its definition, and the user's edit in Settings → Shortcuts overrides it through that preference key. At runtime the effective binding is "user preference if set, else the definition default".

A command's scope: 'main' | 'renderer' | 'both' decides where its handler runs and who listens for its key: the mainprocess globalshortcut registrar (ShortcutService) for main/global, or the perwindow keydown dispatcher (CommandProvider) for renderer.

COMMAND_DEFINITIONS is the single source of truth — the CommandId union, the keybinding rules, the percommand shortcut.<id> key, and the when/enablement context expressions are all derived from it. Menu contributions are a parallel declaration (MENU_CONTRIBUTIONS) keyed by the same CommandIds.

Architecture — three layers

1. Shared declarations — src/shared/command/

Pure data and pure functions, no Electron or React.

File Responsibility
definitions.ts COMMAND_DEFINITIONS (SoT), the derived CommandId, KEYBINDING_RULES, REGISTERED_KEYBINDINGS, lookups
keybindings.ts resolve a binding → command, default/effective shortcut preference, conflict detection, label formatting
menus.ts MENU_CONTRIBUTIONS, the MenuRegistry, and resolveMenuPresentationMode
contextExpr.ts parser/evaluator for when/enablement expressions + ContextKeyService
types.ts all command/keybinding/menu/context types

Token formatting (typed shortcut vocabulary, normalization, display/accelerator formatting) lives in src/shared/shortcuts/tokens.ts; src/shared/shortcuts/types.ts keeps only ShortcutPreferenceKey + ResolvedShortcut.

2. Main runtime — src/main/services/

Service Responsibility
CommandService holds the mainside handler registry; execute(command, window?, ctx?) with context evaluation; wires builtin handlers (window/zoom/settings/quickassistant/selection); registers the native popup menu IPC (NativeCommandPopupMenu_Show)
nativePopupMenu.ts stateless module — materializes a renderersupplied menu model into an Electron native popup and reports the chosen command back; CommandService injects the execute/gate callback
ShortcutService registers globalShortcut accelerators from REGISTERED_KEYBINDINGS (nonrenderer scope) → CommandService.execute
AppMenuService builds the macOS app menu from menuRegistry.resolve({ location: 'app.menu' }) via menu/adapters/nativeMenuAdapterCommandService.execute

3. Renderer runtime — src/renderer/features/command/

Piece Responsibility
CommandProvider one windowlevel keydown dispatcher + the handler stack (useCommandHandler, useCommandRuntime)
ContextKeyProvider windowlocal context keys (useCommandContextKey)
presentation.tsx CommandShortcut, CommandTooltip, CommandButton, useResolvedCommand
menus.tsx CommandContextMenu — renders Cherry UI or a native popup based on menu.presentation_mode

Mount <ContextKeyProvider><CommandProvider> once per renderer window — every window root mounts it: windows/main/MainApp.tsx, windows/settings/SettingsApp.tsx, and windows/subWindow/SubWindowApp.tsx.

Preferences

  • shortcut.<commandId>PreferenceShortcutType ({ binding, enabled }), the editable binding per command. Generated through the dataclassify pipeline (see command-usage.md).
  • menu.presentation_mode'cherry' | 'native'. Read by CommandProvider to choose the menu renderer. There is no settings UI for it yet (planned); it currently defaults via the preference schema.

Dispatch flows

  • Keyboard (renderer): keydownCommandProvidergetShortcutBindingFromKeyboardEventresolveCommandByKeybinding({ scope: 'renderer', canExecuteCommand: hasHandler }) → active handler. While an editable target (<input>, <textarea>, or a contenteditable element) is focused the dispatcher skips no-modifier shortcuts so typing isn't hijacked; modifier shortcuts (Ctrl/Meta/Alt) still fire. It only preventDefaults when a command with a registered handler resolves.
  • Keyboard (global): OS globalShortcutShortcutServiceCommandService.execute(command, window).
  • Native menu: renderer builds a NativePopupMenuModelwindow.api.command.showNativePopupMenuCommandService's NativeCommandPopupMenu_Show handler → showNativePopupMenu (in nativePopupMenu.ts). Mainhandled commands run there; rendererhandled ones are returned to the renderer runtime to execute.