Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com> Signed-off-by: suyao <sy20010504@gmail.com>
5.2 KiB
Command System — Usage
How renderer and main code uses the command system. For the model and architecture, see README.md.
Public entry (renderer)
Import from the barrel only:
import { CommandContextMenu, CommandShortcut, CommandTooltip, useCommandHandler } from '@renderer/features/command'
Do not import subpaths such as @renderer/features/command/presentation from business
code. Keeping a narrow public API lets the runtime change without rewriting call
sites.
The renderer domain (src/renderer/features/command/) is intentionally not under
components/ — most files are runtime plumbing rather than generic UI. It owns no
business state: business surfaces contribute only the minimal context keys and
handlers they are responsible for.
Boundaries
- Shared command metadata, keybindings, menu contributions, and context‑expression
parsing live in
src/shared/command. - Main‑process command execution, native menu creation, and global shortcuts belong to main services.
- Renderer business components must not parse shortcut preferences, format shortcut labels, or resolve menu contributions directly — use the primitives below.
Registering handlers
CommandProvider resolves a keypress to a CommandId; components supply the
behavior:
useCommandHandler('topic.create', handleCreateTopic, { enabled: canCreateTopic })
For the same command, the most recently mounted enabled handler wins; when it unmounts, the previous enabled handler becomes active again. A command with no registered handler never resolves (so the keypress falls through untouched).
While an editable target (
<input>,<textarea>, or acontenteditableelement) is focused, the dispatcher skips no-modifier shortcuts by design — plain keys (Escape, single letters) don't fire commands while the user is typing. Modifier shortcuts (Ctrl/Meta/Alt) still fire everywhere. Don't reach for a per‑component keydown listener to work around this; if a no-modifier command genuinely must fire inside an editor, that's a context‑key/enablement decision to discuss.
Context keys
ContextKeyProvider is window‑local. Context keys are not persisted and are not
synced across windows. Base keys are provided automatically: platform,
feature.quick_assistant.enabled, feature.selection.enabled.
Business surfaces contribute scoped keys:
useCommandContextKey('chat.active', true)
Allowed renderer keys are defined by RendererCommandContextKey; add one only
when an existing command, shortcut, or menu contribution needs it. Scoped keys use
stack semantics — the latest mounted value wins, unmounting restores the previous.
undefined unsets a key; false and null are valid values.
Menus
Use CommandContextMenu for renderer context menus that participate in the
command system:
- Command‑backed items come from
MenuRegistryinsrc/shared/command. - Renderer‑only extra items use
extraItems/getExtraItems(type: 'item'for actions,type: 'submenu'for nested groups). - Use
shortcutCommandon an extra item so the menu resolves the platform label and user preference;shortcutLabelis an escape hatch for non‑command shortcuts.
The same resolved menu model renders through the native adapter or Cherry UI based
on menu.presentation_mode. app.menu and tray.menu always stay native (main
process services).
Presentation
Use these instead of assembling labels/shortcuts in feature components:
CommandShortcut— standalone shortcut badgeCommandTooltip— tooltip content including the command shortcutCommandButton— command‑backed buttonuseResolvedCommand— custom UI needing the command label, enabled state, shortcut label, and execute callback
Adding a command
- Declare it in
src/shared/command/definitions.ts— add an entry toCOMMAND_DEFINITIONS(id,titleKey,categoryKey,scope, optionalkeybindingwith adefaultBinding, optionalenablement). - Add its shortcut preference key
shortcut.<commandId>through the data‑classify pipeline — add an entry tov2-refactor-temp/tools/data-classify/data/target-key-definitions.json(type: "PreferenceTypes.PreferenceShortcutType",defaultValue: { binding, enabled }), then regenerate:(Never hand‑editcd v2-refactor-temp/tools/data-classify && npm run generate:preferences npx biome format --write src/shared/data/preference/preferenceSchemas.tspreferenceSchemas.ts.) - Provide a handler. Renderer‑scope:
useCommandHandler(id, fn)in the owning surface. Main‑scope: add a built‑in handler inCommandService.registerBuiltInHandlers. - Optional — contribute it to a menu by adding a
MENU_CONTRIBUTIONSentry insrc/shared/command/menus.tsfor the relevantMenuLocation.
Tests
Renderer command tests live in src/renderer/features/command/__tests__/; shared
declarations in src/shared/command/__tests__/; main services in
src/main/services/__tests__/.
Prefer targeted checks first:
pnpm vitest run src/shared/command src/renderer/features/command
pnpm typecheck
Run broader suites when the change touches shared command behavior, main menu services, or cross‑window contracts.