Skip to main content

SDK_V1_JS

Constant SDK_V1_JS 

Source
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.