Repository navigation
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.
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.
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.
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.
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.
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.
ChemDraft repository · Apache-2.0 · Home
- Features overview
- Installation
- Quick Start
- Your first molecule
- FAQ
- Troubleshooting
- Glossary
- macOS and Windows differences
- Drawing basics
- Atoms and labels
- Bonds and stereochemistry
- Rings and chains
- Selection and editing
- Charges, radicals, and dative bonds
- Copy, paste, and clipboard
- Keyboard shortcuts
- Toolbars and palettes
- Documents, pages, and tabs
- Arrows and mechanisms
- Art objects and styling
- Publication figures and style presets
- Compound layouts
- Templates and abbreviations
- Chemistry guarantees
- Analysis and properties
- NMR prediction
- Name from structure
- Name to structure
- Structure recognition
- Spin 3D