const SDK_V1_JS: &str = "// Copyright 2026, AgentMux Corp.\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * @agentmuxai/widget-sdk v1: the client for a sandboxed AgentMux widget\n * (docs/specs/SPEC_USER_WIDGETS_AND_WIDGET_API_2026_10_09.md \u{a7}6, \u{a7}7).\n *\n * import { connect } from \"/agentmux/widget-sdk/v1.js\";\n * const am = await connect();\n * am.ui.setTitle(\"Hello\");\n *\n * The widget\'s page runs in a sandboxed iframe with no access to AgentMux\n * itself; everything goes through this client, over the MessagePort AgentMux\n * hands the page when it loads. No dependencies.\n */\n\nexport const SDK_VERSION = \"1.2.0\";\nexport const PROTOCOL = 1;\n\nconst ERROR_NAMES = {\n [-32700]: \"parse_error\",\n [-32600]: \"invalid_request\",\n [-32601]: \"method_not_found\",\n [-32602]: \"invalid_params\",\n 1001: \"permission_denied\",\n 1002: \"limit_exceeded\",\n 1003: \"not_found\",\n 1004: \"network_error\",\n 1005: \"unavailable\",\n 1006: \"cancelled\",\n 1007: \"not_ready\",\n 1099: \"internal\",\n};\n\n/** An error answer from AgentMux: `code`, `name` (e.g. \"permission_denied\"),\n * `message`, `data` (e.g. `{ permission: \"storage\" }`). */\nexport class AgentMuxError extends Error {\n constructor(code, message, data) {\n super(message);\n this.code = code;\n this.name = ERROR_NAMES[code] ?? \"error\";\n this.data = data ?? null;\n }\n}\n\n/** Base64 for binary data (files, `net.fetch` bodies). */\nexport const base64 = {\n /** @param {Uint8Array | ArrayBuffer} bytes */\n encode(bytes) {\n const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n let s = \"\";\n for (let i = 0; i < u8.length; i += 0x8000) s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000));\n return btoa(s);\n },\n /** @param {string} text @returns {Uint8Array} */\n decode(text) {\n const bin = atob(text);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n },\n};\n\nfunction applyThemeVars(theme) {\n if (!theme || typeof document === \"undefined\") return;\n const root = document.documentElement;\n for (const [name, value] of Object.entries(theme.vars ?? {})) root.style.setProperty(name, String(value));\n root.dataset.amTheme = theme.mode;\n root.style.colorScheme = theme.mode === \"light\" ? \"light\" : \"dark\";\n}\n\n/** Wait for AgentMux to hand this page its port. */\nfunction waitForPort(timeoutMs) {\n return new Promise((resolve, reject) => {\n if (typeof window === \"undefined\" || window.parent === window) {\n reject(new Error(\"This page isn\'t running inside AgentMux: open it as an AgentMux widget.\"));\n return;\n }\n const timer = setTimeout(() => {\n window.removeEventListener(\"message\", onMessage);\n reject(new Error(`AgentMux didn\'t connect within ${timeoutMs} ms. Is this page an installed, approved widget?`));\n }, timeoutMs);\n function onMessage(ev) {\n if (ev.source !== window.parent) return;\n const d = ev.data;\n if (!d || d.type !== \"agentmux:connect\" || !ev.ports || !ev.ports[0]) return;\n if (Array.isArray(d.protocols) && !d.protocols.includes(PROTOCOL)) {\n clearTimeout(timer);\n window.removeEventListener(\"message\", onMessage);\n reject(new Error(`This AgentMux speaks widget protocol ${d.protocols.join(\", \")}; this SDK needs ${PROTOCOL}.`));\n return;\n }\n clearTimeout(timer);\n window.removeEventListener(\"message\", onMessage);\n resolve(ev.ports[0]);\n }\n window.addEventListener(\"message\", onMessage);\n });\n}\n\nclass Client {\n constructor(port) {\n this.port = port;\n this.nextId = 1;\n this.pending = new Map();\n this.listeners = new Map();\n // `command` events that came before anyone listened: a pane opened\n // for a command gets it right after the handshake, maybe before the\n // widget\'s `on(\"command\")`. Delivered to the first listener.\n this.unheard = [];\n this.closed = false;\n port.onmessage = (ev) => this.receive(ev.data);\n port.start?.();\n }\n\n receive(msg) {\n if (!msg || msg.jsonrpc !== \"2.0\") return;\n if (msg.id != null && this.pending.has(msg.id)) {\n const p = this.pending.get(msg.id);\n this.pending.delete(msg.id);\n if (msg.error) p.reject(new AgentMuxError(msg.error.code, msg.error.message, msg.error.data));\n else p.resolve(msg.result);\n return;\n }\n if (msg.method && msg.id == null) {\n if (msg.method === \"dispose\") this.closed = true;\n if (msg.method === \"command\" && !this.listeners.get(\"command\")?.size) {\n if (this.unheard.length < 20) this.unheard.push(msg.params ?? {});\n return;\n }\n for (const cb of this.listeners.get(msg.method) ?? []) {\n try {\n cb(msg.params ?? {});\n } catch (e) {\n console.error(`[agentmux widget] a \"${msg.method}\" handler threw`, e);\n }\n }\n }\n }\n\n call(method, params = {}) {\n if (this.closed) return Promise.reject(new AgentMuxError(1007, \"this widget\'s pane is closing\"));\n const id = this.nextId++;\n return new Promise((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n this.port.postMessage({ jsonrpc: \"2.0\", id, method, params });\n });\n }\n\n on(event, cb) {\n if (!this.listeners.has(event)) this.listeners.set(event, new Set());\n this.listeners.get(event).add(cb);\n if (event === \"command\" && this.unheard.length) {\n const kept = this.unheard.splice(0);\n queueMicrotask(() => kept.forEach((params) => this.receive({ jsonrpc: \"2.0\", method: \"command\", params })));\n }\n return () => this.listeners.get(event)?.delete(cb);\n }\n}\n\n/** A `net.fetch` result, with `text()`, `json()` and `bytes()`. */\nfunction wrapResponse(r) {\n return {\n ...r,\n ok: r.status >= 200 && r.status < 300,\n text: () => (r.body != null ? r.body : new TextDecoder().decode(base64.decode(r.bodyBase64 ?? \"\"))),\n json() {\n return JSON.parse(this.text());\n },\n bytes: () => (r.bodyBase64 != null ? base64.decode(r.bodyBase64) : new TextEncoder().encode(r.body ?? \"\")),\n };\n}\n\n/**\n * Connect to AgentMux. Resolves to the client once the handshake is done.\n *\n * @param {{ applyTheme?: boolean, timeoutMs?: number }} [options]\n * `applyTheme` (default true) sets the theme\'s CSS variables on the page and\n * keeps them current; `timeoutMs` (default 10000).\n */\nexport async function connect(options = {}) {\n const { applyTheme = true, timeoutMs = 10000 } = options;\n const port = await waitForPort(timeoutMs);\n const c = new Client(port);\n const info = await c.call(\"hello\", { protocol: PROTOCOL, sdk: SDK_VERSION });\n if (applyTheme) {\n applyThemeVars(info.theme);\n c.on(\"theme\", applyThemeVars);\n }\n const call = (method, params) => c.call(method, params);\n return {\n /** What `hello` returned: widget id/version/pane, AgentMux version, granted permissions, theme, meta, visibility, focused. */\n info,\n /** Subscribe to an event (`visibility`, `focus`, `theme`, `meta`, `action`, `command`, `storage`, `dispose`); returns an unsubscribe. */\n on: (event, cb) => c.on(event, cb),\n /** Any method, including ones this SDK has no wrapper for. */\n call,\n meta: {\n get: async () => (await call(\"meta.get\")).meta,\n set: (patch) => call(\"meta.set\", { patch }),\n },\n ui: {\n setTitle: (text, icon) => call(\"ui.setTitle\", icon ? { text, icon } : { text }),\n setHeaderActions: (actions) => call(\"ui.setHeaderActions\", { actions }),\n setContextMenu: (items) => call(\"ui.setContextMenu\", { items }),\n toast: (text, kind) => call(\"ui.toast\", kind ? { text, kind } : { text }),\n openUrl: (url) => call(\"ui.openUrl\", { url }),\n /** One of the widget\'s status bar items (`contributes.statusItems`): `{ text, icon, tooltip, tone, hidden }`; left-out fields show the manifest\'s. */\n setStatusItem: (id, look = {}) => call(\"ui.setStatusItem\", { id, ...look }),\n },\n theme: {\n get: () => call(\"theme.get\"),\n },\n panes: {\n open: (view, meta, split) => call(\"panes.open\", { view, ...(meta ? { meta } : {}), ...(split ? { split } : {}) }),\n },\n storage: {\n get: async (key) => (await call(\"storage.get\", { key })).value,\n set: (key, value) => call(\"storage.set\", { key, value }),\n delete: (key) => call(\"storage.delete\", { key }),\n list: async (prefix) => (await call(\"storage.list\", prefix ? { prefix } : {})).keys,\n },\n net: {\n /** Like `fetch`, through AgentMux, to an origin the widget declared. */\n fetch: async (url, init = {}) => {\n const params = { url };\n if (init.method) params.method = init.method;\n if (init.headers) params.headers = init.headers;\n if (init.timeoutMs) params.timeoutMs = init.timeoutMs;\n if (init.body instanceof Uint8Array || init.body instanceof ArrayBuffer) params.bodyBase64 = base64.encode(init.body);\n else if (init.body != null) params.body = String(init.body);\n return wrapResponse(await call(\"net.fetch\", params));\n },\n },\n files: {\n pick: async (opts = {}) => (await call(\"files.pick\", opts)).files.map((f) => ({ ...f, bytes: () => base64.decode(f.dataBase64) })),\n save: async (name, data, type) =>\n (\n await call(\"files.save\", {\n name,\n ...(type ? { type } : {}),\n dataBase64: typeof data === \"string\" ? base64.encode(new TextEncoder().encode(data)) : base64.encode(data),\n })\n ).saved,\n },\n clipboard: {\n writeText: (text) => call(\"clipboard.writeText\", { text }),\n },\n agents: {\n list: async () => (await call(\"agents.list\")).agents,\n send: async (agent, text) => (await call(\"agents.send\", { agent, text })).id,\n },\n };\n}\n\n/**\n * Pause work while the pane is hidden: calls `onActive` when it\'s shown (and\n * now, if it is) and `onDormant` when it\'s hidden. Returns an unsubscribe.\n */\nexport function useVisibility(am, { onActive, onDormant } = {}) {\n let last = am.info?.visibility;\n if (last === \"active\") onActive?.();\n return am.on(\"visibility\", ({ state }) => {\n if (state === last) return;\n last = state;\n if (state === \"active\") onActive?.();\n else onDormant?.();\n });\n}\n";Expand description
The widget SDK and its stylesheet (spec §7), served next to the widgets.