Plugins
A Nuclei plugin is a single folder on your disk containing two files: a
plugin.json manifest and a single, dependency-free ES
module whose activate(api) export registers what the plugin
contributes — a custom panel, a theme, a Dirac skill, a gate renderer.
Nuclei reads the folder, validates the manifest, loads the module, and
hands it a PluginAPI. There is no build step,
no bundler, no npm install, and no remote registry: you point Nuclei at a
folder and it runs.
The Plugins view lives in the activity bar in Research mode. It has a “Load plugin…” and a “Create plugin…” button, an Installed tab, and a Panels tab that renders enabled panels and lists enabled themes.
Anatomy of a plugin
Section titled “Anatomy of a plugin”my-plugin/├── plugin.json # the manifest — validated before anything loads└── entry.js # the ES module — exports activate(api)The manifest’s entry field names the module file ("entry.js" by
convention, but any relative path inside the folder works). Everything the
module needs arrives through api — there is no bare import, because
the module is loaded from a blob URL that has no module resolver.
The manifest
Section titled “The manifest”plugin.json is validated against a zod schema
(src/plugins/manifestSchema.ts) before the entry module is ever read. A
malformed file fails the load with a field-addressed error and registers
nothing.
{ "name": "hello-panel", "version": "1.0.0", "description": "Live panel showing the current circuit's qubit count and depth.", "author": "Nuclei", "entry": "entry.js", "capabilities": ["custom-panel"], "permissions": ["read-circuit"]}| Field | Type | Rule |
|---|---|---|
name | string | Required. Kebab-case: ^[a-z0-9][a-z0-9-]*$ (lowercase letters, digits, hyphens). This is the plugin’s identity — installs are deduplicated by it. |
version | string | Required. Semver x.y.z (^\d+\.\d+\.\d+$). |
description | string | Optional, defaults to "". |
author | string | Optional, defaults to "". |
entry | string | Required. Relative path to the ES module, inside the folder. Absolute paths (/…, C:\…, \\…) and any .. segment are rejected — the entry can’t escape the plugin folder. |
capabilities | array | Required, non-empty. One or more of custom-panel, gate-renderer, kernel-extension, dirac-skill, theme. Gates what the plugin may register (see below). |
permissions | array | Optional, defaults to []. Any of read-circuit, read-results, read-editor, write-editor. Gates what the plugin may read. |
Capabilities
Section titled “Capabilities”A capability must be declared before the matching register… call does
anything — calling api.registerPanel(…) without custom-panel in
capabilities is a silent no-op.
| Capability | Unlocks | Status in v1 |
|---|---|---|
custom-panel | registerPanel | Live. Rendered in the Panels tab. |
theme | registerTheme | Live. Appears in Panels → Themes with Apply / Clear. |
dirac-skill | registerDiracSkill | Stored, not yet injected into Dirac’s prompt (planned for v1.1). |
gate-renderer | registerGateRenderer | Accepted and gated, not yet consumed by the circuit renderer (planned for v1.1). |
kernel-extension | — | Reserved vocabulary; no API method yet. |
Permissions
Section titled “Permissions”Permissions gate the read side of the API. Without the matching permission, the getter returns an empty value and the subscription is a no-op — the plugin still loads and runs.
| Permission | Grants | Denied behavior |
|---|---|---|
read-circuit | getCircuitSnapshot(), onCircuitChange() | getter returns null; subscribe is a no-op |
read-results | getSimulationResult(), onResultChange() | getter returns null; subscribe is a no-op |
read-editor | getEditorCode() | getter returns '' |
write-editor | — | reserved; no write method on the API yet |
The activate(api) entry contract
Section titled “The activate(api) entry contract”The entry module exports an activate function — as a named export or
the default export (named is tried first). Nuclei calls it once with the
plugin’s api. activate may return a cleanup function that runs when the
plugin is disabled, uninstalled, or reloaded.
// entry.js — a single-file, dependency-free ES module.export function activate(api) { api.registerPanel({ id: 'my-panel', title: 'My Panel', render(container) { container.textContent = 'hello'; // Optional: return a cleanup run when the panel unmounts. return () => { /* teardown DOM listeners, timers, etc. */ }; }, });
// Optional: return a cleanup run on disable / uninstall / reload. return () => api.log('deactivated');}The signature is (api: PluginAPI) => (() => void) | void. Everything the
plugin does happens through api; there is no other entry point.
The PluginAPI surface
Section titled “The PluginAPI surface”The full object passed to activate (src/plugins/types.ts):
State access
Section titled “State access”| Method | Requires | Returns |
|---|---|---|
getCircuitSnapshot() | read-circuit | the current CircuitSnapshot, or null |
getSimulationResult() | read-results | the last SimulationResult, or null |
getEditorCode() | read-editor | the active editor buffer’s text, or '' |
getFramework() | — | the active framework id ('qiskit', 'cirq', 'cuda-q', 'qsharp') |
Subscriptions
Section titled “Subscriptions”| Method | Requires | Behavior |
|---|---|---|
onCircuitChange(cb) | read-circuit | calls cb(snapshot) whenever the circuit changes; returns an unsubscribe function |
onResultChange(cb) | read-results | calls cb(result) whenever a new simulation result arrives; returns an unsubscribe function |
Both return an unsubscribe function. If the permission is missing the returned unsubscribe is a harmless no-op, so you can call it unconditionally in your cleanup.
Registration
Section titled “Registration”| Method | Requires | Notes |
|---|---|---|
registerPanel({ id, title, render }) | custom-panel | render(container) mounts imperative DOM into a host element and may return a cleanup run on unmount. |
registerTheme({ name, colors }) | theme | colors is a Record<string, string> of token → CSS color; see theme keys. |
registerDiracSkill({ name, systemPromptFragment, tools? }) | dirac-skill | Stored; not injected into Dirac in v1. |
registerGateRenderer({ gateName, render }) | gate-renderer | render(ctx, x, y, size) draws onto a 2D canvas context. Accepted but not consumed in v1. |
Utility
Section titled “Utility”| Method | Notes |
|---|---|
log(message) | Prefixed console.log — only in dev builds (import.meta.env.DEV). Silent in a packaged app. |
Registering a panel
Section titled “Registering a panel”render receives a fresh HTMLElement to mount into. Build DOM
imperatively, subscribe to state, and return a cleanup:
export function activate(api) { api.registerPanel({ id: 'qubit-counter', title: 'Qubit Counter', render(container) { const el = document.createElement('div'); el.style.cssText = 'padding:12px;font:12px system-ui';
const paint = (snapshot) => { el.textContent = snapshot ? `Qubits: ${snapshot.qubit_count} · Depth: ${snapshot.depth}` : 'No circuit yet — start typing quantum code.'; };
paint(api.getCircuitSnapshot()); const unsubscribe = api.onCircuitChange(paint); container.appendChild(el); return unsubscribe; // runs when the panel unmounts }, });}Each panel is mounted in its own error-isolated host: if your render
throws, Nuclei shows an inline error card for that panel instead of taking
down the app.
Registering a theme
Section titled “Registering a theme”A theme plugin registers a named color set. Nuclei intersects your keys against its own theme tokens — unknown keys are ignored, so you only need to override the tokens you care about. The user applies it from Panels → Themes (Apply / Clear).
export function activate(api) { api.registerTheme({ name: 'Ocean', colors: { bg: '#08131f', bgPanel: '#0b1a2b', bgElevated: '#102438', accent: '#00B4D8', accentLight: '#48cae4', }, });}Recognized token keys: bg, bgPanel, bgEditor, bgElevated, border,
borderStrong, text, textMuted, textDim, accent, accentLight,
dirac, success, warning, error. Values are any valid CSS color.
Scaffolding a plugin from the app
Section titled “Scaffolding a plugin from the app”The fastest way to start is Create plugin… in the Plugins view (Research mode):
- Click Create plugin…, type a display name, and click Choose folder….
- Nuclei slugifies the name to a kebab-case id, creates
`<folder>`/`<slug>`/with aplugin.jsonand a workingentry.js(a live circuit panel), and refuses to overwrite an existing plugin folder. - The
entry.jsopens in the editor so the code is never a black box, and the plugin loads immediately — its panel appears live in the Panels tab and updates as you edit quantum code.
Edit entry.js on disk, then hit Reload on the Installed tab to pick
up your changes — that’s the tight authoring loop.
Loading a local plugin
Section titled “Loading a local plugin”To load a plugin you already have on disk, click Load plugin… and pick its folder. Nuclei validates the manifest, loads the entry module, and activates it. On failure it shows a field-addressed error card and registers nothing.
Installed plugins persist across restarts: Nuclei stores each folder path and its enabled state, and re-loads them on boot (enabled plugins are re-activated; disabled ones are listed manifest-only; a folder that has gone missing or broken becomes an honest error record rather than a silent drop).
On the Installed tab each plugin shows its version, author, capabilities, and a status pill, with Enable / Disable, Reload, and Uninstall actions. Disabling runs the plugin’s cleanup and removes its panels and themes; enabling re-activates it from disk.
Example: hello-panel
Section titled “Example: hello-panel”The repo ships two runnable examples under examples/plugins/. The
smallest, hello-panel, is a custom panel that mirrors the live circuit.
examples/plugins/hello-panel/plugin.json:
{ "name": "hello-panel", "version": "1.0.0", "description": "Live panel showing the current circuit's qubit count and depth.", "author": "Nuclei", "entry": "entry.js", "capabilities": ["custom-panel"], "permissions": ["read-circuit"]}examples/plugins/hello-panel/entry.js:
export function activate(api) { api.registerPanel({ id: 'hello-panel', title: 'Hello Panel', render(container) { const el = document.createElement('div'); el.style.cssText = 'padding:12px;font:12px system-ui,sans-serif;line-height:1.6';
const paint = (snapshot) => { if (!snapshot) { el.textContent = 'No circuit yet — start typing quantum code.'; return; } el.innerHTML = `<strong>Qubits:</strong> ${snapshot.qubit_count}<br>` + `<strong>Depth:</strong> ${snapshot.depth}<br>` + `<strong>Gates:</strong> ${snapshot.gates.length}<br>` + `<strong>Framework:</strong> ${snapshot.framework}`; };
paint(api.getCircuitSnapshot()); const unsubscribe = api.onCircuitChange(paint); container.appendChild(el); return unsubscribe; // cleanup on panel unmount }, });
api.log('hello-panel activated');}To try it: Load plugin… → pick examples/plugins/hello-panel. The
panel appears in the Panels tab and updates on every keystroke. It declares
only read-circuit, so getSimulationResult() would return null — grant
read-results in the manifest if you want to react to executions.
The sibling examples/plugins/ocean-theme exercises the theme path: load
it, then Apply it from Panels → Themes to recolor the UI.
The trust model
Section titled “The trust model”What Nuclei does guarantee is protection from broken plugins, not malicious ones:
- Manifest validation — a bad
plugin.jsonfails the load cleanly. - Path-traversal guard —
entrycan’t point outside the plugin folder. - Per-plugin error isolation — a panel that throws shows an error card;
an
activatethat throws marks the plugin errored and registers nothing, rather than crashing the app.
Because there is no remote registry and plugins load only from a folder you explicitly pick, the security posture is the same as running a local script: only load plugins you wrote or trust. Real isolation (Worker/iframe message passing) is future work — Nuclei would rather be honest about the boundary than ship a fake sandbox.
Other ways to extend Nuclei
Section titled “Other ways to extend Nuclei”Plugins are for UI and read-only integrations that live outside the tree. To add a whole quantum framework or a hardware backend, extend the kernel directly: framework adapters, hardware providers, and contributing.