Skip to content

Commands and Registry

Jeremiah edited this page Oct 22, 2026 · 1 revision

Commands and Registry

This page explains how commands connect menus, shortcuts, toolbars, plugins and document workflows.

A command is a named action with a registered handler. ChemDraft uses one command system for core and plugin actions, rather than implementing the same behavior independently in each button. Typed host capabilities provide the other controlled extension boundary; reaching into React state, the DOM or private functions is not an extension API.

Registry contract

CommandRegistry lives in packages/plugin-host/src/index.ts. Its definitions include an ID, title, optional category and description, source, owning plugin, required permissions, default shortcut and enabled state. register associates a definition and handler; invoke dispatches it.

The registry rejects duplicate registration, unknown commands and disabled commands. Plugin ownership matters during rollback and uninstall: unregisterOwnedByPlugin removes a command only if the expected plugin still owns it. This prevents a failed extension registration from deleting another command.

Desktop command specifications in apps/desktop/src/commands.ts add presentation details such as icons and disabled reasons. Registration and invocation remain distinct from that presentation. A command can be visible in several surfaces while still having one handler.

IDs describe the chosen action

Core IDs encode the action and, where needed, its value. Reuse factory helpers rather than building slightly different strings at each call site. Do not introduce generic *.set commands whose real behavior is hidden in an extra parameter, and do not rename existing IDs unless the active plan authorizes it.

Examples confirmed in the desktop catalog:

ID or factory Meaning
document.new Create a document
document.open Open a native document through the host workflow
document.save, document.saveAs Save or choose a save association
edit.undo, edit.redo Route history operations
structure.cleanup2d Clean up a structure's two-dimensional depiction
structure.spin3d Start the Spin 3D workflow
atomElementCommandId(element) Produce atom.setHoveredElement.<element>
atomLabelCommandId(label) Produce a label command, with punctuation normalized to an allowed token
activateDocumentTabCommandId(id) Produce document.activateTab.<id>

Plugin contributions use separate namespaces:

Contribution Pattern
Command plugin.<pluginName>.<action>
Menu menu.<pluginName>.<action>
Panel panel.<pluginName>.<name>
Analyzer analyzer.<pluginName>.<name>

Toolset schemas validate command IDs; they are not arbitrary UI labels. A change to ID encoding may also affect saved toolbar customizations.

From control to handler

toolsets.ts maps registry items into palette models. ToolPalette.tsx renders the inline fallback. Desktop flyouts use PalettePopoverWindow.tsx and the request/snapshot/window-manager path to route a selected command back to the document window.

Do not bypass that route with a private click-only action. It would leave menus, automation and keyboard access with different behavior. The same rule applies to inspector controls and plugin toolbar contributions.

Inline submenus must invoke exactly once per click; disabled entries never invoke. An owner with a disabled primary action can still open enabled submenu choices. If nothing is enabled, the owner is disabled. Use correct owner/menu/menuitem accessibility relationships; a native flyout owner must not point aria-controls at DOM that does not exist.

Unimplemented actions are removed from toolbars and the customize gallery. disabledReason describes temporary, state-dependent unavailability, such as an unsuitable selection, rather than a permanently unfinished feature.

Plugin registration and errors

Every plugin registration path goes through the persistent runtime's registerPlugin and unregisterPlugin. Calling the bare host from a desktop integration path can bypass toolbar permission handling and whole-plugin rollback. Plugin commands share the core registry and carry their pluginId.

A command failure may throw or return { ok: false, error }. Desktop dispatch reports both through the status bar. Do not assume a resolved promise means success, or silently discard a returned failure.

Document-changing commands create validated patches. Deterministic plugin insertion requires the declared document.write capability and that plugin's active invocation. Existing-content changes and recognition results use proposal review. Command wiring does not itself authorize document or native access.

Platform-aware shortcuts

Use the platform-aware shortcut layer rather than treating shortcut strings in the command catalog as final display labels. keyboardShortcuts.ts provides ShortcutPlatform and detectDesktopShortcutPlatform(). Command-based shortcuts use Command on macOS and Control on Windows as appropriate; non-macOS redo also accepts Ctrl+Y. Reload keys must never discard the document by reloading the webview.

Test both platform branches by injecting the platform, then verify actual native menus and focus on macOS and Windows. Windows menus belong only to the document window; macOS menus are application-wide.

See also

Clone this wiki locally