* feat(plugins): panels talk to their own worker, read storage, settings and events
- plugin.request (host API 1.2): a panel or editor asks its plugin's worker, which answers with
orca.panels.handle(name); the worker pushes with orca.panels.post. Same plugin only, JSON
checked, 1 MiB answers, 256 KiB/200-per-10s posts, a deadline and the panel's lifetime.
- Panels and editors get storage.*, settings.* and secrets.* behind the same capabilities, and
storage/settings onDidChange on every surface; events.subscribe delivers workspace and agent
events to panels that hold the read capability.
- contributes.settings: typed settings (a JSON Schema subset) with defaults and value checks.
- Frames get orca.plugin/storage/settings/secrets; components panels import plugin, storage,
settings, secrets from 'orca', and atom(key, init, { persist: 'storage' }) shares a value with
the worker. Workers get orca.panels/storage/settings/secrets sugar.
- One typed contract: declare module 'orca' { interface PluginRequests / PluginPanelEvents }.
- Desktop push channel per open panel session (plugins:openPanelPush).
* feat(plugins): Orca Settings shows and edits a plugin's contributes.settings
The plugin page lists declared settings with Orca's own controls (switch, picker, text and
number fields, Reset to default). Edits are checked against the declaration in main, written to
the plugin's settings, and reach open panels and the running worker through onDidChange.
* docs(plugins): hello-tsx talks to its worker; authoring guide covers panels, workers and data
The example now uses one of each: plugin.request answered by orca.panels.handle, a worker post
heard with plugin.on, a persist: 'storage' atom the worker watches, two contributes.settings and
agent.status.changed in the panel, all typed by one src/protocol.ts. The build-less TSX e2e
drives the whole loop.
* fix(plugins): typed bridge overloads, panel call budget, settings IPC and catalog keys
- PluginBridgeApi/PanelsApi use overloads: declared names are typed (and complete), any other
name carries JSON; no never-unions for the type-aware lint.
- The per-plugin panel call budget is 120 per 10 s, since a panel loading storage atoms,
settings and its first requests makes a burst; the hostile fixture floods past it.
- Settings IPC builds its host per call; the settings section's strings are in en.json.
* test(plugins): the example's declared settings show under the plugin with the person's value
* refactor(plugins): keep hello-tsx the minimal starter; the bridge demo is hello-worker-panel
hello-tsx goes back to a sandboxed-ui panel with no worker or capabilities. The worker, protocol,
storage atom, settings and events move to examples/plugins/hello-worker-panel, with their own
e2e (plugin-worker-panel) and PR route.
* feat(plugins): test kit covers the panel bridge and plugin data
mockHost keeps the plugin's storage, settings and secrets in memory (seeded with storage,
settings, secrets; read with host.data()) and fakes the worker with requests, so plugin.request,
storage/settings/secrets and persist: 'storage' atoms work under mount(), writes reach
onDidChange, and panel.post(event, payload) is what plugin.on hears. Typed by PluginRequests and
PluginPanelEvents.
* test(plugins): focusInGuest waits for the guest and retries focusing as one step
Under load the custom-editor guest could still be attaching, so executeJavaScript threw
'dom-ready' before the focus poll began. Wait for waitForGuestReady, then retry focusing the
webview and the element together, tolerating a guest that is still attaching or remounts.
* docs(plugins): the authoring skill covers panels, workers and data; persistence leaves the renderer
- skill-guides/orca-plugin-authoring.md gains Panels, workers and data (plugin.request/on,
orca.panels.handle/post, persist: 'storage', contributes.settings, panel events, test kit), and
its generated references read orca-services.d.ts for shell, files, on and the bridge types.
- After the dev-loop rebase, persisted-atom wiring (queue plus storage sync) moves to
createDeclarativePersistence so the renderer stays under max-lines.
* feat(plugins): panels show agents: agents.list, terminal ids on agent events, workspace.changed
- agents.list (agent:read, every surface): the focused workspace's agents with terminalId,
title, agent kind, state and stateStartedAt, from the hook server's store.
- agent.status.changed carries terminalId and title: paneKey mapped through the runtime's
terminal list (tabId:leafId) or the status's terminal handle; looked up only while a plugin
listens, in arrival order.
- workspace.readContext terminals carry title; workspace.changed (workspace:read) fires after
workspace session writes when the focused workspace or its terminals change.
- Event subscriptions are kept per panel session, so one made before the push channel opens
still counts.
- shell.openExternal never prompts on a hidden or minimized window: it settles
{ opened: false, reason: 'consent-unavailable' } and says so in the plugin log, which also
notes 'waiting for the person to confirm' while a prompt is up; the rate budget is spent only
when Orca acts. e2e launches never open a real browser or mail client.
- A worker may export activate as default or by name; docs, typings and the authoring skill
document the entry, the agent events and a live agent status recipe.
* fix(plugins): the plugin page names the workspace.changed subscription
* fix(plugins): workers activate only by default export, and say so when it is named
Orca calls the worker module's default export. A module with a named `activate` and no default
now fails to start with an error naming the fix (`export default function activate(orca)`),
instead of being called by a fallback; docs, typings and the skill say default export.
hello-worker-panel gains a picker setting (suggestionOrder) the worker reads.
12 KiB
Orca plugin host API reference
Host API 1.2 ("pluginApi": 1). Workers call a method with await orca.host.call(name, params); panels and custom editors call the same methods through window.orca.host.call(name, params). Each method lists the surfaces allowed to call it; any other surface gets not_permitted. orca.host.has(name) answers synchronously whether this Orca offers a method on the current surface, and orca.api is { major, minor }. See Plugin authoring for the frame API.
Every call is checked against the capabilities the user approved, and params are validated before Orca runs anything: params are strict, so a field this Orca does not know is refused with invalid_params instead of being dropped. Mutations are recorded in the audit log as plugin:<publisher>.<id>.
Methods whose capability is none need no capability but still need the plugin enabled and approved. shell.openExternal and files.saveAs also need transient user activation (a click or key press in the calling frame, within a few seconds), are rate-limited, and run only in the desktop app; frames also reach them as orca.shell.openExternal(url) and orca.files.saveAs({ suggestedName, mimeType, data }), which resolves with { status: "saved" }, or { status: "cancelled" } when the person cancels the dialog. Saved files carry the OS mark for downloaded files.
Methods
| Method | Capability | Surfaces | Mutation | Since | Summary |
|---|---|---|---|---|---|
workspace.readContext |
workspace:read |
worker, panel, editor | no | 1.0 | Branch, name and terminal ids of the focused workspace, or null when none is focused. |
terminal.sendText |
terminal:send |
worker | yes | 1.0 | Types text into one terminal named by id, optionally pressing Enter. |
notifications.show |
notifications:show |
worker, panel, editor | yes | 1.0 | Shows a desktop notification. |
storage.get |
storage:own |
worker, panel, editor | no | 1.0 | Reads a JSON value from the plugin's private store; null when absent. |
storage.set |
storage:own |
worker, panel, editor | yes | 1.0 | Writes a JSON value to the plugin's private store. |
storage.delete |
storage:own |
worker, panel, editor | yes | 1.0 | Removes a key from the plugin's private store. |
storage.keys |
storage:own |
worker, panel, editor | no | 1.0 | Lists the keys in the plugin's private store. |
secrets.get |
secrets:own |
worker, panel, editor | no | 1.0 | Reads a secret from the plugin's encrypted secrets file; null when absent. |
secrets.set |
secrets:own |
worker, panel, editor | yes | 1.0 | Stores a secret in the plugin's encrypted secrets file. |
secrets.delete |
secrets:own |
worker, panel, editor | yes | 1.0 | Removes a secret from the plugin's encrypted secrets file. |
secrets.protection |
secrets:own |
worker, panel, editor | no | 1.1 | How secrets are protected at rest: os-keyring, or unavailable (no OS key store, or Linux basic_text), where secrets.set fails. |
settings.get |
settings:own |
worker, panel, editor | no | 1.0 | Reads every setting the plugin owns. |
settings.set |
settings:own |
worker, panel, editor | yes | 1.0 | Writes one setting the plugin owns. |
shell.openExternal |
none | panel, editor | no | 1.1 | Opens an http:, https: or mailto: URL in the default browser. Needs a click or key press in the frame; desktop app only. Orca asks the person before an http(s) host the plugin was not allowed; opened is false if they cancel. |
files.saveAs |
none | panel, editor | yes | 1.1 | Offers data (a string or ArrayBuffer, up to 64 MiB) in Orca's save dialog; the plugin never learns the path. Needs a click or key press in the frame; desktop app only. |
plugin.request |
none | panel, editor | no | 1.2 | Asks the plugin's own worker to run the handler it registered with orca.panels.handle(name), starting the worker if needed; resolves with what the handler returned. unknown_method when no handler has that name. |
agents.list |
agent:read |
worker, panel, editor | no | 1.2 | The agents in the focused workspace: their terminal, title, agent kind and status. agent.status.changed reports each change after. |
events.subscribe |
none | panel, editor | no | 1.2 | Delivers a workspace or agent event to this panel as orca.on(event); needs the event's read capability. The frame APIs call it for you. |
workspace.readContext
Branch, name and terminal ids of the focused workspace, or null when none is focused.
(params?: {}) => { branch: string; displayName: string; terminals: { id: string; title?: string }[] } | null
terminal.sendText
Types text into one terminal named by id, optionally pressing Enter.
(params: { terminalId: string; text: string; enter?: boolean }) => { accepted: boolean }
notifications.show
Shows a desktop notification.
(params: { title: string; body?: string }) => { delivered: boolean }
storage.get
Reads a JSON value from the plugin's private store; null when absent.
(params: { key: string }) => { value: JSON }
storage.set
Writes a JSON value to the plugin's private store.
(params: { key: string; value: JSON }) => { ok: true }
storage.delete
Removes a key from the plugin's private store.
(params: { key: string }) => { ok: true }
storage.keys
Lists the keys in the plugin's private store.
(params?: {}) => { keys: string[] }
secrets.get
Reads a secret from the plugin's encrypted secrets file; null when absent.
(params: { key: string }) => { value: string | null }
secrets.set
Stores a secret in the plugin's encrypted secrets file.
(params: { key: string; value: string }) => { ok: true }
secrets.delete
Removes a secret from the plugin's encrypted secrets file.
(params: { key: string }) => { ok: true }
secrets.protection
How secrets are protected at rest: os-keyring, or unavailable (no OS key store, or Linux basic_text), where secrets.set fails.
(params?: {}) => { status: "os-keyring" | "unavailable" }
settings.get
Reads every setting the plugin owns.
(params?: {}) => { settings: Record<string, JSON> }
settings.set
Writes one setting the plugin owns.
(params: { key: string; value: JSON }) => { ok: true }
shell.openExternal
Opens an http:, https: or mailto: URL in the default browser. Needs a click or key press in the frame; desktop app only. Orca asks the person before an http(s) host the plugin was not allowed; opened is false if they cancel.
(params: { url: string }) => { opened: boolean; reason?: "cancelled" | "consent-unavailable" }
files.saveAs
Offers data (a string or ArrayBuffer, up to 64 MiB) in Orca's save dialog; the plugin never learns the path. Needs a click or key press in the frame; desktop app only.
(params: { suggestedName?: string; mimeType?: string; data: string | ArrayBuffer }) => { status: "saved" | "cancelled" }
plugin.request
Asks the plugin's own worker to run the handler it registered with orca.panels.handle(name), starting the worker if needed; resolves with what the handler returned. unknown_method when no handler has that name.
(params: { name: string; input?: JSON; timeoutMs?: number }) => { output: JSON }
agents.list
The agents in the focused workspace: their terminal, title, agent kind and status. agent.status.changed reports each change after.
(params?: {}) => { agents: { terminalId: string | null; title: string | null; agent: string | null; state: "working" | "blocked" | "waiting" | "done"; stateStartedAt: number }[] }
events.subscribe
Delivers a workspace or agent event to this panel as orca.on(event); needs the event's read capability. The frame APIs call it for you.
(params: { event: "workspace.created" | "workspace.removed" | "agent.status.changed" | "workspace.changed" }) => { ok: true }
Events
A worker receives the events its manifest lists in contributes.events through orca.events.on(event, handler), which returns { dispose() }. Each needs main, the trusted-worker profile and its domain's capability (workspace:read or agent:read). orca plugin types writes the payloads as PluginEvents in orca/host.
| Event | Payload | Summary |
|---|---|---|
workspace.created |
{ workspaceId: string; kind: "worktree" | "folder"; path: string; branch: string } |
A worktree workspace was created. |
workspace.removed |
{ workspaceId: string; kind: "worktree" | "folder"; path: string } |
A worktree workspace was removed. |
agent.status.changed |
{ workspaceId: string | null; paneKey: string; terminalId?: string; title?: string; state: "working" | "blocked" | "waiting" | "done"; receivedAt: number; mainAgent?: { state: "working" | "blocked" | "waiting" | "done"; outcome?: "success" | "failure" | "cancellation" | "superseded" | "interruption" | "unconfirmed"; stateStartedAt: number } } |
An agent in a terminal pane changed status; workspaceId is null when Orca cannot tie the pane to a workspace, and terminalId names its terminal when Orca lists it. |
workspace.changed |
{ context: { branch: string; displayName: string; terminals: { id: string; title?: string }[] } | null } |
The focused workspace changed, or its terminals were opened, closed or renamed; context is what workspace.readContext now answers. |
Errors
A failed call rejects with an Error carrying code, message, retryable and, for rate_limited, retryAfterMs. Treat a code not listed here as action_failed:
invalid_request: the call itself is malformed.invalid_params: params do not match the method, including a field this Orca does not know; the message names it.unknown_method: the method does not exist in this Orca.capability_denied: the manifest does not declare the capability.consent_required: the plugin is disabled or the user has not approved this version.not_permitted: the method or document is not available here, e.g. a worker-only method from a panel.rate_limited: calls arrive faster than the budget;retryAfterMssays when to try again.unavailable: the service behind the method is not running yet.action_failed: the method ran and failed.cancelled: the caller aborted the call through itssignal.timeout: Orca gave up waiting.conflict: the call was made against a stale version, e.g. an editor edit after an outside change.too_large: the message is larger than Orca accepts.
Stability and deprecation
Plugins are in Alpha: the manifest v2 format and catalog format are stable contracts; items listed as experimental in Plugin authoring may change.
- Host API 1 is frozen. Within
pluginApi1, methods, params and results only grow: a new method or a new optional param arrives with a higherSinceminor, and nothing listed here is removed, renamed or narrowed. - Deprecation. A method to be retired is marked deprecated in this table and keeps working for the rest of the major.
- New majors. A breaking change ships as a new
pluginApimajor beside the old one. Orca refuses a manifest whosepluginApinames a major it does not implement, with "update Orca", instead of loading it against the wrong API. - Detect, do not assume. Check
orca.host.has(name)ororca.api.minorbefore calling a method newer than the oldest Orca you support; a call that slips through fails withunknown_method.