Skip to content

Plugin Runtime and Panels

Jeremiah edited this page Oct 22, 2026 · 1 revision

Plugin Runtime and Panels

This page explains how ChemDraft keeps plugin commands alive across edits and renders their reports in native desktop windows.

One persistent runtime

The desktop owns one persistent plugin runtime through usePluginRuntime and createPluginRuntime. The host receives current document and selection through callbacks. Changing selection, zoom, pages, or undo history must not reconstruct the host, re-register commands, or discard a running plugin's state.

Core and plugin commands share the same CommandRegistry. Plugin-owned registrations carry a pluginId, so invocation can build the correct permission context and unregister can remove only that plugin's commands. All desktop registration paths use the runtime's registerPlugin and unregisterPlugin wrappers. Calling the bare host directly bypasses toolset staging and whole-plugin rollback.

Registration is transactional: validate permissions and references, check conflicts, then register all contributions. A failure must unwind the plugin rather than leaving a toolbar whose command was never registered. Plugin menu items use the same command route in native menus and the browser fallback.

Workers and capability calls

A Web Worker is a separate browser execution environment without the document page's DOM. Analyzer plugins run in per-plugin ES-module workers through PluginWorkerBridge. The bridge uses a versioned ready handshake and request IDs for commands and host capabilities. A handshake must finish before an installed plugin is accepted.

Worker execution does not give a plugin direct native APIs. The bridge services its calls against the host-created, permission-gated command context. Installed worker responses carry a restrictive Content Security Policy: code, nested workers, WebAssembly, and data stay on the same origin. network.fetch is reserved and refused by the current desktop.

terminate() is total teardown: startup and in-flight commands are rejected, not left waiting. Disable, uninstall, failed installation, and replacement all use this lifecycle. Closing a report is different from disabling the plugin; the panel-close hook lets that plugin cancel the work associated with that panel.

For authors, an installable package owns src/workerEntry.ts and calls runPluginWorker. Do not re-export that side-effecting entry from the public src/index.ts. Packaging uses worker.format: "es" and relative asset references. Keep all built assets together.

Report data, not plugin UI code

Plugins call panels.showReport(panelId, report) with a PluginPanelReport. The panel must be declared and ui.panel must be present. The host's single PluginReportRenderer draws the report.

Section kind Use
text Explanations, warnings, failure details
keyValue Label/value rows
table Columns and string-valued rows
svg A static figure; displayed in an image context so embedded scripts do not execute
chemicalName Host-labelled name and warnings, with a host-provided Copy action
linkedFigure Interactive spectrum data and optional structure geometry with cross-highlighting

New section kinds require an SDK contract and renderer change. An unrecognized kind must not be silently dropped. Plugins supply report data rather than React components, page DOM mutations, or arbitrary event handlers.

A minimal report follows the authoring example:

await context.panels?.showReport("panel.widget.result", {
  title: "Widget",
  sections: [{
    kind: "keyValue",
    rows: [{ label: "Result", value: "Complete" }]
  }]
});

Source tracking and repeated analysis

A report may include source: { objectId, sourceFingerprint }. Capture the fingerprint from the selection used for the calculation. Desktop chrome compares it with the current object and marks the report stale after a change. This is a change detector, not proof that two molecules are chemically identical.

rerunCommandId makes Run again invoke the variant that produced the report. Without it, the host falls back to the panel contribution's command. This matters when one panel displays several command variants.

Analysis records are separate from document objects. analysis.write stores a method, version, source, status, payload, and warnings in the session store. The host stamps identity and time; plugins can read only their own records. Neither a report nor an analysis record automatically inserts anything on the page.

Native windows and focus

On desktop, each report has its own native window, keyed by plugin and panel ID. It can move away from the document and onto another monitor. The browser build uses an in-app fallback.

Behavior macOS Windows
Menu ownership Application-wide menu Menu on the document window only
Ordinary report interaction Drawing retains keyboard focus except deliberate text/control editing Same goal, with native focus hand-back to the document
Click while another app is foreground A user click can activate ChemDraft Background focus requests are refused; ordinary panel interaction keeps the other app in front
Packaged plugin origin tauri://localhost http://tauri.localhost

Opening a report does not deliberately take keyboard focus from drawing. Editable controls and deliberate report-text selection borrow focus. Use Command+C on macOS or Ctrl+C on Windows to copy selected report text. Escape first dismisses an inner view if one is open, or releases an active editing/selection focus lease; with neither, it closes the report window.

Report windows have no direct filesystem, clipboard, or dialog permission. Save and copy operations needing native access travel back to the document window and report success, cancellation, or failure.

Closing and cancellation

onPanelClosed(panelId) is the lifecycle hook supplied with registration or runPluginWorker. Use it to cancel expensive work, and prevent late results from reopening a dismissed panel. Disable and uninstall close panels before tearing down registration so the hook can run. Pending proposals remain in their separate review queue until resolved.

On Windows, the host supports one level of nested module worker beneath a plugin worker through a compatibility shim. Deeper nesting is unsupported there. macOS and browser builds keep native worker behavior; author portable plugins to the supported Windows depth.

See also

Clone this wiki locally