Files
orca/resources/plugins/declarative/orca-runtime.mjs
T
Neil ec0d3cedd1 Plugin panels talk to their worker; panel storage, settings and agent events (#37013)
* 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.
2026-11-01 04:40:17 -08:00

1261 lines
38 KiB
JavaScript

// Generated by config/scripts/build-plugin-declarative-runtime.mjs from
// src/shared/plugins/declarative/runtime/. Do not edit.
// src/shared/plugins/declarative/runtime/declarative-atoms.ts
var ATOM = /* @__PURE__ */ Symbol.for("orca.declarative.atom");
var activeTracker = null;
var ATOM_KEY = /^[A-Za-z0-9_.:-]{1,128}$/;
var keyed = /* @__PURE__ */ new Map();
var snapshot = {
memory: /* @__PURE__ */ new Map(),
plugin: /* @__PURE__ */ new Map(),
workspace: /* @__PURE__ */ new Map()
};
var onPersistedChange = null;
var onMemoryChange = null;
var onStorageAtom = null;
var writableAtoms = /* @__PURE__ */ new Set();
function create(initial, fields = {}) {
const created = Object.freeze({
[ATOM]: {
value: initial,
initial,
listeners: /* @__PURE__ */ new Set(),
readonly: false,
persist: "memory",
version: 1,
...fields
}
});
if (!created[ATOM].readonly) {
writableAtoms.add(created[ATOM]);
}
return created;
}
function resetAtoms() {
snapshot.plugin = /* @__PURE__ */ new Map();
snapshot.workspace = /* @__PURE__ */ new Map();
for (const inner of writableAtoms) {
inner.value = inner.initial;
}
}
function stored(inner) {
if (!inner.key || inner.persist === "storage") {
return { found: false };
}
const entry = snapshot[inner.persist].get(inner.key);
if (!entry) {
return { found: false };
}
if (entry.version === inner.version) {
return { found: true, value: entry.value };
}
return inner.migrate && inner.persist !== "memory" ? { found: true, value: inner.migrate(entry.value, entry.version) } : { found: false };
}
function atom(...args) {
if (args.length === 1) {
return create(args[0]);
}
const [key, initial, options = {}] = args;
if (typeof key !== "string" || !ATOM_KEY.test(key)) {
throw new TypeError("an atom key is 1-128 letters, digits, or . _ : -");
}
if (keyed.has(key)) {
throw new Error(`atom key "${key}" is already used`);
}
const persist = options.persist ?? "memory";
if (!["memory", "plugin", "workspace", "storage"].includes(persist)) {
throw new TypeError('persist is "memory", "plugin", "workspace" or "storage"');
}
const result = create(initial, {
key,
persist,
version: options.version ?? 1,
migrate: options.migrate
});
const inner = result[ATOM];
const hydrated = stored(inner);
if (hydrated.found) {
inner.value = hydrated.value;
}
keyed.set(key, result);
if (persist === "storage") {
onStorageAtom?.(key);
}
return result;
}
function derived(compute) {
if (typeof compute !== "function") {
throw new TypeError("derived() takes a function");
}
return create(compute(), { readonly: true, compute });
}
function hostAtom(initial) {
const result = create(initial, { readonly: true });
return { atom: result, set: (value) => assign(result[ATOM], value, false) };
}
function state(target) {
const inner = typeof target === "object" && target !== null ? target[ATOM] : void 0;
if (!inner) {
throw new TypeError("expected an atom created by atom()");
}
return inner;
}
function read(target) {
const inner = state(target);
if (inner.compute) {
return inner.compute();
}
activeTracker?.(target);
return inner.value;
}
function assign(inner, value, persist) {
if (Object.is(value, inner.value)) {
return;
}
inner.value = value;
for (const listener of inner.listeners) {
listener();
}
const target = persist && inner.key ? keyed.get(inner.key) : void 0;
const notify = inner.persist === "memory" ? onMemoryChange : onPersistedChange;
if (target) {
notify?.(target);
}
}
function writable(target, verb) {
if (activeTracker) {
throw new Error(`${verb}() was called while rendering; call it from an event handler instead`);
}
const inner = state(target);
if (inner.readonly) {
throw new TypeError(`${verb}() cannot change a read-only atom`);
}
return inner;
}
function set(target, value) {
assign(writable(target, "set"), value, true);
}
function update(target, updater) {
const inner = writable(target, "update");
if (typeof updater !== "function") {
throw new TypeError("update() takes an updater function; use set() to store a value");
}
assign(inner, updater(inner.value), true);
}
function subscribeAtom(target, listener) {
const inner = state(target);
inner.listeners.add(listener);
return () => inner.listeners.delete(listener);
}
function trackAtomReads(reads, render) {
const previous = activeTracker;
activeTracker = (target) => reads.add(target);
try {
return render();
} finally {
activeTracker = previous;
}
}
function persistedEntry(target) {
const inner = state(target);
return inner.key && inner.persist !== "memory" ? { scope: inner.persist, key: inner.key, version: inner.version, value: inner.value } : null;
}
function watchPersistedAtoms(listener) {
onPersistedChange = listener;
}
function memoryEntry(target) {
const inner = state(target);
return inner.key && inner.persist === "memory" ? { key: inner.key, version: inner.version, value: inner.value } : null;
}
function watchMemoryAtoms(listener) {
onMemoryChange = listener;
}
function watchStorageAtoms(listener) {
onStorageAtom = listener;
for (const [key, target] of keyed) {
if (listener && target[ATOM].persist === "storage") {
listener(key);
}
}
}
function applyStorageValue(key, value) {
const inner = keyed.get(key)?.[ATOM];
if (inner?.persist === "storage") {
assign(inner, value === void 0 ? inner.initial : value, false);
}
}
function hydrateAtoms(scope, values) {
snapshot[scope] = new Map(values);
for (const target of keyed.values()) {
const inner = target[ATOM];
if (inner.persist !== scope) {
continue;
}
const hydrated = stored(inner);
if (hydrated.found || scope !== "memory") {
assign(inner, hydrated.found ? hydrated.value : inner.initial, false);
}
}
}
// src/shared/plugins/declarative/plugin-declarative-message-names.ts
var PLUGIN_DECLARATIVE_MESSAGES = {
/** guest → host, `{ document, revision, tree }` */
render: "ui.render",
/** host → guest, `{ document, revision, callback, args, edit? }`: `revision` is the tree shown. */
event: "ui.event",
/** guest → host, `{ document, revision, message }`: the plugin threw; the last good tree stays. */
error: "ui.error",
/** host → guest, `{ document, revision, accepted, retryAfterMs? }`: whether a render is shown. */
ack: "ui.ack",
/** guest → host, `{ document }`: the runtime is up and waits for `ui.hydrate` to render. */
ready: "ui.ready",
/**
* host → guest, `{ panel?, visible?, plugin?, workspace?, memory?, retain? }`: persisted state and
* context; `memory` hands keyed memory atoms to a dev hot reload, `retain` asks for `ui.retain`.
*/
hydrate: "ui.hydrate",
/** host → guest, `{ visible }` */
visibility: "ui.visibility",
/** guest → host, `{ entries: [{ scope, key, version, value }] }`: persisted atoms changed. */
persist: "ui.persist",
/** guest → host, `{ document, entries: [{ key, version, value? }] }`: keyed memory atoms changed. */
retain: "ui.retain"
};
var PLUGIN_DECLARATIVE_CATALOG_VERSION = 1;
var PLUGIN_DECLARATIVE_COMPONENT_PROPS = {
Stack: ["gap", "align"],
Row: ["gap", "align", "justify", "wrap"],
Text: ["tone", "muted", "size", "weight", "truncate"],
Heading: ["level"],
Button: ["variant", "tone", "size", "icon", "disabled", "onPress"],
IconButton: ["icon", "label", "variant", "tone", "size", "disabled", "onPress"],
Badge: ["tone"],
Card: ["title", "description"],
List: ["label"],
ListItem: ["title", "description", "icon", "current", "disabled", "onPress"],
Input: [
"label",
"labelHidden",
"value",
"defaultValue",
"placeholder",
"type",
"maxLength",
"invalid",
"disabled",
"onChange",
"onSubmit"
],
Switch: ["label", "checked", "defaultChecked", "disabled", "onChange"],
Select: [
"label",
"labelHidden",
"value",
"defaultValue",
"placeholder",
"options",
"disabled",
"onChange"
],
Separator: ["orientation"],
Progress: ["label", "labelHidden", "value"],
Spinner: ["label", "labelHidden"],
EmptyState: ["title", "description", "icon"],
Link: ["href", "onPress"],
Code: ["block"]
};
var PLUGIN_DECLARATIVE_VALUE_CONTROLS = [
"Input",
"Switch",
"Select"
];
var PLUGIN_DECLARATIVE_EDIT_PROP = "$edit";
function isReservedPluginDeclarativeEvent(name) {
return name.startsWith("ui.") || name.startsWith("editor.") || name === "ready" || name === "hello" || name === "ping" || name === "pong";
}
// src/shared/plugins/declarative/runtime/declarative-elements.ts
var ORCA_ELEMENT = /* @__PURE__ */ Symbol.for("orca.declarative.element");
var Fragment = /* @__PURE__ */ Symbol.for("orca.declarative.fragment");
function isOrcaElement(value) {
return typeof value === "object" && value !== null && "$$typeof" in value && value.$$typeof === ORCA_ELEMENT;
}
function createElement(type, props, key) {
return Object.freeze({
$$typeof: ORCA_ELEMENT,
type,
props,
key: key === void 0 || key === null ? void 0 : String(key)
});
}
function jsx(type, props, key) {
const { key: propKey, ...rest } = props ?? {};
return createElement(type, rest, key ?? propKey);
}
function h(type, props, ...children) {
const { key, ...rest } = props ?? {};
if (children.length > 0) {
rest.children = children.length === 1 ? children[0] : children;
}
return createElement(type, rest, key);
}
// src/shared/plugins/declarative/runtime/declarative-evaluate.ts
var MAX_EVALUATION_DEPTH = 512;
var MAX_PROP_DEPTH = 16;
var IGNORED_PROPS = /* @__PURE__ */ new Set([
"children",
"ref",
"key",
PLUGIN_DECLARATIVE_EDIT_PROP
]);
function isCallback(value) {
return typeof value === "function";
}
function describe(value) {
if (value === null || typeof value !== "object") {
return typeof value === "number" ? String(value) : typeof value;
}
return Object.getPrototypeOf(value)?.constructor?.name ?? "object";
}
function assertPlainJson(value, where, depth = 0) {
if (depth > MAX_PROP_DEPTH) {
throw new TypeError(`${where} is nested too deeply`);
}
if (value === null || typeof value === "string" || typeof value === "boolean") {
return;
}
if (typeof value === "number" && Number.isFinite(value)) {
return;
}
if (Array.isArray(value)) {
value.forEach((item, index) => assertPlainJson(item, `${where}[${index}]`, depth + 1));
return;
}
if (typeof value === "object") {
const prototype = Object.getPrototypeOf(value);
if (prototype === Object.prototype || prototype === null) {
for (const [name, item] of Object.entries(value)) {
assertPlainJson(item, `${where}.${name}`, depth + 1);
}
return;
}
}
throw new TypeError(`${where} must be plain JSON data, not ${describe(value)}`);
}
function assertUniqueKeys(children, parent) {
const seen = /* @__PURE__ */ new Set();
for (const child of children) {
if (typeof child !== "string") {
if (seen.has(child.key)) {
throw new Error(`duplicate key "${child.key.slice(0, 64)}" among the children of ${parent}`);
}
seen.add(child.key);
}
}
}
function hashId(text) {
let a = 2166136261;
let b = 16777619;
for (let index = 0; index < text.length; index++) {
const code = text.charCodeAt(index);
a = Math.imul(a ^ code, 16777619);
b = Math.imul(b ^ code, 1540483477);
}
return `c${(a >>> 0).toString(36)}${(b >>> 0).toString(36)}`;
}
function evaluateOrcaTree(component, editsSeen = /* @__PURE__ */ new Map()) {
const valueControls = new Set(PLUGIN_DECLARATIVE_VALUE_CONTROLS);
const callbacks = /* @__PURE__ */ new Map();
function callbackId(path, prop) {
const base = hashId(`${path}|${prop}`);
let id = base;
for (let attempt = 1; callbacks.has(id); attempt++) {
id = `${base}.${attempt}`;
}
return id;
}
function hostNode(type, props, key, parentPath, depth) {
if (!/^[A-Z]/.test(type)) {
throw new TypeError(
`<${type.slice(0, 64)}> is not an Orca component; panels never render DOM`
);
}
const path = `${parentPath}/${key}`;
const wireProps = {};
for (const [name, value] of Object.entries(props)) {
if (IGNORED_PROPS.has(name) || value === void 0) {
continue;
}
if (isCallback(value)) {
const id = callbackId(path, name);
callbacks.set(id, value);
wireProps[name] = { $callback: id };
} else {
assertPlainJson(value, `<${type}> prop "${name}"`);
wireProps[name] = value;
}
}
const onChange = wireProps.onChange;
if (valueControls.has(type) && typeof onChange === "object" && onChange !== null) {
const id = "$callback" in onChange ? onChange.$callback : void 0;
wireProps[PLUGIN_DECLARATIVE_EDIT_PROP] = typeof id === "string" ? editsSeen.get(id) ?? 0 : 0;
}
const children = [];
const list = Array.isArray(props.children) ? props.children : [props.children];
list.forEach((child, index) => emit(child, `#${index}`, "", children, path, depth + 1));
assertUniqueKeys(children, `<${type}>`);
return { type, key, props: wireProps, children };
}
function emit(value, slot, scope, out, parentPath, depth) {
if (depth > MAX_EVALUATION_DEPTH) {
throw new Error(`component tree is deeper than ${MAX_EVALUATION_DEPTH}`);
}
if (value === null || value === void 0 || typeof value === "boolean") {
return;
}
if (typeof value === "string" || typeof value === "number") {
out.push(String(value));
return;
}
if (Array.isArray(value)) {
value.forEach(
(item, index) => emit(item, `${slot}.${index}`, `${slot}.`, out, parentPath, depth + 1)
);
return;
}
if (!isOrcaElement(value)) {
throw new TypeError(`a ${describe(value)} is not a valid child; render a component or text`);
}
const { type, props } = value;
const key = value.key === void 0 ? slot : `${scope}${value.key}`;
if (type === Fragment) {
emit(props.children, key, `${key}.`, out, parentPath, depth + 1);
} else if (typeof type === "function") {
emit(type(props), key, `${key}:`, out, parentPath, depth + 1);
} else if (typeof type === "string") {
out.push(hostNode(type, props, key, parentPath, depth));
} else {
throw new TypeError('invalid element type; import components from "orca"');
}
}
const root = [];
emit(component({}), "#0", "", root, "", 0);
const nodes = root.map(
(child, index) => typeof child === "string" ? { type: "Text", key: `#text${index}`, props: {}, children: [child] } : child
);
assertUniqueKeys(nodes, "the panel");
const tree = nodes.length <= 1 ? nodes[0] ?? null : { type: "Stack", key: "#root", props: {}, children: nodes };
return { tree, callbacks };
}
// src/shared/plugins/declarative/runtime/declarative-memory-retention.ts
var RETAIN_DEBOUNCE_MS = 50;
var RETAIN_BATCH = 64;
function createMemoryRetention(post) {
const pending = /* @__PURE__ */ new Set();
let timer = null;
let started = false;
function flush() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
const entries = [];
for (const target of pending) {
const entry = memoryEntry(target);
if (!entry) {
continue;
}
try {
assertPlainJson(entry.value, `atom "${entry.key}"`);
entries.push(entry);
} catch {
entries.push({ key: entry.key, version: entry.version });
}
}
pending.clear();
for (let start = 0; start < entries.length; start += RETAIN_BATCH) {
post(entries.slice(start, start + RETAIN_BATCH));
}
}
function add(target) {
pending.add(target);
timer ??= setTimeout(flush, RETAIN_DEBOUNCE_MS);
}
return {
start() {
if (!started) {
started = true;
watchMemoryAtoms(add);
}
},
flush,
stop() {
if (started) {
flush();
watchMemoryAtoms(null);
started = false;
}
}
};
}
// src/shared/plugins/declarative/runtime/declarative-error-location.ts
var ORCA_RESERVED_PATH_PREFIX = "/__orca/";
var STACK_FRAME_URL = /([a-z][a-z0-9+.-]*:\/\/[^\s()]+):(\d+):(\d+)\)?\s*$/i;
function pageBase() {
return typeof location === "undefined" ? null : new URL("/", location.href).href;
}
function pluginErrorLocation(stack, base) {
for (const line of stack.split("\n")) {
const match = STACK_FRAME_URL.exec(line);
const url = match?.[1];
if (!match || !url?.startsWith(base)) {
continue;
}
const path = `/${url.slice(base.length).split(/[?#]/)[0] ?? ""}`;
if (path.startsWith(ORCA_RESERVED_PATH_PREFIX)) {
continue;
}
try {
return `${decodeURIComponent(path.slice(1))}:${match[2]}:${match[3]}`;
} catch {
return `${path.slice(1)}:${match[2]}:${match[3]}`;
}
}
return null;
}
function describePluginError(error, base = pageBase()) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error && typeof error.stack === "string" ? error.stack : "";
const at = base ? pluginErrorLocation(stack, base) : null;
if (!at) {
return message;
}
const file = at.replace(/:\d+:\d+$/, "");
return message.startsWith(`${file}:`) ? message : `${at}: ${message}`;
}
// src/shared/plugins/declarative/runtime/declarative-renderer-support.ts
var MIN_RENDER_INTERVAL_MS = 16;
var MAX_RETAINED_RENDERS = 32;
var HYDRATION_TIMEOUT_MS = 3e3;
function errorMessage(error) {
return describePluginError(error).slice(0, 4096);
}
function defaultSchedule(flush, delayMs) {
if (delayMs > 0) {
setTimeout(flush, delayMs);
} else {
queueMicrotask(flush);
}
}
function newDocumentId(now) {
return `${now.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}
function isCallable(value) {
return typeof value === "function";
}
// src/shared/plugins/declarative/runtime/declarative-host-messages.ts
function readHostMessage(payload) {
if (typeof payload !== "object" || payload === null) {
return {};
}
const panel = "panel" in payload ? payload.panel : void 0;
return {
document: "document" in payload ? payload.document : void 0,
revision: "revision" in payload ? payload.revision : void 0,
callback: "callback" in payload ? payload.callback : void 0,
args: "args" in payload ? payload.args : void 0,
edit: "edit" in payload ? payload.edit : void 0,
accepted: "accepted" in payload ? payload.accepted : void 0,
retryAfterMs: "retryAfterMs" in payload ? payload.retryAfterMs : void 0,
colorScheme: "colorScheme" in payload ? payload.colorScheme : void 0,
visible: "visible" in payload ? payload.visible : void 0,
panelId: typeof panel === "object" && panel !== null && "id" in panel ? panel.id : void 0,
plugin: "plugin" in payload ? payload.plugin : void 0,
workspace: "workspace" in payload ? payload.workspace : void 0,
memory: "memory" in payload ? payload.memory : void 0,
retain: "retain" in payload ? payload.retain : void 0
};
}
function readStoredValues(raw) {
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
return null;
}
const values = /* @__PURE__ */ new Map();
for (const [key, entry] of Object.entries(raw)) {
if (typeof entry !== "object" || entry === null) {
continue;
}
const version = "version" in entry ? entry.version : void 0;
if (typeof version === "number" && "value" in entry) {
values.set(key, { version, value: entry.value });
}
}
return values;
}
// src/shared/plugins/declarative/runtime/declarative-panel-context.ts
function createPanelContext(onRetain) {
const visibility = hostAtom(true);
const lifetime = new AbortController();
const context = {
subscriptions: [],
signal: lifetime.signal,
visible: visibility.atom,
panel: { id: "" }
};
let hydratedNow = false;
let resolve = () => void 0;
const hydrated = new Promise((done) => {
resolve = () => {
hydratedNow = true;
done();
};
});
function setVisibility(payload) {
const { visible } = readHostMessage(payload);
if (typeof visible === "boolean") {
visibility.set(visible);
}
}
return {
context,
hydrated,
isHydrated: () => hydratedNow,
markHydrated: () => resolve(),
setVisibility,
/** `ui.hydrate`: on boot, and again with the new workspace's values after a switch. */
hydrate(payload) {
const message = readHostMessage(payload);
if (typeof message.panelId === "string") {
context.panel = { id: message.panelId };
}
setVisibility(payload);
for (const scope of ["plugin", "workspace", "memory"]) {
const values = readStoredValues(message[scope]);
if (values) {
hydrateAtoms(scope, values);
}
}
if (message.retain === true) {
onRetain?.();
}
resolve();
},
abort: () => lifetime.abort()
};
}
// src/shared/plugins/declarative/runtime/declarative-persist-queue.ts
var PERSIST_DEBOUNCE_MS = 400;
function createPersistQueue(options) {
const pending = /* @__PURE__ */ new Set();
let timer = null;
function flush() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
const entries = [];
for (const target of pending) {
const entry = persistedEntry(target);
if (!entry) {
continue;
}
try {
assertPlainJson(entry.value, `persisted atom "${entry.key}"`);
entries.push(entry);
} catch (error) {
options.report(error);
}
}
pending.clear();
if (entries.length > 0) {
options.post(entries);
}
}
return {
add(target) {
pending.add(target);
timer ??= setTimeout(flush, PERSIST_DEBOUNCE_MS);
},
flush
};
}
// src/shared/plugins/declarative/runtime/declarative-storage-sync.ts
function createDeclarativeStorageSync(options) {
const storage2 = options.bridge?.storage;
const loads = /* @__PURE__ */ new Set();
const writtenSince = /* @__PURE__ */ new Map();
let generation = 0;
function load(key) {
if (!storage2) {
return;
}
const startedAt = generation;
const task = storage2.get(key).then((value) => {
if ((writtenSince.get(key) ?? -1) < startedAt) {
applyStorageValue(key, value ?? void 0);
}
}).catch(options.report).finally(() => loads.delete(task));
loads.add(task);
}
watchStorageAtoms((key) => {
generation += 1;
load(key);
});
const subscription = storage2?.onDidChange(({ key }) => {
generation += 1;
load(key);
});
return {
write(entries) {
const rest = [];
for (const entry of entries) {
if (entry.scope !== "storage") {
rest.push(entry);
continue;
}
generation += 1;
writtenSince.set(entry.key, generation);
void storage2?.set(entry.key, entry.value).catch(options.report);
}
return rest;
},
loaded: async () => {
await Promise.all(loads);
},
dispose() {
watchStorageAtoms(null);
subscription?.dispose();
}
};
}
function createDeclarativePersistence(options) {
const storageSync = createDeclarativeStorageSync(options);
const queue = createPersistQueue({
report: options.report,
post: (all) => {
const entries = storageSync.write(all);
if (entries.length > 0) {
options.post(entries);
}
}
});
watchPersistedAtoms(queue.add);
return {
flush: queue.flush,
loaded: storageSync.loaded,
dispose() {
watchPersistedAtoms(null);
storageSync.dispose();
}
};
}
// src/shared/plugins/declarative/runtime/declarative-renderer.ts
function createOrcaDeclarativeRuntime(options) {
const { post } = options;
const bridge = options.bridge ?? null;
const schedule = options.schedule ?? defaultSchedule;
const now = options.now ?? (() => Date.now());
const document2 = newDocumentId(now());
const theme = hostAtom({ colorScheme: options.colorScheme ?? "dark" });
const retention = createMemoryRetention(
(entries) => post(PLUGIN_DECLARATIVE_MESSAGES.retain, { document: document2, entries })
);
const panel = createPanelContext(retention.start);
const { context } = panel;
let component = null;
let revision = 0;
let lastRenderAt = Number.NEGATIVE_INFINITY;
let renders = /* @__PURE__ */ new Map();
let accepted = null;
const editsSeen = /* @__PURE__ */ new Map();
let subscriptions = /* @__PURE__ */ new Map();
let scheduled = false;
let setupCleanup = null;
let disposed = false;
function invalidate(delayMs = 0) {
if (scheduled || !component) {
return;
}
scheduled = true;
const wait = Math.max(delayMs, lastRenderAt + MIN_RENDER_INTERVAL_MS - now());
schedule(() => {
if (scheduled) {
render();
}
}, wait);
}
function resubscribe(reads) {
const next = /* @__PURE__ */ new Map();
for (const target of reads) {
next.set(target, subscriptions.get(target) ?? subscribeAtom(target, () => invalidate()));
}
for (const [target, unsubscribe] of subscriptions) {
if (!next.has(target)) {
unsubscribe();
}
}
subscriptions = next;
}
function report(error) {
console.error(error instanceof Error && error.stack ? error.stack : String(error));
const stack = error instanceof Error && error.stack ? error.stack.slice(0, 8192) : void 0;
post(PLUGIN_DECLARATIVE_MESSAGES.error, {
document: document2,
revision,
message: errorMessage(error),
...stack ? { stack } : {}
});
}
function retain(at, callbacks) {
renders.set(at, callbacks);
for (const kept of renders.keys()) {
if (renders.size <= MAX_RETAINED_RENDERS) {
break;
}
if (kept !== accepted) {
renders.delete(kept);
}
}
}
function render() {
scheduled = false;
if (!component || disposed) {
return;
}
lastRenderAt = now();
const reads = /* @__PURE__ */ new Set();
const mounted = component;
let evaluation;
try {
evaluation = trackAtomReads(reads, () => evaluateOrcaTree(mounted, editsSeen));
} catch (error) {
resubscribe(/* @__PURE__ */ new Set([...subscriptions.keys(), ...reads]));
report(error);
return;
}
revision += 1;
retain(revision, evaluation.callbacks);
resubscribe(reads);
post(PLUGIN_DECLARATIVE_MESSAGES.render, { document: document2, revision, tree: evaluation.tree });
}
function dispatchEvent(payload) {
const message = readHostMessage(payload);
const { callback, args, edit } = message;
if (message.document !== document2 || typeof callback !== "string") {
return false;
}
const handler = typeof message.revision === "number" ? renders.get(message.revision)?.get(callback) : void 0;
if (!handler) {
return false;
}
if (typeof edit === "number" && Number.isSafeInteger(edit)) {
editsSeen.set(callback, Math.max(edit, editsSeen.get(callback) ?? 0));
invalidate();
}
try {
const result = handler(...Array.isArray(args) ? args : []);
if (result instanceof Promise) {
result.catch(report);
}
} catch (error) {
report(error);
}
return true;
}
function acknowledge(payload) {
const message = readHostMessage(payload);
const acked = message.revision;
if (message.document !== document2 || typeof acked !== "number" || !renders.has(acked)) {
return;
}
if (message.accepted === true) {
accepted = acked;
for (const kept of renders.keys()) {
if (kept < acked) {
renders.delete(kept);
}
}
return;
}
renders.delete(acked);
const retryAfterMs = message.retryAfterMs;
if (typeof retryAfterMs === "number" && acked === revision) {
invalidate(retryAfterMs);
}
}
const persistence = createDeclarativePersistence({
bridge,
report,
post: (entries) => post(PLUGIN_DECLARATIVE_MESSAGES.persist, { document: document2, entries })
});
const flushPersist = persistence.flush;
const bridgeSubscriptions = bridge?.on ? [
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.event, dispatchEvent),
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.ack, acknowledge),
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.hydrate, panel.hydrate),
bridge.on(PLUGIN_DECLARATIVE_MESSAGES.visibility, panel.setVisibility),
bridge.on("theme", (payload) => {
const { colorScheme } = readHostMessage(payload);
if ((colorScheme === "light" || colorScheme === "dark") && read(theme.atom).colorScheme !== colorScheme) {
theme.set({ colorScheme });
}
})
] : [];
if (options.awaitHydration) {
post(PLUGIN_DECLARATIVE_MESSAGES.ready, { document: document2 });
const fallback = setTimeout(panel.markHydrated, HYDRATION_TIMEOUT_MS);
void panel.hydrated.then(() => clearTimeout(fallback));
} else {
panel.markHydrated();
}
function unsubscribeAll() {
for (const unsubscribe of subscriptions.values()) {
unsubscribe();
}
subscriptions = /* @__PURE__ */ new Map();
}
const runtime2 = {
document: document2,
async boot(module) {
await panel.hydrated;
await persistence.loaded();
if (isCallable(module.setup)) {
const cleanup = module.setup(context);
setupCleanup = isCallable(cleanup) ? () => void cleanup() : null;
}
const panelComponent = module.default;
if (isCallable(panelComponent)) {
runtime2.mount((props) => panelComponent(props));
} else if (!component) {
const kind = module.default === null ? "null" : typeof module.default;
throw new Error(
`The panel module must export a component function as its default export (got ${kind})`
);
}
},
mount(next) {
component = next;
if (panel.isHydrated()) {
render();
return;
}
void panel.hydrated.then(() => {
if (component === next) {
render();
}
});
},
unmount() {
component = null;
scheduled = false;
unsubscribeAll();
renders = /* @__PURE__ */ new Map();
accepted = null;
revision += 1;
post(PLUGIN_DECLARATIVE_MESSAGES.render, { document: document2, revision, tree: null });
},
mounted: () => component !== null,
dispatchEvent,
acknowledge,
hydrate: panel.hydrate,
rerender: () => render(),
flush() {
if (scheduled) {
render();
}
flushPersist();
retention.flush();
},
report,
themeAtom: theme.atom,
bridge,
dispose() {
if (disposed) {
return;
}
flushPersist();
retention.stop();
disposed = true;
component = null;
scheduled = false;
unsubscribeAll();
persistence.dispose();
for (const subscription of [...bridgeSubscriptions, ...context.subscriptions]) {
try {
subscription.dispose();
} catch (error) {
report(error);
}
}
try {
setupCleanup?.();
} catch (error) {
report(error);
}
panel.abort();
}
};
return runtime2;
}
// src/shared/plugins/declarative/runtime/declarative-sdk.ts
function hostError(code, message) {
return Object.assign(new Error(message), { code, retryable: code === "timeout" });
}
function unavailable(what) {
return Promise.reject(hostError("unavailable", `${what} is not available in this Orca`));
}
function isComponentTag(name) {
return Object.hasOwn(PLUGIN_DECLARATIVE_COMPONENT_PROPS, name);
}
function createOrcaSdk(source) {
const host2 = Object.freeze({
call(method, params, options = {}) {
const bridgeHost = source.bridge()?.host;
if (!bridgeHost) {
return unavailable("host.call");
}
const { signal, timeoutMs } = options;
if (timeoutMs === void 0) {
return bridgeHost.call(method, params, signal ? { signal } : void 0);
}
const controller = new AbortController();
const forward = () => controller.abort();
if (signal?.aborted) {
controller.abort();
}
signal?.addEventListener("abort", forward, { once: true });
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
controller.abort();
}, timeoutMs);
return bridgeHost.call(method, params, { signal: controller.signal }).catch((error) => {
throw timedOut ? hostError("timeout", `${method} took longer than ${timeoutMs} ms`) : error;
}).finally(() => {
clearTimeout(timer);
signal?.removeEventListener("abort", forward);
});
},
has(method) {
return source.bridge()?.host?.has?.(method) === true;
}
});
const api2 = Object.freeze({
get major() {
return source.bridge()?.api?.major ?? 1;
},
get minor() {
return source.bridge()?.api?.minor ?? 0;
}
});
const env2 = Object.freeze({
/** Orca's UI language (BCP-47); the guest's `navigator.language` follows the OS instead. */
get locale() {
return source.bridge()?.locale ?? "en";
},
/** Reading it while rendering re-renders on light/dark switches. */
get theme() {
return source.theme();
},
client: "desktop"
});
const shell2 = Object.freeze({
openExternal(url) {
return host2.call("shell.openExternal", { url: String(url) });
}
});
const files2 = Object.freeze({
saveAs(options) {
const { data } = options;
const bytes = ArrayBuffer.isView(data) ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data;
return host2.call("files.saveAs", { ...options, data: bytes });
}
});
function on2(event, handler) {
if (typeof event !== "string" || isReservedPluginDeclarativeEvent(event)) {
throw new TypeError(`"${String(event)}" is not an event plugins can subscribe to`);
}
return source.bridge()?.on?.(event, handler) ?? { dispose: () => void 0 };
}
const ui2 = Object.freeze({
/** Grows with the component catalog. */
catalog: PLUGIN_DECLARATIVE_CATALOG_VERSION,
/** Whether this Orca draws `component`, and accepts `prop` on it when given. */
has(component, prop) {
if (!isComponentTag(component)) {
return false;
}
const props = PLUGIN_DECLARATIVE_COMPONENT_PROPS[component];
return prop === void 0 || props.includes(prop);
}
});
return { host: host2, api: api2, env: env2, shell: shell2, files: files2, on: on2, ui: ui2 };
}
// src/shared/plugins/declarative/runtime/declarative-data-sdk.ts
function unavailable2(what) {
return Promise.reject(
Object.assign(new Error(`${what} is not available in this Orca`), {
code: "unavailable",
retryable: false
})
);
}
var NOTHING = { dispose: () => void 0 };
function createOrcaDataSdk(bridge) {
const plugin2 = Object.freeze({
request(name, input, options) {
return bridge()?.plugin?.request(name, input, options) ?? unavailable2("plugin.request");
},
on(event, handler) {
return bridge()?.plugin?.on(event, handler) ?? NOTHING;
}
});
const storage2 = Object.freeze({
get: (key) => bridge()?.storage?.get(key) ?? unavailable2("storage"),
set: (key, value) => bridge()?.storage?.set(key, value) ?? unavailable2("storage"),
delete: (key) => bridge()?.storage?.delete(key) ?? unavailable2("storage"),
keys: () => bridge()?.storage?.keys() ?? unavailable2("storage"),
onDidChange: (handler) => bridge()?.storage?.onDidChange(handler) ?? NOTHING
});
const settings2 = Object.freeze({
get: (key) => bridge()?.settings?.get(key) ?? unavailable2("settings"),
set: (key, value) => bridge()?.settings?.set(key, value) ?? unavailable2("settings"),
onDidChange: (handler) => bridge()?.settings?.onDidChange(handler) ?? NOTHING
});
const secrets2 = Object.freeze({
get: (key) => bridge()?.secrets?.get(key) ?? unavailable2("secrets"),
set: (key, value) => bridge()?.secrets?.set(key, value) ?? unavailable2("secrets"),
delete: (key) => bridge()?.secrets?.delete(key) ?? unavailable2("secrets")
});
return { plugin: plugin2, storage: storage2, settings: settings2, secrets: secrets2 };
}
// src/shared/plugins/declarative/runtime/orca-module.ts
var runtime = null;
var pendingMount = null;
function isOrcaBridge(value) {
return typeof value === "object" && value !== null && "on" in value && typeof value.on === "function";
}
function globalBridge() {
const scope = globalThis;
const candidate = "orca" in scope ? scope.orca : void 0;
return isOrcaBridge(candidate) ? candidate : null;
}
function documentColorScheme() {
const root = typeof document === "undefined" ? null : document.documentElement;
if (!root) {
return void 0;
}
return root.classList.contains("light") ? "light" : "dark";
}
function installOrcaDeclarativeRuntime(options) {
runtime?.dispose();
resetAtoms();
const installed = createOrcaDeclarativeRuntime({
bridge: globalBridge(),
colorScheme: documentColorScheme(),
...options
});
runtime = installed;
if (typeof addEventListener === "function") {
addEventListener("pagehide", () => installed.dispose(), { once: true });
}
const queued = pendingMount;
pendingMount = null;
if (queued) {
installed.mount(queued);
}
return installed;
}
function mount(component) {
if (runtime) {
runtime.mount(component);
} else {
pendingMount = component;
}
}
var sdk = createOrcaSdk({
bridge: () => runtime?.bridge ?? globalBridge(),
theme: () => runtime ? read(runtime.themeAtom) : { colorScheme: documentColorScheme() ?? "dark" }
});
var { host, api, env, shell, files, on, ui } = sdk;
var { plugin, storage, settings, secrets } = createOrcaDataSdk(
() => runtime?.bridge ?? globalBridge()
);
var Stack = "Stack";
var Row = "Row";
var Text = "Text";
var Heading = "Heading";
var Button = "Button";
var IconButton = "IconButton";
var Badge = "Badge";
var Card = "Card";
var List = "List";
var ListItem = "ListItem";
var Input = "Input";
var Switch = "Switch";
var Select = "Select";
var Separator = "Separator";
var Progress = "Progress";
var Spinner = "Spinner";
var EmptyState = "EmptyState";
var Link = "Link";
var Code = "Code";
export {
Badge,
Button,
Card,
Code,
EmptyState,
Fragment,
Heading,
IconButton,
Input,
Link,
List,
ListItem,
Progress,
Row,
Select,
Separator,
Spinner,
Stack,
Switch,
Text,
api,
atom,
derived,
env,
files,
h,
host,
installOrcaDeclarativeRuntime,
jsx,
jsx as jsxDEV,
jsx as jsxs,
mount,
on,
plugin,
read,
secrets,
set,
settings,
shell,
storage,
ui,
update
};