* feat(plugins): add permissioned desktop appearance publication * docs(plugins): add one-line API comments for appearance provider * chore(plugins): regenerate declarative testing kit for appearance API --------- Co-authored-by: Matthew Wilkinson <wilkinson.m@outlook.com>
20 KiB
Host API by surface
Host API 1.4 ("pluginApi": 1). Call host.call(method, params, { signal }); a surface not listed for a method gets not_permitted. Params are strict: a field this Orca does not know is refused with invalid_params, so check host.has(method) or api.minor before using a method newer than your engines.orca floor. Each call also needs the manifest capability and the user's approval.
Methods per surface
- Worker (
trusted-worker,main):orcapassed toactivate(orca):workspace.readContext,terminal.sendText,notifications.show,storage.get,storage.set,storage.delete,storage.keys,secrets.get,secrets.set,secrets.delete,secrets.protection,settings.get,settings.set,agents.list,statusBar.update,appearance.read,appearance.apply,workspace.list,workspace.open,terminal.create,terminal.read,agent.launch,editor.open,commands.run - Panel: global
orcain an HTML panel;hostimported fromorcain a components panel:workspace.readContext,notifications.show,storage.get,storage.set,storage.delete,storage.keys,secrets.get,secrets.set,secrets.delete,secrets.protection,settings.get,settings.set,shell.openExternal,files.saveAs,plugin.request,agents.list,events.subscribe - Custom editor: global
orcain the editor frame:workspace.readContext,notifications.show,storage.get,storage.set,storage.delete,storage.keys,secrets.get,secrets.set,secrets.delete,secrets.protection,settings.get,settings.set,shell.openExternal,files.saveAs,plugin.request,agents.list,events.subscribe
Worker API (import type { OrcaWorkerApi } from 'orca/worker')
api: HostApiVersioncommands.register(commandId: string, handler: (args: unknown, context: CommandContext) => unknown): A string result is shown as-is in Orca; anything else as JSON.events.on(event: E, handler: (payload: PluginEvents[E]) => void | Promise<void>): Only events listed in the manifest'scontributes.eventsarrive.hooks.on(hook: H, handler: HookHandler<H>): Only hooks listed incontributes.hooksrun. Returnnext(event)to go on (with the fields the hook allows changed), a result such as{ deny: reason }to stop, or nothing to pass. Plugins run in turn, then Orca acts; past 2 s, or on a throw, Orca carries on as if this plugin passed and logs it.host: HostApi<'worker'>panels: PanelsApi: This plugin's panels and custom editors.storage: StorageApi: Same store as panels'storage(storage:own);onDidChangehears their writes too.settings: SettingsApi:settings:own;onDidChangealso hears the person changing them in Settings.secrets: SecretsApi:secrets:own.grantedCapabilities: readonly string[]: Consented capability kinds; informational, the host re-checks every call.log(message: string): Lands inorca plugin logs.
Frame API (global orca, also window.orca, in HTML panels and custom editors)
api: HostApiVersionhost: HostApi<'panel' | 'editor'>on(event: E, handler: (payload: FrameEvents[E]) => unknown)locale: string: Orca's UI language as a BCP-47 tag.plugin: PluginBridgeApi: Your plugin's own worker:requestits handlers, hear what itposts.storage: StorageApi: The plugin's own storage, the same its worker uses (storage:own).settings: SettingsApi: The plugin's settings, declared ones with their defaults (settings:own).secrets: SecretsApi: The plugin's encrypted secrets (secrets:own).shell.openExternal(url: string):reason: the person cancelled, or Orca's window was hidden so nobody could confirm.files.saveAs(options: SaveAsOptions): Desktop isolated frames only;cancelledwhen the person closes the dialog.document?: EditorDocumentApi: Custom editors only. Orca keeps no undo history for them: Cmd/Ctrl+Z reaches the frame.document.get(): The latest document, with your accepted edits' content at the same version.document.onDidChange(handler: (document: EditorDocument) => void): Replays the current document, then each one Orca sends when the file changes outside.document.applyEdit(edit: EditorEdit): Rejects withconflict,too_largeornot_permitted(read-only).document.session(options?: EditorSessionOptions<T>): Document sync in one call; resolves once theme, locale and the first document are in.
Theme variables in frames
HTML panels and custom editors get these CSS variables from Orca's active theme, plus a light or dark class on <html>. A theme change updates them in place and emits theme (orca.on('theme', ...)); the frame is not reloaded. Style from them, not fixed colours.
--orca-background: App canvas--orca-foreground: Body text--orca-card: Cards and panels--orca-card-foreground: Text on--orca-card--orca-popover: Menus, popovers and dialogs--orca-popover-foreground: Text on--orca-popover--orca-primary: Primary buttons--orca-primary-foreground: Text on--orca-primary--orca-secondary: Secondary buttons--orca-secondary-foreground: Text on--orca-secondary--orca-muted: Quiet fills--orca-muted-foreground: Secondary text--orca-accent: Hover and selected fills--orca-accent-foreground: Text on--orca-accent--orca-destructive--orca-destructive-foreground--orca-border: Borders and separators--orca-input: Input borders--orca-ring: Focus rings--orca-radius: Corner radius of cards and controls
Methods
workspace.readContext(capabilityworkspace:read; worker, panel, editor; since 1.0): 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 }[] } | nullterminal.sendText(capabilityterminal:send; worker; since 1.0): Types text into one terminal named by id, optionally pressing Enter.(params: { terminalId: string; text: string; enter?: boolean }) => { accepted: boolean }notifications.show(capabilitynotifications:show; worker, panel, editor; since 1.0): Shows a desktop notification.(params: { title: string; body?: string }) => { delivered: boolean }storage.get(capabilitystorage:own; worker, panel, editor; since 1.0): Reads a JSON value from the plugin's private store; null when absent.(params: { key: string }) => { value: JSON }storage.set(capabilitystorage:own; worker, panel, editor; since 1.0): Writes a JSON value to the plugin's private store.(params: { key: string; value: JSON }) => { ok: true }storage.delete(capabilitystorage:own; worker, panel, editor; since 1.0): Removes a key from the plugin's private store.(params: { key: string }) => { ok: true }storage.keys(capabilitystorage:own; worker, panel, editor; since 1.0): Lists the keys in the plugin's private store.(params?: {}) => { keys: string[] }secrets.get(capabilitysecrets:own; worker, panel, editor; since 1.0): Reads a secret from the plugin's encrypted secrets file; null when absent.(params: { key: string }) => { value: string | null }secrets.set(capabilitysecrets:own; worker, panel, editor; since 1.0): Stores a secret in the plugin's encrypted secrets file.(params: { key: string; value: string }) => { ok: true }secrets.delete(capabilitysecrets:own; worker, panel, editor; since 1.0): Removes a secret from the plugin's encrypted secrets file.(params: { key: string }) => { ok: true }secrets.protection(capabilitysecrets:own; worker, panel, editor; since 1.1): How secrets are protected at rest:os-keyring, orunavailable(no OS key store, or Linuxbasic_text), wheresecrets.setfails.(params?: {}) => { status: "os-keyring" | "unavailable" }settings.get(capabilitysettings:own; worker, panel, editor; since 1.0): Reads every setting the plugin owns.(params?: {}) => { settings: Record<string, JSON> }settings.set(capabilitysettings:own; worker, panel, editor; since 1.0): Writes one setting the plugin owns.(params: { key: string; value: JSON }) => { ok: true }shell.openExternal(no capability; panel, editor; since 1.1; needs a user gesture): 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;openedis false if they cancel.(params: { url: string }) => { opened: boolean; reason?: "cancelled" | "consent-unavailable" }files.saveAs(no capability; panel, editor; since 1.1; needs a user gesture): 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(no capability; panel, editor; since 1.2): Asks the plugin's own worker to run the handler it registered withorca.panels.handle(name), starting the worker if needed; resolves with what the handler returned.unknown_methodwhen no handler has that name.(params: { name: string; input?: JSON; timeoutMs?: number }) => { output: JSON }agents.list(capabilityagent:read; worker, panel, editor; since 1.2): The agents in the focused workspace: their terminal, title, agent kind and status.agent.status.changedreports each change after.(params?: {}) => { agents: { terminalId: string | null; title: string | null; agent: string | null; state: "working" | "blocked" | "waiting" | "done"; stateStartedAt: number }[] }events.subscribe(no capability; panel, editor; since 1.2): Delivers a workspace or agent event to this panel asorca.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 }statusBar.update(no capability; worker; since 1.2): Changes one of the plugin'scontributes.statusBarItems(named byid): fields given replace their current value,visible: falsehides it. Desktop app only; lasts until Orca restarts, and is hidden while the plugin is disabled. A plugin withmainand status bar items has its worker started when it is enabled and kept running while it stays enabled, so a worker can poll and update its items without an event or a hook.(params: { id: string; text?: string; icon?: "activity" | "bar-chart-3" | "bell" | "blocks" | "book" | "bot" | "bug" | "calendar" | "cloud" | "code" | "database" | "file-text" | "flag" | "folder" | "gauge" | "globe" | "hammer" | "layers" | "lightbulb" | "package" | "plug" | "puzzle" | "rocket" | "star" | "terminal" | "wrench" | "zap" | "add" | "remove" | "delete" | "edit" | "refresh" | "search" | "settings" | "check" | "close" | "info" | "warning" | "error" | "open-external" | "copy" | "download" | "upload" | "play" | "pause" | "stop" | "more" | "expand" | "collapse" | "next" | "filter" | "branch" | "file" | "agent" | "link"; tone?: "neutral" | "accent" | "success" | "warning" | "danger"; tooltip?: string; visible?: boolean }) => { ok: true }appearance.read(capabilityappearance:write; worker; since 1.4): Reads the current desktop appearance revision for a conditional update.(params?: {}) => { revision: string }appearance.apply(capabilityappearance:write; worker; since 1.4): Publishes owned app/editor and terminal colors and selects them in the desktop. Returns publication, not renderer acknowledgement.(params: { expectedRevision: string; app: string; terminal: string }) => { revision: string; themeId: string; mode: "dark" | "light"; delivery: "published" }workspace.list(capabilityworkspace:read; worker; since 1.3): Lists every workspace, Git worktrees and folders, on every host Orca reaches.(params?: {}) => { workspaces: { id: string; kind: "worktree" | "folder"; name: string; path: string; branch: string | null; hostId: string | null }[]; truncated: boolean }workspace.open(capabilityworkspace:write; worker; since 1.3): Shows a workspace in Orca, as clicking it in the sidebar does; a folder workspace needs the desktop app.(params: { workspaceId: string }) => { opened: true }terminal.create(capabilityterminal:send; worker; since 1.3): Opens a terminal in a workspace, optionally running a command, in the background.(params: { workspaceId: string; title?: string; command?: string }) => { terminalId: string }terminal.read(capabilityterminal:read; worker; since 1.3): Reads a terminal's newest output as plain text, at most 2000 lines.(params: { terminalId: string; maxLines?: number }) => { lines: string[]; truncated: boolean }agent.launch(capabilityagent:launch; worker; since 1.3): Starts an agent's CLI in a new terminal of a workspace, in the background, and returns as soon as it starts; apromptis sent once the agent is ready.(params: { workspaceId: string; agent: string; prompt?: string }) => { terminalId: string }editor.open(capabilityworkspace:write; worker; since 1.3): Opens a file of a workspace in an editor tab of Orca's window; desktop app only.workspace.openshows the workspace.(params: { workspaceId: string; path: string }) => { opened: true }commands.run(capabilityworkspace:write; worker; since 1.3): Runs one of Orca's built-in actions in its window, never a plugin's replacement of it; desktop app only.(params: { action: "worktree.history.back" | "worktree.history.forward" | "sidebar.left.toggle" | "sidebar.sleepingWorkspaces.toggle" | "floatingWorkspace.maximize" | "tab.rename" | "workspace.rename" | "workspace.openBoard" | "view.tasks" | "sidebar.right.toggle" | "sidebar.explorer.toggle" | "sidebar.search.toggle" | "sidebar.sourceControl.toggle" | "sidebar.checks.toggle" | "sidebar.ports.toggle" }) => { ran: true }
Hooks
List each in contributes.hooks ({ "event", "onError"? }; main and trusted-worker) and handle it with orca.hooks.on(event, (event, next) => ...). Return next(event) (or nothing) to go on, or the stop result. Plugins run in turn, 2 s each; a throw, timeout or crash passes unless the entry says "onError": "deny". Decide first: an awaited call before you return (a notification, say) can fail or time out and turn your deny into a pass.
terminal.send(since 1.4.248): Text Orca, the CLI or a plugin is about to send to a terminal. A person typing (on the desktop, a phone or a paired client) is never hooked, and prompts Orca submits to an agent takeagent.prompt.beforeinstead. Writes to one terminal stay in order while a hook decides. Rewritetextorenterwithnext, or return{ deny }to refuse the send. event{ terminalId: string; text: string; enter: boolean; inputKind: "driving" | "orchestration" | "launch" };nextmay changetext,enter; stop with{ deny: string }workspace.create.before(since 1.4.248): A worktree or folder workspace is about to be created. Changename,branchorbasewithnext, or return{ deny }to refuse it. event{ kind: "worktree" | "folder"; projectId: string; name: string; branch?: string; base?: string };nextmay changename,branch,base; stop with{ deny: string }workspace.create.after(since 1.4.248): A worktree or folder workspace was created. Observe only: Orca does not wait for it, and results are ignored. event{ workspaceId: string; kind: "worktree" | "folder"; path: string; branch: string };nextmay change nothing; stop with nothing (observe only)agent.launch.before(since 1.4.248): Orca is about to start an agent CLI in a terminal, from any launcher (new tab, workspace create, CLI, automation, orchestration). Changecommandor addenvwithnext, or return{ deny }to refuse the launch. Restores of saved sessions are not hooked. event{ agent: string; workspaceId: string | null; workspaceKind: "worktree" | "folder" | null; terminalId?: string; command: string; env: Record<string, string> };nextmay changecommand,env; stop with{ deny: string }agent.prompt.before(since 1.4.248): Orca is about to submit a prompt to an agent in a terminal: a launch prompt, an orchestration dispatch, an issue or task prompt, ororca terminal sendto an agent. A prompt a person submits is never hooked, and writes to one terminal stay in order while it decides. Rewritepromptwithnext, or return{ deny }to refuse it. event{ agent: string | null; terminalId: string; prompt: string; inputKind: "driving" | "orchestration" | "launch" };nextmay changeprompt; stop with{ deny: string }agent.tool.before(since 1.4.248): An agent in an Orca terminal is about to run a tool. Claude Code waits for the answer: return{ deny }to refuse the call (the agent sees the reason) or changeinputwithnext; the person is still asked as usual. Other agents only report the call, so their results are ignored. Agents on SSH or WSL hosts are not hooked yet. event{ agent: string; workspaceId: string | null; terminalId?: string; tool: string; input: JSON; callId?: string };nextmay changeinput; stop with{ deny: string }agent.tool.after(since 1.4.248): An agent in an Orca terminal finished a tool call (successfully or not). Observe only: Orca does not wait for it. event{ agent: string; workspaceId: string | null; terminalId?: string; tool: string; input: JSON; callId?: string; output?: JSON };nextmay change nothing; stop with nothing (observe only)editor.save.before(since 1.4.248): The person is about to save a text file from an Orca editor tab (an explicit save, not autosave). Changetextwithnext(format on save) or return{ deny }to refuse the save, which shows your reason. Edits made while the hook ran stay unsaved. event{ workspaceId: string | null; path: string; languageId?: string; text: string };nextmay changetext; stop with{ deny: string }
Capabilities
workspace:read(since 1.4.160): Read the name, branch, and terminal list of your focused workspace, and get notified when workspaces are created or removedworkspace:write(since 1.4.248): Open your workspaces and their files in Orca, and run its built-in commandsagent:read(since 1.4.245): Get notified when agent status changesagent:launch(since 1.4.248): Start agents in your workspaces, local and SSHterminal:send(since 1.4.160): Type and run commands in your terminals, local and SSHterminal:read(since 1.4.248): Read the output of your terminals, local and SSHnotifications:show(since 1.4.160): Show desktop notifications labeled with the plugin namestorage:own(since 1.4.245): Store data in the plugin's own storage foldersecrets:own(since 1.4.245): Store and read secrets in the plugin's own encrypted filesettings:own(since 1.4.160): Read and change the plugin's own settingsappearance:write(since 1.4.249): Publish and select desktop application, editor and terminal colors
Errors
A failed call rejects with an Error carrying code, message, retryable and, for rate_limited, retryAfterMs. Treat an unknown code as action_failed.
invalid_request: the call itself is malformedinvalid_params: params do not match the method, including a field this Orca does not know; the message names itunknown_method: the method does not exist in this Orcacapability_denied: the manifest does not declare the capabilityconsent_required: the plugin is disabled or the user has not approved this versionnot_permitted: the method or document is not available here, e.g. a worker-only method from a panelrate_limited: calls arrive faster than the budget;retryAfterMssays when to try againunavailable: the service behind the method is not running yetaction_failed: the method ran and failedcancelled: the caller aborted the call through itssignaltimeout: Orca gave up waitingconflict: the call was made against a stale version, e.g. an editor edit after an outside changetoo_large: the message is larger than Orca accepts