@nimblebrain/synapse
Advanced tools
| import { applyTheme, foldFontFaces, fontFacesKey } from './chunk-DOKVNVTI.js'; | ||
| // src/host/theme.ts | ||
| function applyHostTheme(theme) { | ||
| if (typeof document !== "undefined") { | ||
| document.documentElement.setAttribute("data-theme", theme.mode); | ||
| } | ||
| applyTheme(theme.mode, theme.tokens, theme.fontFaces); | ||
| } | ||
| function preferredMode(win) { | ||
| try { | ||
| if (win?.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark"; | ||
| } catch { | ||
| } | ||
| return "light"; | ||
| } | ||
| function coerceMode(value, fallback) { | ||
| return value === "light" || value === "dark" ? value : fallback; | ||
| } | ||
| // src/host/types.ts | ||
| var HostUnsupportedError = class extends Error { | ||
| constructor(feature, host) { | ||
| super(`"${feature}" is not supported by the "${host}" host`); | ||
| this.name = "HostUnsupportedError"; | ||
| } | ||
| }; | ||
| var SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| var MCPUI_READY = "ui-lifecycle-iframe-ready"; | ||
| var MCPUI_RENDER_DATA = "ui-lifecycle-iframe-render-data"; | ||
| var MCPUI_SIZE_CHANGE = "ui-size-change"; | ||
| var MCPUI_LINK = "link"; | ||
| var MCPUI_PROMPT = "prompt"; | ||
| var OPENAI_SET_GLOBALS = "openai:set_globals"; | ||
| var MCPAPP_PROTOCOL_VERSION = "2026-01-26"; | ||
| var MCPAPP_INITIALIZE = "ui/initialize"; | ||
| var MCPAPP_INITIALIZED = "ui/notifications/initialized"; | ||
| var MCPAPP_TOOL_RESULT = "ui/notifications/tool-result"; | ||
| var MCPAPP_HOST_CONTEXT_CHANGED = "ui/notifications/host-context-changed"; | ||
| var MCPAPP_SIZE_CHANGED = "ui/notifications/size-changed"; | ||
| var MCPAPP_OPEN_LINK = "ui/open-link"; | ||
| var MCPAPP_MESSAGE = "ui/message"; | ||
| var MCPAPP_TEARDOWN = "ui/resource-teardown"; | ||
| var MCP_TOOLS_CALL = "tools/call"; | ||
| // src/host/adapters/chatgpt.ts | ||
| function createChatGPTAdapter(win, _options) { | ||
| const openai = () => win.openai; | ||
| let currentData = openai()?.toolOutput ?? null; | ||
| let currentTheme = { | ||
| mode: coerceMode(openai()?.theme, "light"), | ||
| tokens: {} | ||
| }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| const onSetGlobals = (event) => { | ||
| if (destroyed) return; | ||
| const globals = event.detail?.globals; | ||
| if (!globals) return; | ||
| if ("toolOutput" in globals && globals.toolOutput != null) { | ||
| currentData = globals.toolOutput; | ||
| for (const cb of dataCbs) cb(currentData); | ||
| } | ||
| if ("theme" in globals && globals.theme != null) { | ||
| const mode = coerceMode(globals.theme, currentTheme.mode); | ||
| if (mode !== currentTheme.mode) { | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| host: "chatgpt", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| const call = openai()?.callTool; | ||
| if (!call) throw new HostUnsupportedError("callTool", "chatgpt"); | ||
| return await call(name, args ?? {}); | ||
| }, | ||
| sendPrompt(text) { | ||
| const o = openai(); | ||
| const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage; | ||
| send?.({ prompt: text }); | ||
| }, | ||
| openLink(url) { | ||
| const open = openai()?.openExternal; | ||
| if (open) open({ href: url }); | ||
| else win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| const o = openai(); | ||
| return { | ||
| pull: typeof o?.callTool === "function", | ||
| sendPrompt: typeof o?.sendFollowUpMessage === "function" || typeof o?.sendFollowupMessage === "function", | ||
| openLink: true | ||
| }; | ||
| }, | ||
| start() { | ||
| win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals, { passive: true }); | ||
| currentData = openai()?.toolOutput ?? currentData; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/data.ts | ||
| function readInlineData(doc, elementId) { | ||
| if (!doc) return null; | ||
| const el = doc.getElementById(elementId); | ||
| const text = el?.textContent; | ||
| if (!text) return null; | ||
| try { | ||
| const parsed = JSON.parse(text); | ||
| return parsed ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function unwrapRenderData(payload) { | ||
| if (payload == null || typeof payload !== "object") return null; | ||
| const rec = payload; | ||
| const nested = rec.renderData; | ||
| const source = nested != null && typeof nested === "object" ? nested : rec; | ||
| if (source.toolOutput != null) return source.toolOutput; | ||
| if (source.structuredContent != null) return source.structuredContent; | ||
| return source; | ||
| } | ||
| // src/host/adapters/inline.ts | ||
| function createInlineAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let media = null; | ||
| const onSchemeChange = (event) => { | ||
| if (destroyed) return; | ||
| const mode = coerceMode(event.matches ? "dark" : "light", currentTheme.mode); | ||
| if (mode === currentTheme.mode) return; | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| }; | ||
| return { | ||
| host: "generic", | ||
| getData: () => currentData, | ||
| onData() { | ||
| return () => { | ||
| }; | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(_name) { | ||
| throw new HostUnsupportedError("callTool", "generic"); | ||
| }, | ||
| sendPrompt() { | ||
| }, | ||
| openLink(url) { | ||
| win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| return { pull: false, sendPrompt: false, openLink: true }; | ||
| }, | ||
| start() { | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| try { | ||
| media = win.matchMedia?.("(prefers-color-scheme: dark)") ?? null; | ||
| media?.addEventListener?.("change", onSchemeChange); | ||
| } catch { | ||
| media = null; | ||
| } | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| media?.removeEventListener?.("change", onSchemeChange); | ||
| media = null; | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/adapters/mcpapps.ts | ||
| var REQUEST_TIMEOUT_MS = 3e4; | ||
| function createMcpAppsAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| const autoResize = options.autoResize !== false; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let standardConfirmed = false; | ||
| let nextId = 1; | ||
| const pending = /* @__PURE__ */ new Map(); | ||
| let lastReportedHeight = -1; | ||
| let resizeObserver = null; | ||
| let onWindowResize = null; | ||
| const parent = () => win.parent ?? win; | ||
| function post(message) { | ||
| parent().postMessage(message, "*"); | ||
| } | ||
| function postLegacy(message) { | ||
| if (standardConfirmed) return; | ||
| post(message); | ||
| } | ||
| function notify(method, params) { | ||
| post({ jsonrpc: "2.0", method, params: params ?? {} }); | ||
| } | ||
| function request(method, params) { | ||
| const id = nextId++; | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| pending.delete(id); | ||
| reject(new Error(`"${method}" timed out`)); | ||
| }, REQUEST_TIMEOUT_MS); | ||
| pending.set(id, { resolve, reject, timer }); | ||
| post({ jsonrpc: "2.0", id, method, params: params ?? {} }); | ||
| }); | ||
| } | ||
| function emitData(next) { | ||
| if (next == null) return; | ||
| currentData = next; | ||
| for (const cb of dataCbs) cb(next); | ||
| } | ||
| function applyHostContext(ctx) { | ||
| if (!ctx || typeof ctx !== "object") return; | ||
| let { mode, tokens, fontFaces } = currentTheme; | ||
| let changed = false; | ||
| if (ctx.theme != null) { | ||
| const next = coerceMode(ctx.theme, mode); | ||
| if (next !== mode) { | ||
| mode = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| const styles = ctx.styles; | ||
| if (styles?.variables && typeof styles.variables === "object") { | ||
| tokens = { ...tokens, ...styles.variables }; | ||
| changed = true; | ||
| } | ||
| const nextFaces = foldFontFaces(fontFaces, ctx); | ||
| if (fontFacesKey(nextFaces) !== fontFacesKey(fontFaces)) { | ||
| fontFaces = nextFaces; | ||
| changed = true; | ||
| } | ||
| if (changed) { | ||
| currentTheme = { mode, tokens, ...fontFaces ? { fontFaces } : {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| function reportSize(height) { | ||
| if (destroyed) return; | ||
| const h = typeof height === "number" ? height : Math.ceil(win.document.body.scrollHeight); | ||
| if (h === lastReportedHeight) return; | ||
| lastReportedHeight = h; | ||
| notify(MCPAPP_SIZE_CHANGED, { height: h }); | ||
| postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } }); | ||
| } | ||
| function handleResponse(d) { | ||
| const id = Number(d.id); | ||
| const p = pending.get(id); | ||
| if (!p) return; | ||
| pending.delete(id); | ||
| clearTimeout(p.timer); | ||
| if (d.error != null) { | ||
| const err = d.error; | ||
| p.reject(new Error(err.message ?? "request failed")); | ||
| } else { | ||
| p.resolve(d.result); | ||
| } | ||
| } | ||
| function handleNotification(method, params) { | ||
| if (method === MCPAPP_TOOL_RESULT) { | ||
| const structured = params.structuredContent; | ||
| emitData(structured != null ? structured : unwrapRenderData(params)); | ||
| } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) { | ||
| applyHostContext(params); | ||
| } | ||
| } | ||
| function handleRequest(d) { | ||
| if (d.method === MCPAPP_TEARDOWN) { | ||
| post({ jsonrpc: "2.0", id: d.id, result: {} }); | ||
| } | ||
| } | ||
| function handleLegacy(d) { | ||
| if (d.type === MCPUI_RENDER_DATA || d.type === "renderData") { | ||
| const { theme, ...rest } = d.payload ?? {}; | ||
| if (theme != null) applyHostContext({ theme }); | ||
| if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest)); | ||
| } | ||
| } | ||
| const onMessage = (event) => { | ||
| if (destroyed) return; | ||
| if (event.source && event.source !== parent()) return; | ||
| const d = event.data; | ||
| if (!d || typeof d !== "object") return; | ||
| if (d.jsonrpc !== "2.0") { | ||
| handleLegacy(d); | ||
| return; | ||
| } | ||
| if (d.id != null && ("result" in d || "error" in d)) { | ||
| handleResponse(d); | ||
| } else if (typeof d.method === "string") { | ||
| if (d.id != null) handleRequest(d); | ||
| else handleNotification(d.method, d.params ?? {}); | ||
| } | ||
| }; | ||
| function setupResize() { | ||
| onWindowResize = () => reportSize(); | ||
| win.addEventListener("resize", onWindowResize); | ||
| if (typeof win.ResizeObserver !== "undefined") { | ||
| resizeObserver = new win.ResizeObserver(() => reportSize()); | ||
| resizeObserver.observe(win.document.body); | ||
| } | ||
| } | ||
| return { | ||
| host: "claude", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| return await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} }); | ||
| }, | ||
| sendPrompt(text) { | ||
| void request(MCPAPP_MESSAGE, { role: "user", content: [{ type: "text", text }] }).catch( | ||
| () => { | ||
| } | ||
| ); | ||
| postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } }); | ||
| }, | ||
| openLink(url) { | ||
| void request(MCPAPP_OPEN_LINK, { url }).catch(() => { | ||
| }); | ||
| postLegacy({ type: MCPUI_LINK, payload: { url } }); | ||
| }, | ||
| resize(height) { | ||
| reportSize(height); | ||
| }, | ||
| capabilities() { | ||
| return { pull: true, sendPrompt: true, openLink: true }; | ||
| }, | ||
| start() { | ||
| win.addEventListener("message", onMessage); | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| if (autoResize) setupResize(); | ||
| post({ type: MCPUI_READY }); | ||
| request(MCPAPP_INITIALIZE, { | ||
| appInfo: { name: options.name ?? "synapse-ui", version: options.version ?? "0.0.0" }, | ||
| appCapabilities: { availableDisplayModes: ["inline"] }, | ||
| protocolVersion: MCPAPP_PROTOCOL_VERSION | ||
| }).then((result) => { | ||
| if (destroyed) return; | ||
| standardConfirmed = true; | ||
| applyHostContext(result?.hostContext); | ||
| notify(MCPAPP_INITIALIZED, {}); | ||
| lastReportedHeight = -1; | ||
| reportSize(); | ||
| }).catch(() => { | ||
| }); | ||
| reportSize(); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener("message", onMessage); | ||
| if (onWindowResize) win.removeEventListener("resize", onWindowResize); | ||
| onWindowResize = null; | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| for (const p of pending.values()) { | ||
| clearTimeout(p.timer); | ||
| p.reject(new Error("adapter destroyed")); | ||
| } | ||
| pending.clear(); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/detect.ts | ||
| function detectHostKind(win) { | ||
| if (win.openai != null) return "chatgpt"; | ||
| try { | ||
| if (win.parent != null && win.parent !== win) return "claude"; | ||
| } catch { | ||
| return "claude"; | ||
| } | ||
| return "generic"; | ||
| } | ||
| function adapterForKind(kind, win, options) { | ||
| switch (kind) { | ||
| case "chatgpt": | ||
| return createChatGPTAdapter(win); | ||
| case "claude": | ||
| case "nimblebrain": | ||
| return createMcpAppsAdapter(win, options); | ||
| default: | ||
| return createInlineAdapter(win, options); | ||
| } | ||
| } | ||
| function selectAdapter(win, options) { | ||
| const kind = options.host ?? detectHostKind(win); | ||
| return adapterForKind(kind, win, options); | ||
| } | ||
| // src/host/connect.ts | ||
| function connectUI(options = {}) { | ||
| const win = options.window ?? globalThis; | ||
| const adapter = selectAdapter(win, options); | ||
| applyHostTheme(adapter.getTheme()); | ||
| const unsubTheme = adapter.onTheme(applyHostTheme); | ||
| adapter.start(); | ||
| let destroyed = false; | ||
| return { | ||
| data: () => adapter.getData(), | ||
| onData: (cb) => adapter.onData(cb), | ||
| theme: () => adapter.getTheme(), | ||
| onTheme: (cb) => adapter.onTheme(cb), | ||
| callTool: (name, args) => adapter.callTool(name, args), | ||
| sendPrompt: (text) => adapter.sendPrompt(text), | ||
| openLink: (url) => adapter.openLink(url), | ||
| resize: (height) => adapter.resize(height), | ||
| capabilities: () => adapter.capabilities(), | ||
| host: () => adapter.host, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| unsubTheme(); | ||
| adapter.destroy(); | ||
| } | ||
| }; | ||
| } | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, adapterForKind, applyHostTheme, coerceMode, connectUI, createChatGPTAdapter, createInlineAdapter, createMcpAppsAdapter, detectHostKind, preferredMode, selectAdapter }; | ||
| //# sourceMappingURL=chunk-5AR3IROW.js.map | ||
| //# sourceMappingURL=chunk-5AR3IROW.js.map |
| {"version":3,"sources":["../src/host/theme.ts","../src/host/types.ts","../src/host/adapters/chatgpt.ts","../src/host/data.ts","../src/host/adapters/inline.ts","../src/host/adapters/mcpapps.ts","../src/host/detect.ts","../src/host/connect.ts"],"names":[],"mappings":"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","file":"chunk-5AR3IROW.js","sourcesContent":["import { applyTheme } from \"../theme-defaults.js\";\nimport type { SynapseUITheme } from \"./types.js\";\n\n/**\n * Apply a resolved theme to the DOM for the cross-host client.\n *\n * Two conventions coexist across Synapse components, so the client drives both:\n *\n * - `document.documentElement[data-theme=\"light\"|\"dark\"]` — how self-contained\n * HTML components (Bassethound's report) gate their `--var` palettes, and the\n * lever a host's light/dark signal actually flips.\n * - CSS custom properties via {@link applyTheme} — how the\n * `@nimblebrain/synapse/ui` token components consume theme, backed by the\n * SDK's neutral defaults so every referenced var resolves in both modes.\n *\n * Setting both means an app can use either convention (or a host that supplies\n * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe.\n *\n * Any `fontFaces` the host supplies are loaded here too — a token can name a\n * family but not load it, so the two travel together. A host that sends none\n * leaves the web-safe fallbacks in force.\n */\nexport function applyHostTheme(theme: SynapseUITheme): void {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"data-theme\", theme.mode);\n }\n applyTheme(theme.mode, theme.tokens, theme.fontFaces);\n}\n\n/** Read the OS-level color scheme as a sane default for hosts that don't push a\n * theme until later (mcp-ui) or ever (standalone). */\nexport function preferredMode(win: Window | undefined): \"light\" | \"dark\" {\n try {\n if (win?.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) return \"dark\";\n } catch {\n // matchMedia unavailable (older test envs) — fall through to light.\n }\n return \"light\";\n}\n\n/** Coerce an arbitrary host-supplied theme signal to a mode. Accepts the string\n * form (`\"dark\"`) both hosts use; anything else falls back. */\nexport function coerceMode(value: unknown, fallback: \"light\" | \"dark\"): \"light\" | \"dark\" {\n return value === \"light\" || value === \"dark\" ? value : fallback;\n}\n","/**\n * Cross-host UI client — types and wire constants.\n *\n * The cross-host client (`connectUI`) renders one Synapse-authored component in\n * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude\n * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch\n * a host protocol. This is a **push-first** surface: the tool output that spawned\n * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the\n * pull escape hatch, advertised per host via `capabilities()`.\n *\n * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the\n * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so\n * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas).\n */\n\n/**\n * The host the client resolved to, as reported by `synapse.host()`. An escape\n * hatch — apps should rarely branch on it; `capabilities()` is the supported way\n * to feature-detect. `\"nimblebrain\"` is reserved for the runtime adapter (P3).\n */\nimport type { FontFaceDescriptor } from \"../types.js\";\n\nexport type HostKind = \"chatgpt\" | \"claude\" | \"nimblebrain\" | \"generic\";\n\n/** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS\n * custom properties the host publishes — the MCP Apps adapter reads them from\n * `hostContext.styles.variables`; where a host sends none they stay empty and the\n * SDK's neutral defaults back them. */\nexport interface SynapseUITheme {\n mode: \"light\" | \"dark\";\n tokens: Record<string, string>;\n /** Font faces the host wants loaded. Absent where the host sends none, which\n * leaves the SDK's web-safe fallbacks in force. See `FontFaceDescriptor`. */\n fontFaces?: FontFaceDescriptor[];\n}\n\n/** What the active host actually supports. `data()`/`onData()`/`theme()`/\n * `resize()` work everywhere; these three vary. */\nexport interface HostCapabilities {\n /** `callTool()` can reach the server (widget→server fetch). */\n pull: boolean;\n /** `sendPrompt()` reaches the agent conversation. */\n sendPrompt: boolean;\n /** `openLink()` opens an external URL through the host. */\n openLink: boolean;\n}\n\n/** Thrown by `callTool()` when the active host offers no widget→server call. */\nexport class HostUnsupportedError extends Error {\n constructor(feature: string, host: HostKind) {\n super(`\"${feature}\" is not supported by the \"${host}\" host`);\n this.name = \"HostUnsupportedError\";\n }\n}\n\nexport interface ConnectUIOptions {\n /** App name — informational; forwarded to hosts that accept an appInfo. */\n name?: string;\n /** App semver — informational. */\n version?: string;\n /**\n * Force a host adapter instead of auto-detecting. Used by preview harnesses,\n * SSR, and tests; production apps omit it and let the SDK feature-detect.\n * `\"claude\"` → MCP Apps standard adapter, `\"chatgpt\"` → OpenAI Apps adapter,\n * `\"generic\"` → inline adapter.\n */\n host?: HostKind;\n /**\n * `id` of the `<script type=\"application/json\">` element carrying pushed data\n * baked into the HTML (the mcp-ui / SSR path). Defaults to\n * {@link SYNAPSE_DATA_ELEMENT_ID}.\n */\n dataElementId?: string;\n /**\n * Auto-report content height to the host on layout changes (mcp-ui only).\n * Defaults to `true`. Set `false` to size manually via `resize()`.\n */\n autoResize?: boolean;\n /** Window to bind to. Defaults to the global `window`. Injectable for tests. */\n window?: Window & typeof globalThis;\n}\n\n/**\n * The public cross-host client. Bound to `synapse` by convention.\n *\n * ```ts\n * const synapse = connectUI();\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (may be null → empty state)\n * ```\n */\nexport interface SynapseUIClient {\n /** The current pushed data, or `null` before anything has been delivered. */\n data<T = unknown>(): T | null;\n /** Subscribe to data updates (NOT replayed — read `data()` for the current\n * value). Returns an unsubscribe. */\n onData<T = unknown>(cb: (data: T) => void): () => void;\n\n /** The current resolved theme. */\n theme(): SynapseUITheme;\n /** Subscribe to theme changes. The client already applies the theme to the DOM\n * (`data-theme` attribute + CSS variables) before this fires. */\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n\n /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the\n * host advertises no pull (`capabilities().pull === false`). */\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n /** Send a follow-up message to the agent conversation. No-op where unsupported. */\n sendPrompt(text: string): void;\n /** Open an external URL through the host (falls back to `window.open`). */\n openLink(url: string): void;\n /** Report content height to the host. Omit `height` to measure `document.body`. */\n resize(height?: number): void;\n\n /** What the active host supports. */\n capabilities(): HostCapabilities;\n /** The resolved host — an escape hatch; prefer `capabilities()`. */\n host(): HostKind;\n /** Tear down listeners/observers. */\n destroy(): void;\n}\n\n/**\n * Internal adapter contract. One per host bridge; the client is a thin façade\n * over the selected adapter.\n */\nexport interface HostAdapter {\n readonly host: HostKind;\n getData<T = unknown>(): T | null;\n onData<T = unknown>(cb: (data: T) => void): () => void;\n getTheme(): SynapseUITheme;\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n sendPrompt(text: string): void;\n openLink(url: string): void;\n resize(height?: number): void;\n capabilities(): HostCapabilities;\n /** Begin listening / send the ready handshake / read baked-in data. Called once\n * by `connectUI` synchronously so `getData()` is populated on return. */\n start(): void;\n destroy(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Wire constants — the bridge message shapes each host speaks. Centralized so a\n// host protocol tweak is a one-line change, not a grep-and-pray.\n// ---------------------------------------------------------------------------\n\n/** Default `id` of the baked-in data `<script type=\"application/json\">`. */\nexport const SYNAPSE_DATA_ELEMENT_ID = \"synapse-ui-data\";\n\n/** mcp-ui iframe-lifecycle messages (child → host, host → child). */\nexport const MCPUI_READY = \"ui-lifecycle-iframe-ready\";\nexport const MCPUI_RENDER_DATA = \"ui-lifecycle-iframe-render-data\";\nexport const MCPUI_SIZE_CHANGE = \"ui-size-change\";\n/** mcp-ui action messages (child → host). */\nexport const MCPUI_LINK = \"link\";\nexport const MCPUI_PROMPT = \"prompt\";\n\n/** OpenAI Apps SDK globals-broadcast event (host → child). */\nexport const OPENAI_SET_GLOBALS = \"openai:set_globals\";\n\n// ---------------------------------------------------------------------------\n// MCP Apps standard (SEP-1865) — the convergence bridge, primary for Claude\n// Desktop and the NimbleBrain runtime. The View iframe is an MCP client and the\n// host an MCP server; they exchange raw JSON-RPC 2.0 objects over `postMessage`\n// (no wrapper envelope). Method names are canonical to the ext-apps spec.\n// ---------------------------------------------------------------------------\n\n/** Protocol version exchanged in the `ui/initialize` handshake. */\nexport const MCPAPP_PROTOCOL_VERSION = \"2026-01-26\";\n/** View → host handshake request; the result carries the host context. */\nexport const MCPAPP_INITIALIZE = \"ui/initialize\";\n/** View → host notification sent once after the init result — the host holds all\n * pushes until it arrives. */\nexport const MCPAPP_INITIALIZED = \"ui/notifications/initialized\";\n/** Host → view notification carrying the tool output. `params` IS the\n * `CallToolResult` (data at `params.structuredContent`) — no `result` wrapper. */\nexport const MCPAPP_TOOL_RESULT = \"ui/notifications/tool-result\";\n/** Host → view notification carrying a partial host context (theme, styles, …). */\nexport const MCPAPP_HOST_CONTEXT_CHANGED = \"ui/notifications/host-context-changed\";\n/** View → host notification reporting intrinsic size (`{ width?, height? }`). A\n * host may keep the frame hidden until it receives one, so it is sent promptly. */\nexport const MCPAPP_SIZE_CHANGED = \"ui/notifications/size-changed\";\n/** View → host request: open an external URL (`{ url }`). */\nexport const MCPAPP_OPEN_LINK = \"ui/open-link\";\n/** View → host request: send a follow-up to the conversation\n * (`{ role: \"user\", content: ContentBlock[] }`). */\nexport const MCPAPP_MESSAGE = \"ui/message\";\n/** Host → view request: graceful teardown; the view replies `{}`. */\nexport const MCPAPP_TEARDOWN = \"ui/resource-teardown\";\n/** Standard MCP method the View may call over the same channel (pull). */\nexport const MCP_TOOLS_CALL = \"tools/call\";\n","import { coerceMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n OPENAI_SET_GLOBALS,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * The subset of the OpenAI Apps SDK `window.openai` surface this adapter uses.\n * All members are optional — the SDK version a host ships may not carry every\n * one, so each call is guarded and degraded.\n */\ninterface OpenAiHost {\n toolOutput?: unknown;\n theme?: unknown;\n callTool?: (name: string, args?: Record<string, unknown>) => Promise<unknown>;\n sendFollowUpMessage?: (arg: { prompt: string }) => void;\n /** Older casing seen in the wild — tried as a fallback. */\n sendFollowupMessage?: (arg: { prompt: string }) => void;\n openExternal?: (arg: { href: string }) => void;\n}\n\ninterface SetGlobalsDetail {\n globals?: { toolOutput?: unknown; theme?: unknown };\n}\n\n/**\n * ChatGPT (OpenAI Apps SDK) adapter.\n *\n * Data arrives on `window.openai.toolOutput` (available synchronously before the\n * widget script runs) and updates via the `openai:set_globals` event. Theme is a\n * mode string on the same surface. The host auto-sizes the iframe, so `resize()`\n * is a no-op. `callTool` is available when the host exposes `window.openai.callTool`.\n */\nexport function createChatGPTAdapter(\n win: Window & typeof globalThis,\n _options: ConnectUIOptions,\n): HostAdapter {\n const openai = () => (win as unknown as { openai?: OpenAiHost }).openai;\n\n let currentData: unknown = openai()?.toolOutput ?? null;\n let currentTheme: SynapseUITheme = {\n mode: coerceMode(openai()?.theme, \"light\"),\n tokens: {},\n };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n\n const onSetGlobals = (event: Event) => {\n if (destroyed) return;\n const globals = (event as CustomEvent<SetGlobalsDetail>).detail?.globals;\n if (!globals) return;\n if (\"toolOutput\" in globals && globals.toolOutput != null) {\n currentData = globals.toolOutput;\n for (const cb of dataCbs) cb(currentData);\n }\n if (\"theme\" in globals && globals.theme != null) {\n const mode = coerceMode(globals.theme, currentTheme.mode);\n if (mode !== currentTheme.mode) {\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n };\n\n return {\n host: \"chatgpt\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n const call = openai()?.callTool;\n if (!call) throw new HostUnsupportedError(\"callTool\", \"chatgpt\");\n return (await call(name, args ?? {})) as O;\n },\n sendPrompt(text: string) {\n const o = openai();\n const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage;\n send?.({ prompt: text });\n },\n openLink(url: string) {\n const open = openai()?.openExternal;\n if (open) open({ href: url });\n else win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // ChatGPT auto-sizes the widget iframe — nothing to report.\n },\n capabilities(): HostCapabilities {\n const o = openai();\n return {\n pull: typeof o?.callTool === \"function\",\n sendPrompt:\n typeof o?.sendFollowUpMessage === \"function\" ||\n typeof o?.sendFollowupMessage === \"function\",\n openLink: true,\n };\n },\n start() {\n win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener, { passive: true });\n // Re-read in case the host mutated globals between construction and start.\n currentData = openai()?.toolOutput ?? currentData;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener);\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","/**\n * Read data baked into the HTML as `<script type=\"application/json\" id=…>`.\n *\n * This is how the mcp-ui embedded-resource path and SSR/standalone deliver the\n * pushed tool output with no round-trip: the server helper escapes the payload\n * and substitutes it into the element (see the Python `SynapseUI.render_html`).\n * A data-free template leaves the marker comment in place, which fails\n * `JSON.parse` and reads back as `null` — exactly the \"no data yet\" state.\n */\nexport function readInlineData<T = unknown>(\n doc: Document | undefined,\n elementId: string,\n): T | null {\n if (!doc) return null;\n const el = doc.getElementById(elementId);\n const text = el?.textContent;\n if (!text) return null;\n try {\n const parsed = JSON.parse(text);\n return (parsed ?? null) as T | null;\n } catch {\n // Unreplaced marker or malformed blob — treat as no data.\n return null;\n }\n}\n\n/**\n * Pull the app payload out of an arbitrary render-data envelope. Hosts wrap the\n * tool output differently (`renderData`, `toolOutput`, `structuredContent`, or\n * the bare object), so unwrap the known envelope keys, else pass the object\n * through. Kept framework-generic — no app-specific keys.\n */\nexport function unwrapRenderData<T = unknown>(payload: unknown): T | null {\n if (payload == null || typeof payload !== \"object\") return null;\n const rec = payload as Record<string, unknown>;\n const nested = rec.renderData;\n const source =\n nested != null && typeof nested === \"object\" ? (nested as Record<string, unknown>) : rec;\n if (source.toolOutput != null) return source.toolOutput as T;\n if (source.structuredContent != null) return source.structuredContent as T;\n // No bare `data` envelope key — too ambiguous with an app's own `data` field.\n return source as T;\n}\n","import { readInlineData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * Inline / standalone fallback adapter.\n *\n * No live host bridge — used for SSR, previews, and a plain browser render. Data\n * comes only from the baked-in `<script type=\"application/json\">` blob; there is\n * nothing to push, so `onData` never fires after `start()`. Theme follows the OS\n * color scheme and tracks `prefers-color-scheme` changes. `openLink` opens a new\n * tab; `sendPrompt`/`callTool` have no destination.\n */\nexport function createInlineAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n let media: MediaQueryList | null = null;\n\n const onSchemeChange = (event: MediaQueryListEvent) => {\n if (destroyed) return;\n const mode = coerceMode(event.matches ? \"dark\" : \"light\", currentTheme.mode);\n if (mode === currentTheme.mode) return;\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n };\n\n return {\n host: \"generic\",\n getData: <T>() => currentData as T | null,\n onData() {\n // Static render — no updates after the initial baked-in read.\n return () => {};\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(_name: string): Promise<O> {\n throw new HostUnsupportedError(\"callTool\", \"generic\");\n },\n sendPrompt() {\n // No agent to reach in a standalone render.\n },\n openLink(url: string) {\n win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // No host to size for.\n },\n capabilities(): HostCapabilities {\n return { pull: false, sendPrompt: false, openLink: true };\n },\n start() {\n currentData = readInlineData(win.document, dataElementId);\n try {\n media = win.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n media?.addEventListener?.(\"change\", onSchemeChange);\n } catch {\n media = null;\n }\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n media?.removeEventListener?.(\"change\", onSchemeChange);\n media = null;\n themeCbs.clear();\n },\n };\n}\n","// `detection` and `theme-defaults` are type-only against `@modelcontextprotocol/*`,\n// so importing them keeps this adapter free of the ext-apps runtime (and Zod)\n// that the lean `window.SynapseUI` IIFE deliberately excludes.\nimport { foldFontFaces } from \"../../detection.js\";\nimport { fontFacesKey } from \"../../theme-defaults.js\";\nimport { readInlineData, unwrapRenderData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n MCP_TOOLS_CALL,\n MCPAPP_HOST_CONTEXT_CHANGED,\n MCPAPP_INITIALIZE,\n MCPAPP_INITIALIZED,\n MCPAPP_MESSAGE,\n MCPAPP_OPEN_LINK,\n MCPAPP_PROTOCOL_VERSION,\n MCPAPP_SIZE_CHANGED,\n MCPAPP_TEARDOWN,\n MCPAPP_TOOL_RESULT,\n MCPUI_LINK,\n MCPUI_PROMPT,\n MCPUI_READY,\n MCPUI_RENDER_DATA,\n MCPUI_SIZE_CHANGE,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/** How long a widget→host request (`callTool`, `openLink`, `sendPrompt`) waits\n * for its response before rejecting, so a silent host never hangs a promise. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * MCP Apps standard (SEP-1865) adapter — the convergence bridge for Claude\n * Desktop and other MCP Apps hosts.\n *\n * The View iframe is an MCP client speaking JSON-RPC 2.0 to `window.parent`:\n *\n * 1. posts `ui/initialize` and awaits the host context (theme, style variables);\n * 2. posts `ui/notifications/initialized`, then a `size-changed` — a host keeps\n * the frame hidden until it has both the handshake and a size;\n * 3. receives data via `ui/notifications/tool-result` (`params` IS the\n * `CallToolResult`, so data is at `params.structuredContent`) and theme via\n * `ui/notifications/host-context-changed`.\n *\n * Actions go up as requests: `ui/open-link`, `ui/message` (follow-up), and\n * `tools/call` (pull). The legacy mcp-ui `ui-lifecycle-*` messages are sent and\n * accepted alongside so a pre-standard host still renders — a standard host drops\n * the non-JSON-RPC frames, and a legacy host ignores the JSON-RPC ones.\n */\nexport function createMcpAppsAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n const autoResize = options.autoResize !== false;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n // Set once `ui/initialize` resolves — proof we're on an MCP Apps standard host,\n // which lets us stop mirroring actions to the legacy shim.\n let standardConfirmed = false;\n let nextId = 1;\n const pending = new Map<number, PendingRequest>();\n let lastReportedHeight = -1;\n let resizeObserver: ResizeObserver | null = null;\n let onWindowResize: (() => void) | null = null;\n\n const parent = () => win.parent ?? win;\n\n function post(message: Record<string, unknown>): void {\n parent().postMessage(message, \"*\");\n }\n\n // The legacy mcp-ui frames (render-data in, size/link/prompt out) are a\n // transitional shim for the one host that still speaks it — the NimbleBrain\n // runtime, which shares this adapter via the `nimblebrain` kind until the P3\n // `nimblebrain` adapter lands and this shim is removed. Once the handshake\n // confirms a standard host we stop mirroring, so a host that understood both\n // dialects never acts on an action twice.\n function postLegacy(message: Record<string, unknown>): void {\n if (standardConfirmed) return;\n post(message);\n }\n\n function notify(method: string, params?: Record<string, unknown>): void {\n post({ jsonrpc: \"2.0\", method, params: params ?? {} });\n }\n\n function request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`\"${method}\" timed out`));\n }, REQUEST_TIMEOUT_MS);\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });\n post({ jsonrpc: \"2.0\", id, method, params: params ?? {} });\n });\n }\n\n function emitData(next: unknown): void {\n if (next == null) return;\n currentData = next;\n for (const cb of dataCbs) cb(next);\n }\n\n /** Merge a full or partial host context into the resolved theme (mode, tokens,\n * font faces). Fonts ride the `synapse/fontFaces` extension: absent means\n * unchanged, an explicit (possibly empty) list replaces. ChatGPT supplies\n * only a mode string, so this is the adapter where host typography arrives. */\n function applyHostContext(ctx: Record<string, unknown> | null | undefined): void {\n if (!ctx || typeof ctx !== \"object\") return;\n let { mode, tokens, fontFaces } = currentTheme;\n let changed = false;\n if (ctx.theme != null) {\n const next = coerceMode(ctx.theme, mode);\n if (next !== mode) {\n mode = next;\n changed = true;\n }\n }\n const styles = ctx.styles as { variables?: Record<string, string> } | undefined;\n if (styles?.variables && typeof styles.variables === \"object\") {\n tokens = { ...tokens, ...styles.variables };\n changed = true;\n }\n const nextFaces = foldFontFaces(fontFaces, ctx);\n if (fontFacesKey(nextFaces) !== fontFacesKey(fontFaces)) {\n fontFaces = nextFaces;\n changed = true;\n }\n if (changed) {\n currentTheme = { mode, tokens, ...(fontFaces ? { fontFaces } : {}) };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n\n function reportSize(height?: number): void {\n if (destroyed) return;\n const h = typeof height === \"number\" ? height : Math.ceil(win.document.body.scrollHeight);\n if (h === lastReportedHeight) return;\n lastReportedHeight = h;\n notify(MCPAPP_SIZE_CHANGED, { height: h });\n postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } });\n }\n\n function handleResponse(d: Record<string, unknown>): void {\n // Normalize the echoed id: JSON-RPC requires a same-type echo, but a lax host\n // that returned \"1\" for 1 would otherwise miss the numeric-keyed pending map.\n const id = Number(d.id);\n const p = pending.get(id);\n if (!p) return;\n pending.delete(id);\n clearTimeout(p.timer);\n if (d.error != null) {\n const err = d.error as { message?: string };\n p.reject(new Error(err.message ?? \"request failed\"));\n } else {\n p.resolve(d.result);\n }\n }\n\n function handleNotification(method: string, params: Record<string, unknown>): void {\n if (method === MCPAPP_TOOL_RESULT) {\n // `params` is the CallToolResult: the render data lives at structuredContent.\n const structured = params.structuredContent;\n emitData(structured != null ? structured : unwrapRenderData(params));\n } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) {\n applyHostContext(params);\n }\n }\n\n function handleRequest(d: Record<string, unknown>): void {\n // The one host→view request we honor: acknowledge teardown so the host can\n // dispose the frame cleanly.\n if (d.method === MCPAPP_TEARDOWN) {\n post({ jsonrpc: \"2.0\", id: d.id, result: {} });\n }\n }\n\n /** Legacy mcp-ui render-data (non-JSON-RPC) — a pre-standard host's data path. */\n function handleLegacy(d: Record<string, unknown>): void {\n if (d.type === MCPUI_RENDER_DATA || d.type === \"renderData\") {\n const { theme, ...rest } = (d.payload ?? {}) as Record<string, unknown>;\n if (theme != null) applyHostContext({ theme });\n if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest));\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n if (destroyed) return;\n // Accept only frames from the host window when the browser sets a source.\n if (event.source && event.source !== parent()) return;\n const d = event.data as Record<string, unknown> | null | undefined;\n if (!d || typeof d !== \"object\") return;\n if (d.jsonrpc !== \"2.0\") {\n handleLegacy(d);\n return;\n }\n if (d.id != null && (\"result\" in d || \"error\" in d)) {\n handleResponse(d);\n } else if (typeof d.method === \"string\") {\n if (d.id != null) handleRequest(d);\n else handleNotification(d.method, (d.params ?? {}) as Record<string, unknown>);\n }\n };\n\n function setupResize(): void {\n onWindowResize = () => reportSize();\n win.addEventListener(\"resize\", onWindowResize);\n if (typeof win.ResizeObserver !== \"undefined\") {\n resizeObserver = new win.ResizeObserver(() => reportSize());\n resizeObserver.observe(win.document.body);\n }\n }\n\n return {\n host: \"claude\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n return (await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} })) as O;\n },\n sendPrompt(text: string) {\n // Standard follow-up (ack ignored); legacy mirror for a pre-standard host.\n void request(MCPAPP_MESSAGE, { role: \"user\", content: [{ type: \"text\", text }] }).catch(\n () => {},\n );\n postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } });\n },\n openLink(url: string) {\n void request(MCPAPP_OPEN_LINK, { url }).catch(() => {});\n postLegacy({ type: MCPUI_LINK, payload: { url } });\n },\n resize(height?: number) {\n reportSize(height);\n },\n capabilities(): HostCapabilities {\n // The MCP Apps standard host answers `tools/call` over this bridge, so pull\n // is advertised. A legacy-only host that shares this adapter (nimblebrain,\n // pre-P3) does not, so there callTool rejects only after REQUEST_TIMEOUT_MS\n // rather than failing fast.\n return { pull: true, sendPrompt: true, openLink: true };\n },\n start() {\n win.addEventListener(\"message\", onMessage as EventListener);\n\n // Baked-in data → available on first paint, before the handshake resolves.\n currentData = readInlineData(win.document, dataElementId);\n\n if (autoResize) setupResize();\n\n // Legacy mcp-ui ready (a standard host drops this non-JSON-RPC frame).\n post({ type: MCPUI_READY });\n\n // MCP Apps standard handshake. A legacy-only host never answers, so the\n // legacy render-data path (handleLegacy) still feeds the widget.\n request<{ hostContext?: Record<string, unknown> }>(MCPAPP_INITIALIZE, {\n appInfo: { name: options.name ?? \"synapse-ui\", version: options.version ?? \"0.0.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\"] },\n protocolVersion: MCPAPP_PROTOCOL_VERSION,\n })\n .then((result) => {\n if (destroyed) return;\n standardConfirmed = true;\n applyHostContext(result?.hostContext);\n notify(MCPAPP_INITIALIZED, {});\n // A host keeps the frame hidden until it gets a size after init — force one.\n lastReportedHeight = -1;\n reportSize();\n })\n .catch(() => {\n // Not an MCP Apps host (or it was slow) — the legacy path covers data.\n });\n\n reportSize();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(\"message\", onMessage as EventListener);\n if (onWindowResize) win.removeEventListener(\"resize\", onWindowResize);\n onWindowResize = null;\n resizeObserver?.disconnect();\n resizeObserver = null;\n for (const p of pending.values()) {\n clearTimeout(p.timer);\n p.reject(new Error(\"adapter destroyed\"));\n }\n pending.clear();\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","import { createChatGPTAdapter } from \"./adapters/chatgpt.js\";\nimport { createInlineAdapter } from \"./adapters/inline.js\";\nimport { createMcpAppsAdapter } from \"./adapters/mcpapps.js\";\nimport type { ConnectUIOptions, HostAdapter, HostKind } from \"./types.js\";\n\n/**\n * Minimal window shape the detector inspects. Kept structural so detection is\n * unit-testable with a plain object — no full `Window` fake required.\n */\ninterface DetectableWindow {\n openai?: unknown;\n parent?: unknown;\n self?: unknown;\n}\n\n/**\n * Feature-detect the host from the browsing context.\n *\n * - `window.openai` present → **chatgpt** (OpenAI Apps SDK).\n * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin\n * access that throws) → **claude** (MCP Apps standard postMessage host).\n * - top-level document → **generic** (inline / standalone).\n *\n * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and\n * honors an explicit `options.host` override for previews/SSR/tests.\n */\nexport function detectHostKind(win: DetectableWindow): HostKind {\n if (win.openai != null) return \"chatgpt\";\n try {\n if (win.parent != null && win.parent !== win) return \"claude\";\n } catch {\n // Cross-origin parent access throws → we are framed by another origin.\n return \"claude\";\n }\n return \"generic\";\n}\n\n/** Build the adapter for an explicit host kind (`options.host` or a detected one). */\nexport function adapterForKind(\n kind: HostKind,\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n switch (kind) {\n case \"chatgpt\":\n return createChatGPTAdapter(win, options);\n case \"claude\":\n case \"nimblebrain\":\n // Both speak the MCP Apps standard (SEP-1865). `nimblebrain` shares it for\n // now; a dedicated adapter over the `synapse/*` extension lands in P3.\n return createMcpAppsAdapter(win, options);\n default:\n return createInlineAdapter(win, options);\n }\n}\n\n/** Select and construct the host adapter, honoring `options.host` when set. */\nexport function selectAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const kind = options.host ?? detectHostKind(win as unknown as DetectableWindow);\n return adapterForKind(kind, win, options);\n}\n","import { selectAdapter } from \"./detect.js\";\nimport { applyHostTheme } from \"./theme.js\";\nimport type { ConnectUIOptions, SynapseUIClient } from \"./types.js\";\n\n/**\n * Connect a Synapse-authored component to whatever host it renders in — ChatGPT\n * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one\n * push-first API. Feature-detects the host, selects an adapter, and applies the\n * host theme to the DOM before returning.\n *\n * Synchronous: `data()` is populated from baked-in data (where present) on\n * return, and pushed updates arrive via `onData`. Bind the result to `synapse`:\n *\n * ```ts\n * const synapse = connectUI({ name: \"my-widget\", version: \"1.0.0\" });\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (null → empty state)\n * ```\n */\nexport function connectUI(options: ConnectUIOptions = {}): SynapseUIClient {\n const win = options.window ?? (globalThis as unknown as Window & typeof globalThis);\n const adapter = selectAdapter(win, options);\n\n // The client owns theme application: adapters resolve mode/tokens; this puts\n // them on the DOM (data-theme + CSS vars) so apps never wire theme by hand.\n applyHostTheme(adapter.getTheme());\n const unsubTheme = adapter.onTheme(applyHostTheme);\n\n adapter.start();\n\n let destroyed = false;\n\n return {\n data: <T>() => adapter.getData<T>(),\n onData: <T>(cb: (data: T) => void) => adapter.onData<T>(cb),\n theme: () => adapter.getTheme(),\n onTheme: (cb) => adapter.onTheme(cb),\n callTool: <O>(name: string, args?: Record<string, unknown>) => adapter.callTool<O>(name, args),\n sendPrompt: (text: string) => adapter.sendPrompt(text),\n openLink: (url: string) => adapter.openLink(url),\n resize: (height?: number) => adapter.resize(height),\n capabilities: () => adapter.capabilities(),\n host: () => adapter.host,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubTheme();\n adapter.destroy();\n },\n };\n}\n"]} |
| import { extractFontFaces, applyTheme, foldFontFaces, detectHost, extractTheme, fontFacesKey } from './chunk-DOKVNVTI.js'; | ||
| import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, HOST_CONTEXT_CHANGED_METHOD, INITIALIZED_METHOD, MESSAGE_METHOD, OPEN_LINK_METHOD, TOOL_RESULT_METHOD, RESOURCE_TEARDOWN_METHOD, TOOL_CANCELLED_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_INPUT_METHOD } from '@modelcontextprotocol/ext-apps'; | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": TOOL_RESULT_METHOD, | ||
| "tool-input": TOOL_INPUT_METHOD, | ||
| "tool-input-partial": TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": TOOL_CANCELLED_METHOD, | ||
| "theme-changed": HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| const initialFontFaces = extractFontFaces(ctx); | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {}, | ||
| ...initialFontFaces ? { fontFaces: initialFontFaces } : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| applyTheme(currentTheme.mode, currentTheme.tokens, currentTheme.fontFaces); | ||
| } | ||
| } | ||
| transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| const fontFaces = foldFontFaces(currentTheme.fontFaces, ctx); | ||
| currentTheme = { mode, tokens, ...fontFaces ? { fontFaces } : {} }; | ||
| applyTheme(mode, tokens, fontFaces); | ||
| const set = handlers.get(HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let currentFontFaces; | ||
| function syncFontFaces(ctx) { | ||
| currentFontFaces = foldFontFaces(currentFontFaces, ctx); | ||
| return currentFontFaces; | ||
| } | ||
| function resolveTheme(ctx) { | ||
| const theme = extractTheme(ctx); | ||
| return currentFontFaces ? { ...theme, fontFaces: currentFontFaces } : theme; | ||
| } | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| { | ||
| const theme = extractTheme(currentHostContext); | ||
| applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext)); | ||
| } | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| const theme = extractTheme(currentHostContext); | ||
| applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext)); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return resolveTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? resolveTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = resolveTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| if (fontFacesKey(a.fontFaces) !== fontFacesKey(b.fontFaces)) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| export { connect, createSynapse }; | ||
| //# sourceMappingURL=chunk-AGN3NDOK.js.map | ||
| //# sourceMappingURL=chunk-AGN3NDOK.js.map |
Sorry, the diff of this file is too big to display
| // src/theme-defaults.ts | ||
| var FONT_DISPLAY_VALUES = /* @__PURE__ */ new Set([ | ||
| "auto", | ||
| "block", | ||
| "swap", | ||
| "fallback", | ||
| "optional" | ||
| ]); | ||
| var LIGHT = { | ||
| // ── Surfaces ── | ||
| "--color-background-primary": "#ffffff", | ||
| "--color-background-secondary": "#fafafa", | ||
| "--color-background-tertiary": "#f3f4f6", | ||
| // ── Text ── | ||
| "--color-text-primary": "#111827", | ||
| "--color-text-secondary": "#6b7280", | ||
| "--color-text-tertiary": "#9ca3af", | ||
| "--color-text-accent": "#2563eb", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring ── | ||
| "--color-border-primary": "#e5e7eb", | ||
| "--color-border-secondary": "#d1d5db", | ||
| "--color-ring-primary": "#2563eb", | ||
| // ── Status / brand semantics ── | ||
| "--nb-color-danger": "#dc2626", | ||
| "--nb-color-success": "#059669", | ||
| "--nb-color-warning": "#f59e0b", | ||
| "--nb-color-warm": "#d4620a", | ||
| "--nb-color-warm-light": "#fef5ee", | ||
| "--nb-color-processing": "#7c3aed", | ||
| "--nb-color-processing-light": "#f3eeff", | ||
| "--nb-color-info-light": "#eef4ff" | ||
| }; | ||
| var DARK = { | ||
| // ── Surfaces (base → lifted) ── | ||
| "--color-background-primary": "#18181b", | ||
| "--color-background-secondary": "#27272a", | ||
| "--color-background-tertiary": "#2f2f34", | ||
| // ── Text ── | ||
| "--color-text-primary": "#fafafa", | ||
| "--color-text-secondary": "#a1a1aa", | ||
| "--color-text-tertiary": "#71717a", | ||
| "--color-text-accent": "#818cf8", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring (lighter than surfaces so they remain visible) ── | ||
| "--color-border-primary": "#3f3f46", | ||
| "--color-border-secondary": "#52525b", | ||
| "--color-ring-primary": "#818cf8", | ||
| // ── Status / brand semantics (brightened for contrast on dark) ── | ||
| "--nb-color-danger": "#f87171", | ||
| "--nb-color-success": "#34d399", | ||
| "--nb-color-warning": "#fbbf24", | ||
| "--nb-color-warm": "#fb923c", | ||
| "--nb-color-warm-light": "#3a2a1e", | ||
| "--nb-color-processing": "#a78bfa", | ||
| "--nb-color-processing-light": "#2a2440", | ||
| "--nb-color-info-light": "#1e2a44" | ||
| }; | ||
| var DEFAULT_THEME_VARS = { | ||
| light: LIGHT, | ||
| dark: DARK | ||
| }; | ||
| function applyThemeVariables(mode, hostVars) { | ||
| if (typeof document === "undefined") return; | ||
| const root = document.documentElement.style; | ||
| for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) { | ||
| root.setProperty(k, v); | ||
| } | ||
| if (hostVars && typeof hostVars === "object") { | ||
| for (const [k, v] of Object.entries(hostVars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| root.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| var managedFaces = /* @__PURE__ */ new Set(); | ||
| var appliedFontKey = ""; | ||
| function fontFacesKey(faces) { | ||
| if (!Array.isArray(faces)) return ""; | ||
| return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display])); | ||
| } | ||
| function normalizeFontFaces(raw) { | ||
| if (!Array.isArray(raw)) return void 0; | ||
| const usable = raw.filter(isFontFaceDescriptor); | ||
| if (raw.length > 0 && usable.length === 0) return void 0; | ||
| return usable; | ||
| } | ||
| function applyThemeFontFaces(faces) { | ||
| const next = normalizeFontFaces(faces); | ||
| if (next === void 0) return; | ||
| if (typeof document === "undefined") return; | ||
| if (!document.fonts || typeof FontFace === "undefined") return; | ||
| const key = fontFacesKey(next); | ||
| if (key === appliedFontKey) return; | ||
| appliedFontKey = key; | ||
| for (const face of managedFaces) { | ||
| try { | ||
| document.fonts.delete(face); | ||
| } catch { | ||
| } | ||
| } | ||
| managedFaces.clear(); | ||
| for (const d of next) { | ||
| try { | ||
| const face = new FontFace(d.family, d.src, { | ||
| weight: d.weight, | ||
| style: d.style, | ||
| // Wire data is untyped: ignore an unrecognised `display` rather than let | ||
| // the constructor reject the descriptor and cost the app this face. | ||
| display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : "swap" | ||
| }); | ||
| document.fonts.add(face); | ||
| managedFaces.add(face); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| function isFontFaceDescriptor(value) { | ||
| if (!value || typeof value !== "object") return false; | ||
| const d = value; | ||
| return typeof d.family === "string" && typeof d.src === "string"; | ||
| } | ||
| function applyTheme(mode, hostVars, fontFaces) { | ||
| applyThemeVariables(mode, hostVars); | ||
| applyThemeFontFaces(fontFaces); | ||
| } | ||
| // src/detection.ts | ||
| var FONT_FACES_CONTEXT_KEY = "synapse/fontFaces"; | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| function extractFontFaces(ctx) { | ||
| return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]); | ||
| } | ||
| function foldFontFaces(prev, ctx) { | ||
| return extractFontFaces(ctx) ?? prev; | ||
| } | ||
| export { FONT_FACES_CONTEXT_KEY, applyTheme, detectHost, extractFontFaces, extractTheme, foldFontFaces, fontFacesKey }; | ||
| //# sourceMappingURL=chunk-DOKVNVTI.js.map | ||
| //# sourceMappingURL=chunk-DOKVNVTI.js.map |
| {"version":3,"sources":["../src/theme-defaults.ts","../src/detection.ts"],"names":[],"mappings":"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","file":"chunk-DOKVNVTI.js","sourcesContent":["/**\n * Theme-aware default backing for the `@nimblebrain/synapse/ui` token contract.\n *\n * `tokens` (see `ui/tokens.ts`) are `var(--token, fallback)` references. A CSS\n * `var()` fallback is a *static literal* — it cannot branch on light vs. dark.\n * So any var the host does NOT inject resolves to its single hardcoded (light)\n * fallback in BOTH themes. Pair such a token with a theme-aware one and you get\n * white-on-white in dark mode: it looks correct in light, passes `tsc`/build,\n * and only breaks when the theme is toggled.\n *\n * This module closes that gap with a default layer the SDK controls and that\n * CAN branch on theme. {@link applyThemeVariables} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits still resolves to a theme-correct neutral value.\n *\n * It's the `var()` fallback, but able to branch on theme. The values stay\n * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. This keeps\n * the library host-agnostic while guaranteeing every token resolves correctly\n * in both themes even against an incomplete host, a standalone `connect()`\n * widget, or a third-party host.\n *\n * Font *faces* are the one part of theming CSS variables cannot carry — a token\n * names a family, it cannot load one. {@link applyThemeFontFaces} closes that\n * half, and {@link applyTheme} applies both together.\n *\n * Only theme-sensitive (color) vars are listed here. Theme-invariant vars\n * (radii, type scale, shadows, font stacks, weights, border widths) look the\n * same in both themes, so their static `var()` fallback is already correct —\n * they are intentionally absent. The `tokens` regression test enforces this\n * with a TOTAL partition: every referenced var must be either declared\n * theme-invariant (an explicit allowlist) or defined in both maps below, so a\n * newly added theme-sensitive token can't slip through unbacked.\n */\n\nimport type { FontDisplayValue, FontFaceDescriptor } from \"./types.js\";\n\nconst FONT_DISPLAY_VALUES = new Set<FontDisplayValue>([\n \"auto\",\n \"block\",\n \"swap\",\n \"fallback\",\n \"optional\",\n]);\n\n/**\n * Light-theme neutral defaults. Values match the (light) fallbacks baked into\n * `tokens` — applying them changes nothing observable in light mode; they exist\n * so the light path flows through the same code as dark and so a standalone\n * render with an explicit `light` theme is backed identically to its fallbacks.\n */\nconst LIGHT: Record<string, string> = {\n // ── Surfaces ──\n \"--color-background-primary\": \"#ffffff\",\n \"--color-background-secondary\": \"#fafafa\",\n \"--color-background-tertiary\": \"#f3f4f6\",\n // ── Text ──\n \"--color-text-primary\": \"#111827\",\n \"--color-text-secondary\": \"#6b7280\",\n \"--color-text-tertiary\": \"#9ca3af\",\n \"--color-text-accent\": \"#2563eb\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring ──\n \"--color-border-primary\": \"#e5e7eb\",\n \"--color-border-secondary\": \"#d1d5db\",\n \"--color-ring-primary\": \"#2563eb\",\n // ── Status / brand semantics ──\n \"--nb-color-danger\": \"#dc2626\",\n \"--nb-color-success\": \"#059669\",\n \"--nb-color-warning\": \"#f59e0b\",\n \"--nb-color-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\n \"--nb-color-processing\": \"#7c3aed\",\n \"--nb-color-processing-light\": \"#f3eeff\",\n \"--nb-color-info-light\": \"#eef4ff\",\n};\n\n/**\n * Dark-theme neutral defaults. A monotonic neutral ladder — the three surface\n * tiers are the darkest, borders sit a step lighter so they stay visible\n * against every surface, and text inverts to light. \"Subtle\"/\"strong\" keep\n * their light-mode semantics: `bgSubtle` reads as a lifted hover/inset tint and\n * `borderStrong` is more prominent than `border`.\n */\nconst DARK: Record<string, string> = {\n // ── Surfaces (base → lifted) ──\n \"--color-background-primary\": \"#18181b\",\n \"--color-background-secondary\": \"#27272a\",\n \"--color-background-tertiary\": \"#2f2f34\",\n // ── Text ──\n \"--color-text-primary\": \"#fafafa\",\n \"--color-text-secondary\": \"#a1a1aa\",\n \"--color-text-tertiary\": \"#71717a\",\n \"--color-text-accent\": \"#818cf8\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring (lighter than surfaces so they remain visible) ──\n \"--color-border-primary\": \"#3f3f46\",\n \"--color-border-secondary\": \"#52525b\",\n \"--color-ring-primary\": \"#818cf8\",\n // ── Status / brand semantics (brightened for contrast on dark) ──\n \"--nb-color-danger\": \"#f87171\",\n \"--nb-color-success\": \"#34d399\",\n \"--nb-color-warning\": \"#fbbf24\",\n \"--nb-color-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\n \"--nb-color-processing\": \"#a78bfa\",\n \"--nb-color-processing-light\": \"#2a2440\",\n \"--nb-color-info-light\": \"#1e2a44\",\n};\n\n/**\n * The neutral default theme, keyed by mode. Exported for the regression test\n * that asserts every color var referenced by `tokens` is backed in both modes.\n */\nexport const DEFAULT_THEME_VARS: Record<\"light\" | \"dark\", Record<string, string>> = {\n light: LIGHT,\n dark: DARK,\n};\n\n/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default.\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on every theme change is correct and cheap.\n *\n * Prefer {@link applyTheme}: it applies variables *and* font faces together, so\n * a caller cannot wire up half a theme.\n */\nexport function applyThemeVariables(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\n}\n\n/** The faces this module has added, so a re-apply replaces rather than accumulates. */\nconst managedFaces = new Set<FontFace>();\n\n/** Signature of the currently-applied set — lets a repeat call no-op. */\nlet appliedFontKey = \"\";\n\n/** Stable signature of a face list — the one comparison rule for \"same fonts?\",\n * shared by the sink's re-apply guard and `onThemeChanged`'s equality filter. */\nexport function fontFacesKey(faces: readonly FontFaceDescriptor[] | undefined | null): string {\n if (!Array.isArray(faces)) return \"\";\n return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display]));\n}\n\n/**\n * Normalise an untrusted face list into the one shape every layer trusts.\n *\n * THE rule, in one place:\n * - not an array → `undefined` — nothing was said, keep what's loaded\n * - array, none usable → `undefined` — a shape mistake is not a clear\n * - array, some usable → those entries\n * - empty array → `[]` — the explicit clear\n *\n * The wire and the sink each used to spell this out separately and drifted\n * apart, so a host whose descriptors were mis-shaped kept its typeface through\n * one path and lost it through the other. One predicate, one spelling.\n */\nexport function normalizeFontFaces(raw: unknown): FontFaceDescriptor[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const usable = raw.filter(isFontFaceDescriptor);\n if (raw.length > 0 && usable.length === 0) return undefined;\n return usable;\n}\n\n/**\n * Load host-supplied `@font-face`s into the app document.\n *\n * Uses the CSSOM {@link FontFace} constructor rather than building `@font-face`\n * CSS text. That is deliberate, and load-bearing for two reasons:\n *\n * 1. **No CSS-injection sink.** `family` and `src` are separate arguments, not\n * concatenated into a stylesheet, so a host value containing `}` cannot\n * escape the rule and inject arbitrary CSS into the app.\n * 2. **Uniform URL handling.** The `src` descriptor is parsed by the browser\n * against the `src` grammar, so relative (`url('/fonts/x.woff2')`), absolute\n * (`url('https://cdn.example/x.woff2')`) and `data:` forms all just work.\n * A malformed descriptor throws `SyntaxError` here instead of silently\n * producing dead CSS.\n *\n * The SDK ships no font data. A host that sends nothing leaves the web-safe\n * fallbacks in `ui/tokens.ts` in force — which is why omitting fonts entirely is\n * a supported configuration, not a degraded one.\n *\n * Faces are added but not force-loaded: the browser fetches a face when CSS\n * first matches it, so an unused weight costs nothing. `display` defaults to\n * `swap` so text paints in the fallback immediately rather than blocking.\n *\n * SSR-safe, and safe in environments without the CSS Font Loading API (older\n * jsdom): both no-op. Re-applying an unchanged set is a no-op.\n */\nexport function applyThemeFontFaces(faces: readonly FontFaceDescriptor[] | undefined | null): void {\n // `normalizeFontFaces` owns what absent/garbage means. Reached from the public\n // `applyHostTheme`, so the input here is not necessarily typed: a host\n // building `fontFaces` from untrusted JSON can hand us mis-shaped entries,\n // and reading those as \"clear\" would unload the typeface over a typo.\n const next = normalizeFontFaces(faces);\n if (next === undefined) return;\n if (typeof document === \"undefined\") return;\n // `document.fonts` (CSS Font Loading API) is absent in some test DOMs.\n if (!document.fonts || typeof FontFace === \"undefined\") return;\n\n const key = fontFacesKey(next);\n if (key === appliedFontKey) return;\n appliedFontKey = key;\n\n for (const face of managedFaces) {\n try {\n document.fonts.delete(face);\n } catch {\n // Already evicted, or the API rejected the handle — nothing to undo.\n }\n }\n managedFaces.clear();\n\n for (const d of next) {\n try {\n const face = new FontFace(d.family, d.src, {\n weight: d.weight,\n style: d.style,\n // Wire data is untyped: ignore an unrecognised `display` rather than let\n // the constructor reject the descriptor and cost the app this face.\n display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : \"swap\",\n });\n document.fonts.add(face);\n managedFaces.add(face);\n } catch {\n // Malformed descriptor — skip this face and keep the web-safe fallback.\n // One bad entry must not cost the app the rest of its typography.\n }\n }\n}\n\nfunction isFontFaceDescriptor(value: unknown): value is FontFaceDescriptor {\n if (!value || typeof value !== \"object\") return false;\n const d = value as Partial<FontFaceDescriptor>;\n return typeof d.family === \"string\" && typeof d.src === \"string\";\n}\n\n/**\n * Apply a resolved theme to the app document — CSS variables and font faces.\n *\n * This is the single path by which theming reaches the DOM: the handshake,\n * `host-context-changed`, and the React `<SynapseProvider>` all funnel through\n * here. Keeping colour and typography on one call is the point — two entry\n * points invite a caller to wire one and forget the other, shipping a host's\n * palette under the wrong typeface.\n *\n * Omitting `fontFaces` is safe and means \"leave the loaded faces alone\" (see\n * {@link applyThemeFontFaces}), so a caller re-applying a theme derived from a\n * partial host context cannot silently strip the host's typeface.\n */\nexport function applyTheme(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n fontFaces?: readonly FontFaceDescriptor[] | null,\n): void {\n applyThemeVariables(mode, hostVars);\n applyThemeFontFaces(fontFaces);\n}\n\n/** Test seam — forget applied faces so a fresh apply is observable. */\nexport function resetAppliedFontFaces(): void {\n managedFaces.clear();\n appliedFontKey = \"\";\n}\n","import type { McpUiHostContext, McpUiInitializeResult } from \"@modelcontextprotocol/ext-apps\";\nimport { normalizeFontFaces } from \"./theme-defaults.js\";\nimport type { FontFaceDescriptor, HostInfo, SynapseTheme } from \"./types\";\n\n/**\n * Host-context key carrying `@font-face` descriptors.\n *\n * A NimbleBrain extension (`synapse/` prefix), because the ext-apps spec has no\n * equivalent: `styles.variables` is a flat `Record<McpUiStyleVariableKey, string>`\n * that can name a font family but cannot carry the rule that loads it.\n * `McpUiHostContext` declares `[key: string]: unknown` for forward\n * compatibility, so riding an extra key is spec-legal, and hosts that don't send\n * it degrade to the web-safe token fallbacks.\n */\nexport const FONT_FACES_CONTEXT_KEY = \"synapse/fontFaces\";\n\nconst DEFAULT_THEME: SynapseTheme = {\n mode: \"light\",\n primaryColor: \"#6366f1\",\n tokens: {},\n};\n\n/**\n * Detect the host environment from the ext-apps `ui/initialize` response.\n *\n * Reports identity only (host name, protocol version). Theme lives in the\n * unified host-context state and is read via `extractTheme(hostContext)`\n * — no parallel `theme` field on `HostInfo`.\n *\n * Handles missing or malformed fields gracefully — never throws.\n */\nexport function detectHost(initResponse: unknown): HostInfo {\n const resp = initResponse as Partial<McpUiInitializeResult> | null | undefined;\n\n const hostName = resp?.hostInfo?.name ?? \"unknown\";\n const protocolVersion = resp?.protocolVersion ?? \"unknown\";\n\n return {\n isNimbleBrain: hostName === \"nimblebrain\",\n serverName: hostName,\n protocolVersion,\n };\n}\n\nexport function extractTheme(ctx: Partial<McpUiHostContext> | undefined): SynapseTheme {\n if (!ctx) return { ...DEFAULT_THEME };\n\n // Spec: theme is a string (\"light\" | \"dark\")\n const mode = ctx.theme === \"light\" || ctx.theme === \"dark\" ? ctx.theme : DEFAULT_THEME.mode;\n\n // Spec: tokens live under styles.variables\n const variables = ctx.styles?.variables;\n const tokens =\n variables && typeof variables === \"object\" && !Array.isArray(variables)\n ? (variables as Record<string, string>)\n : {};\n\n // Deliberately no `fontFaces` here. Faces are sticky across a partial context\n // (see `foldFontFaces`), and this function sees only the context in hand — so\n // populating the field would publish a NON-sticky value on the return type and\n // invite `extractTheme(ctx).fontFaces`, which is the unload-on-toggle bug.\n // `resolveTheme` overlays the sticky set; nothing else should.\n return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens };\n}\n\n/**\n * Read the `synapse/fontFaces` host-context extension.\n *\n * `undefined` and `[]` are **distinct answers**, because a host-context-changed\n * carries only the fields that changed:\n * - `undefined` — the host said nothing about fonts, so keep what's loaded.\n * - `[]` — the host explicitly sent an empty list, i.e. drop the faces.\n * Collapsing the two would leave a host no way to clear typography it had\n * previously set.\n *\n * Individual malformed entries are dropped rather than rejecting the batch —\n * one bad face must not cost the app the rest of its typography. Anything that\n * survives is still validated by the browser when the descriptor is built.\n */\nexport function extractFontFaces(\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);\n}\n\n/**\n * THE rule for merging incoming faces over what is already loaded, in one\n * place: **absent — or a batch with nothing usable — means unchanged; an\n * explicit list, including empty, replaces.**\n *\n * Every consumer of the host-context font extension folds through here. The\n * rule leaked one layer at a time when each site spelled it itself, so the\n * spelling now lives once and the call sites just call it.\n */\nexport function foldFontFaces(\n prev: FontFaceDescriptor[] | undefined,\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return extractFontFaces(ctx) ?? prev;\n}\n"]} |
| 'use strict'; | ||
| // src/theme-defaults.ts | ||
| var FONT_DISPLAY_VALUES = /* @__PURE__ */ new Set([ | ||
| "auto", | ||
| "block", | ||
| "swap", | ||
| "fallback", | ||
| "optional" | ||
| ]); | ||
| var LIGHT = { | ||
| // ── Surfaces ── | ||
| "--color-background-primary": "#ffffff", | ||
| "--color-background-secondary": "#fafafa", | ||
| "--color-background-tertiary": "#f3f4f6", | ||
| // ── Text ── | ||
| "--color-text-primary": "#111827", | ||
| "--color-text-secondary": "#6b7280", | ||
| "--color-text-tertiary": "#9ca3af", | ||
| "--color-text-accent": "#2563eb", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring ── | ||
| "--color-border-primary": "#e5e7eb", | ||
| "--color-border-secondary": "#d1d5db", | ||
| "--color-ring-primary": "#2563eb", | ||
| // ── Status / brand semantics ── | ||
| "--nb-color-danger": "#dc2626", | ||
| "--nb-color-success": "#059669", | ||
| "--nb-color-warning": "#f59e0b", | ||
| "--nb-color-warm": "#d4620a", | ||
| "--nb-color-warm-light": "#fef5ee", | ||
| "--nb-color-processing": "#7c3aed", | ||
| "--nb-color-processing-light": "#f3eeff", | ||
| "--nb-color-info-light": "#eef4ff" | ||
| }; | ||
| var DARK = { | ||
| // ── Surfaces (base → lifted) ── | ||
| "--color-background-primary": "#18181b", | ||
| "--color-background-secondary": "#27272a", | ||
| "--color-background-tertiary": "#2f2f34", | ||
| // ── Text ── | ||
| "--color-text-primary": "#fafafa", | ||
| "--color-text-secondary": "#a1a1aa", | ||
| "--color-text-tertiary": "#71717a", | ||
| "--color-text-accent": "#818cf8", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring (lighter than surfaces so they remain visible) ── | ||
| "--color-border-primary": "#3f3f46", | ||
| "--color-border-secondary": "#52525b", | ||
| "--color-ring-primary": "#818cf8", | ||
| // ── Status / brand semantics (brightened for contrast on dark) ── | ||
| "--nb-color-danger": "#f87171", | ||
| "--nb-color-success": "#34d399", | ||
| "--nb-color-warning": "#fbbf24", | ||
| "--nb-color-warm": "#fb923c", | ||
| "--nb-color-warm-light": "#3a2a1e", | ||
| "--nb-color-processing": "#a78bfa", | ||
| "--nb-color-processing-light": "#2a2440", | ||
| "--nb-color-info-light": "#1e2a44" | ||
| }; | ||
| var DEFAULT_THEME_VARS = { | ||
| light: LIGHT, | ||
| dark: DARK | ||
| }; | ||
| function applyThemeVariables(mode, hostVars) { | ||
| if (typeof document === "undefined") return; | ||
| const root = document.documentElement.style; | ||
| for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) { | ||
| root.setProperty(k, v); | ||
| } | ||
| if (hostVars && typeof hostVars === "object") { | ||
| for (const [k, v] of Object.entries(hostVars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| root.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| var managedFaces = /* @__PURE__ */ new Set(); | ||
| var appliedFontKey = ""; | ||
| function fontFacesKey(faces) { | ||
| if (!Array.isArray(faces)) return ""; | ||
| return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display])); | ||
| } | ||
| function normalizeFontFaces(raw) { | ||
| if (!Array.isArray(raw)) return void 0; | ||
| const usable = raw.filter(isFontFaceDescriptor); | ||
| if (raw.length > 0 && usable.length === 0) return void 0; | ||
| return usable; | ||
| } | ||
| function applyThemeFontFaces(faces) { | ||
| const next = normalizeFontFaces(faces); | ||
| if (next === void 0) return; | ||
| if (typeof document === "undefined") return; | ||
| if (!document.fonts || typeof FontFace === "undefined") return; | ||
| const key = fontFacesKey(next); | ||
| if (key === appliedFontKey) return; | ||
| appliedFontKey = key; | ||
| for (const face of managedFaces) { | ||
| try { | ||
| document.fonts.delete(face); | ||
| } catch { | ||
| } | ||
| } | ||
| managedFaces.clear(); | ||
| for (const d of next) { | ||
| try { | ||
| const face = new FontFace(d.family, d.src, { | ||
| weight: d.weight, | ||
| style: d.style, | ||
| // Wire data is untyped: ignore an unrecognised `display` rather than let | ||
| // the constructor reject the descriptor and cost the app this face. | ||
| display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : "swap" | ||
| }); | ||
| document.fonts.add(face); | ||
| managedFaces.add(face); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| function isFontFaceDescriptor(value) { | ||
| if (!value || typeof value !== "object") return false; | ||
| const d = value; | ||
| return typeof d.family === "string" && typeof d.src === "string"; | ||
| } | ||
| function applyTheme(mode, hostVars, fontFaces) { | ||
| applyThemeVariables(mode, hostVars); | ||
| applyThemeFontFaces(fontFaces); | ||
| } | ||
| // src/detection.ts | ||
| var FONT_FACES_CONTEXT_KEY = "synapse/fontFaces"; | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| function extractFontFaces(ctx) { | ||
| return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]); | ||
| } | ||
| function foldFontFaces(prev, ctx) { | ||
| return extractFontFaces(ctx) ?? prev; | ||
| } | ||
| exports.FONT_FACES_CONTEXT_KEY = FONT_FACES_CONTEXT_KEY; | ||
| exports.applyTheme = applyTheme; | ||
| exports.detectHost = detectHost; | ||
| exports.extractFontFaces = extractFontFaces; | ||
| exports.extractTheme = extractTheme; | ||
| exports.foldFontFaces = foldFontFaces; | ||
| exports.fontFacesKey = fontFacesKey; | ||
| //# sourceMappingURL=chunk-IA2M4GTK.cjs.map | ||
| //# sourceMappingURL=chunk-IA2M4GTK.cjs.map |
| {"version":3,"sources":["../src/theme-defaults.ts","../src/detection.ts"],"names":[],"mappings":"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","file":"chunk-IA2M4GTK.cjs","sourcesContent":["/**\n * Theme-aware default backing for the `@nimblebrain/synapse/ui` token contract.\n *\n * `tokens` (see `ui/tokens.ts`) are `var(--token, fallback)` references. A CSS\n * `var()` fallback is a *static literal* — it cannot branch on light vs. dark.\n * So any var the host does NOT inject resolves to its single hardcoded (light)\n * fallback in BOTH themes. Pair such a token with a theme-aware one and you get\n * white-on-white in dark mode: it looks correct in light, passes `tsc`/build,\n * and only breaks when the theme is toggled.\n *\n * This module closes that gap with a default layer the SDK controls and that\n * CAN branch on theme. {@link applyThemeVariables} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits still resolves to a theme-correct neutral value.\n *\n * It's the `var()` fallback, but able to branch on theme. The values stay\n * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. This keeps\n * the library host-agnostic while guaranteeing every token resolves correctly\n * in both themes even against an incomplete host, a standalone `connect()`\n * widget, or a third-party host.\n *\n * Font *faces* are the one part of theming CSS variables cannot carry — a token\n * names a family, it cannot load one. {@link applyThemeFontFaces} closes that\n * half, and {@link applyTheme} applies both together.\n *\n * Only theme-sensitive (color) vars are listed here. Theme-invariant vars\n * (radii, type scale, shadows, font stacks, weights, border widths) look the\n * same in both themes, so their static `var()` fallback is already correct —\n * they are intentionally absent. The `tokens` regression test enforces this\n * with a TOTAL partition: every referenced var must be either declared\n * theme-invariant (an explicit allowlist) or defined in both maps below, so a\n * newly added theme-sensitive token can't slip through unbacked.\n */\n\nimport type { FontDisplayValue, FontFaceDescriptor } from \"./types.js\";\n\nconst FONT_DISPLAY_VALUES = new Set<FontDisplayValue>([\n \"auto\",\n \"block\",\n \"swap\",\n \"fallback\",\n \"optional\",\n]);\n\n/**\n * Light-theme neutral defaults. Values match the (light) fallbacks baked into\n * `tokens` — applying them changes nothing observable in light mode; they exist\n * so the light path flows through the same code as dark and so a standalone\n * render with an explicit `light` theme is backed identically to its fallbacks.\n */\nconst LIGHT: Record<string, string> = {\n // ── Surfaces ──\n \"--color-background-primary\": \"#ffffff\",\n \"--color-background-secondary\": \"#fafafa\",\n \"--color-background-tertiary\": \"#f3f4f6\",\n // ── Text ──\n \"--color-text-primary\": \"#111827\",\n \"--color-text-secondary\": \"#6b7280\",\n \"--color-text-tertiary\": \"#9ca3af\",\n \"--color-text-accent\": \"#2563eb\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring ──\n \"--color-border-primary\": \"#e5e7eb\",\n \"--color-border-secondary\": \"#d1d5db\",\n \"--color-ring-primary\": \"#2563eb\",\n // ── Status / brand semantics ──\n \"--nb-color-danger\": \"#dc2626\",\n \"--nb-color-success\": \"#059669\",\n \"--nb-color-warning\": \"#f59e0b\",\n \"--nb-color-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\n \"--nb-color-processing\": \"#7c3aed\",\n \"--nb-color-processing-light\": \"#f3eeff\",\n \"--nb-color-info-light\": \"#eef4ff\",\n};\n\n/**\n * Dark-theme neutral defaults. A monotonic neutral ladder — the three surface\n * tiers are the darkest, borders sit a step lighter so they stay visible\n * against every surface, and text inverts to light. \"Subtle\"/\"strong\" keep\n * their light-mode semantics: `bgSubtle` reads as a lifted hover/inset tint and\n * `borderStrong` is more prominent than `border`.\n */\nconst DARK: Record<string, string> = {\n // ── Surfaces (base → lifted) ──\n \"--color-background-primary\": \"#18181b\",\n \"--color-background-secondary\": \"#27272a\",\n \"--color-background-tertiary\": \"#2f2f34\",\n // ── Text ──\n \"--color-text-primary\": \"#fafafa\",\n \"--color-text-secondary\": \"#a1a1aa\",\n \"--color-text-tertiary\": \"#71717a\",\n \"--color-text-accent\": \"#818cf8\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring (lighter than surfaces so they remain visible) ──\n \"--color-border-primary\": \"#3f3f46\",\n \"--color-border-secondary\": \"#52525b\",\n \"--color-ring-primary\": \"#818cf8\",\n // ── Status / brand semantics (brightened for contrast on dark) ──\n \"--nb-color-danger\": \"#f87171\",\n \"--nb-color-success\": \"#34d399\",\n \"--nb-color-warning\": \"#fbbf24\",\n \"--nb-color-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\n \"--nb-color-processing\": \"#a78bfa\",\n \"--nb-color-processing-light\": \"#2a2440\",\n \"--nb-color-info-light\": \"#1e2a44\",\n};\n\n/**\n * The neutral default theme, keyed by mode. Exported for the regression test\n * that asserts every color var referenced by `tokens` is backed in both modes.\n */\nexport const DEFAULT_THEME_VARS: Record<\"light\" | \"dark\", Record<string, string>> = {\n light: LIGHT,\n dark: DARK,\n};\n\n/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default.\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on every theme change is correct and cheap.\n *\n * Prefer {@link applyTheme}: it applies variables *and* font faces together, so\n * a caller cannot wire up half a theme.\n */\nexport function applyThemeVariables(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\n}\n\n/** The faces this module has added, so a re-apply replaces rather than accumulates. */\nconst managedFaces = new Set<FontFace>();\n\n/** Signature of the currently-applied set — lets a repeat call no-op. */\nlet appliedFontKey = \"\";\n\n/** Stable signature of a face list — the one comparison rule for \"same fonts?\",\n * shared by the sink's re-apply guard and `onThemeChanged`'s equality filter. */\nexport function fontFacesKey(faces: readonly FontFaceDescriptor[] | undefined | null): string {\n if (!Array.isArray(faces)) return \"\";\n return JSON.stringify(faces.map((d) => [d.family, d.src, d.weight, d.style, d.display]));\n}\n\n/**\n * Normalise an untrusted face list into the one shape every layer trusts.\n *\n * THE rule, in one place:\n * - not an array → `undefined` — nothing was said, keep what's loaded\n * - array, none usable → `undefined` — a shape mistake is not a clear\n * - array, some usable → those entries\n * - empty array → `[]` — the explicit clear\n *\n * The wire and the sink each used to spell this out separately and drifted\n * apart, so a host whose descriptors were mis-shaped kept its typeface through\n * one path and lost it through the other. One predicate, one spelling.\n */\nexport function normalizeFontFaces(raw: unknown): FontFaceDescriptor[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const usable = raw.filter(isFontFaceDescriptor);\n if (raw.length > 0 && usable.length === 0) return undefined;\n return usable;\n}\n\n/**\n * Load host-supplied `@font-face`s into the app document.\n *\n * Uses the CSSOM {@link FontFace} constructor rather than building `@font-face`\n * CSS text. That is deliberate, and load-bearing for two reasons:\n *\n * 1. **No CSS-injection sink.** `family` and `src` are separate arguments, not\n * concatenated into a stylesheet, so a host value containing `}` cannot\n * escape the rule and inject arbitrary CSS into the app.\n * 2. **Uniform URL handling.** The `src` descriptor is parsed by the browser\n * against the `src` grammar, so relative (`url('/fonts/x.woff2')`), absolute\n * (`url('https://cdn.example/x.woff2')`) and `data:` forms all just work.\n * A malformed descriptor throws `SyntaxError` here instead of silently\n * producing dead CSS.\n *\n * The SDK ships no font data. A host that sends nothing leaves the web-safe\n * fallbacks in `ui/tokens.ts` in force — which is why omitting fonts entirely is\n * a supported configuration, not a degraded one.\n *\n * Faces are added but not force-loaded: the browser fetches a face when CSS\n * first matches it, so an unused weight costs nothing. `display` defaults to\n * `swap` so text paints in the fallback immediately rather than blocking.\n *\n * SSR-safe, and safe in environments without the CSS Font Loading API (older\n * jsdom): both no-op. Re-applying an unchanged set is a no-op.\n */\nexport function applyThemeFontFaces(faces: readonly FontFaceDescriptor[] | undefined | null): void {\n // `normalizeFontFaces` owns what absent/garbage means. Reached from the public\n // `applyHostTheme`, so the input here is not necessarily typed: a host\n // building `fontFaces` from untrusted JSON can hand us mis-shaped entries,\n // and reading those as \"clear\" would unload the typeface over a typo.\n const next = normalizeFontFaces(faces);\n if (next === undefined) return;\n if (typeof document === \"undefined\") return;\n // `document.fonts` (CSS Font Loading API) is absent in some test DOMs.\n if (!document.fonts || typeof FontFace === \"undefined\") return;\n\n const key = fontFacesKey(next);\n if (key === appliedFontKey) return;\n appliedFontKey = key;\n\n for (const face of managedFaces) {\n try {\n document.fonts.delete(face);\n } catch {\n // Already evicted, or the API rejected the handle — nothing to undo.\n }\n }\n managedFaces.clear();\n\n for (const d of next) {\n try {\n const face = new FontFace(d.family, d.src, {\n weight: d.weight,\n style: d.style,\n // Wire data is untyped: ignore an unrecognised `display` rather than let\n // the constructor reject the descriptor and cost the app this face.\n display: d.display && FONT_DISPLAY_VALUES.has(d.display) ? d.display : \"swap\",\n });\n document.fonts.add(face);\n managedFaces.add(face);\n } catch {\n // Malformed descriptor — skip this face and keep the web-safe fallback.\n // One bad entry must not cost the app the rest of its typography.\n }\n }\n}\n\nfunction isFontFaceDescriptor(value: unknown): value is FontFaceDescriptor {\n if (!value || typeof value !== \"object\") return false;\n const d = value as Partial<FontFaceDescriptor>;\n return typeof d.family === \"string\" && typeof d.src === \"string\";\n}\n\n/**\n * Apply a resolved theme to the app document — CSS variables and font faces.\n *\n * This is the single path by which theming reaches the DOM: the handshake,\n * `host-context-changed`, and the React `<SynapseProvider>` all funnel through\n * here. Keeping colour and typography on one call is the point — two entry\n * points invite a caller to wire one and forget the other, shipping a host's\n * palette under the wrong typeface.\n *\n * Omitting `fontFaces` is safe and means \"leave the loaded faces alone\" (see\n * {@link applyThemeFontFaces}), so a caller re-applying a theme derived from a\n * partial host context cannot silently strip the host's typeface.\n */\nexport function applyTheme(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n fontFaces?: readonly FontFaceDescriptor[] | null,\n): void {\n applyThemeVariables(mode, hostVars);\n applyThemeFontFaces(fontFaces);\n}\n\n/** Test seam — forget applied faces so a fresh apply is observable. */\nexport function resetAppliedFontFaces(): void {\n managedFaces.clear();\n appliedFontKey = \"\";\n}\n","import type { McpUiHostContext, McpUiInitializeResult } from \"@modelcontextprotocol/ext-apps\";\nimport { normalizeFontFaces } from \"./theme-defaults.js\";\nimport type { FontFaceDescriptor, HostInfo, SynapseTheme } from \"./types\";\n\n/**\n * Host-context key carrying `@font-face` descriptors.\n *\n * A NimbleBrain extension (`synapse/` prefix), because the ext-apps spec has no\n * equivalent: `styles.variables` is a flat `Record<McpUiStyleVariableKey, string>`\n * that can name a font family but cannot carry the rule that loads it.\n * `McpUiHostContext` declares `[key: string]: unknown` for forward\n * compatibility, so riding an extra key is spec-legal, and hosts that don't send\n * it degrade to the web-safe token fallbacks.\n */\nexport const FONT_FACES_CONTEXT_KEY = \"synapse/fontFaces\";\n\nconst DEFAULT_THEME: SynapseTheme = {\n mode: \"light\",\n primaryColor: \"#6366f1\",\n tokens: {},\n};\n\n/**\n * Detect the host environment from the ext-apps `ui/initialize` response.\n *\n * Reports identity only (host name, protocol version). Theme lives in the\n * unified host-context state and is read via `extractTheme(hostContext)`\n * — no parallel `theme` field on `HostInfo`.\n *\n * Handles missing or malformed fields gracefully — never throws.\n */\nexport function detectHost(initResponse: unknown): HostInfo {\n const resp = initResponse as Partial<McpUiInitializeResult> | null | undefined;\n\n const hostName = resp?.hostInfo?.name ?? \"unknown\";\n const protocolVersion = resp?.protocolVersion ?? \"unknown\";\n\n return {\n isNimbleBrain: hostName === \"nimblebrain\",\n serverName: hostName,\n protocolVersion,\n };\n}\n\nexport function extractTheme(ctx: Partial<McpUiHostContext> | undefined): SynapseTheme {\n if (!ctx) return { ...DEFAULT_THEME };\n\n // Spec: theme is a string (\"light\" | \"dark\")\n const mode = ctx.theme === \"light\" || ctx.theme === \"dark\" ? ctx.theme : DEFAULT_THEME.mode;\n\n // Spec: tokens live under styles.variables\n const variables = ctx.styles?.variables;\n const tokens =\n variables && typeof variables === \"object\" && !Array.isArray(variables)\n ? (variables as Record<string, string>)\n : {};\n\n // Deliberately no `fontFaces` here. Faces are sticky across a partial context\n // (see `foldFontFaces`), and this function sees only the context in hand — so\n // populating the field would publish a NON-sticky value on the return type and\n // invite `extractTheme(ctx).fontFaces`, which is the unload-on-toggle bug.\n // `resolveTheme` overlays the sticky set; nothing else should.\n return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens };\n}\n\n/**\n * Read the `synapse/fontFaces` host-context extension.\n *\n * `undefined` and `[]` are **distinct answers**, because a host-context-changed\n * carries only the fields that changed:\n * - `undefined` — the host said nothing about fonts, so keep what's loaded.\n * - `[]` — the host explicitly sent an empty list, i.e. drop the faces.\n * Collapsing the two would leave a host no way to clear typography it had\n * previously set.\n *\n * Individual malformed entries are dropped rather than rejecting the batch —\n * one bad face must not cost the app the rest of its typography. Anything that\n * survives is still validated by the browser when the descriptor is built.\n */\nexport function extractFontFaces(\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return normalizeFontFaces(ctx?.[FONT_FACES_CONTEXT_KEY]);\n}\n\n/**\n * THE rule for merging incoming faces over what is already loaded, in one\n * place: **absent — or a batch with nothing usable — means unchanged; an\n * explicit list, including empty, replaces.**\n *\n * Every consumer of the host-context font extension folds through here. The\n * rule leaked one layer at a time when each site spelled it itself, so the\n * spelling now lives once and the call sites just call it.\n */\nexport function foldFontFaces(\n prev: FontFaceDescriptor[] | undefined,\n ctx: Partial<McpUiHostContext> | undefined,\n): FontFaceDescriptor[] | undefined {\n return extractFontFaces(ctx) ?? prev;\n}\n"]} |
| 'use strict'; | ||
| var chunkIA2M4GTK_cjs = require('./chunk-IA2M4GTK.cjs'); | ||
| var extApps = require('@modelcontextprotocol/ext-apps'); | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": extApps.TOOL_RESULT_METHOD, | ||
| "tool-input": extApps.TOOL_INPUT_METHOD, | ||
| "tool-input-partial": extApps.TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": extApps.TOOL_CANCELLED_METHOD, | ||
| "theme-changed": extApps.HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: extApps.RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| extApps.INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| const initialFontFaces = chunkIA2M4GTK_cjs.extractFontFaces(ctx); | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {}, | ||
| ...initialFontFaces ? { fontFaces: initialFontFaces } : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| chunkIA2M4GTK_cjs.applyTheme(currentTheme.mode, currentTheme.tokens, currentTheme.fontFaces); | ||
| } | ||
| } | ||
| transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| const fontFaces = chunkIA2M4GTK_cjs.foldFontFaces(currentTheme.fontFaces, ctx); | ||
| currentTheme = { mode, tokens, ...fontFaces ? { fontFaces } : {} }; | ||
| chunkIA2M4GTK_cjs.applyTheme(mode, tokens, fontFaces); | ||
| const set = handlers.get(extApps.HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([extApps.HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === extApps.TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let currentFontFaces; | ||
| function syncFontFaces(ctx) { | ||
| currentFontFaces = chunkIA2M4GTK_cjs.foldFontFaces(currentFontFaces, ctx); | ||
| return currentFontFaces; | ||
| } | ||
| function resolveTheme(ctx) { | ||
| const theme = chunkIA2M4GTK_cjs.extractTheme(ctx); | ||
| return currentFontFaces ? { ...theme, fontFaces: currentFontFaces } : theme; | ||
| } | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(extApps.INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = chunkIA2M4GTK_cjs.detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| { | ||
| const theme = chunkIA2M4GTK_cjs.extractTheme(currentHostContext); | ||
| chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext)); | ||
| } | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| const theme = chunkIA2M4GTK_cjs.extractTheme(currentHostContext); | ||
| chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, syncFontFaces(currentHostContext)); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return resolveTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? resolveTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = resolveTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| if (chunkIA2M4GTK_cjs.fontFacesKey(a.fontFaces) !== chunkIA2M4GTK_cjs.fontFacesKey(b.fontFaces)) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| exports.connect = connect; | ||
| exports.createSynapse = createSynapse; | ||
| //# sourceMappingURL=chunk-QHS7BJLI.cjs.map | ||
| //# sourceMappingURL=chunk-QHS7BJLI.cjs.map |
Sorry, the diff of this file is too big to display
| 'use strict'; | ||
| var chunkIA2M4GTK_cjs = require('./chunk-IA2M4GTK.cjs'); | ||
| // src/host/theme.ts | ||
| function applyHostTheme(theme) { | ||
| if (typeof document !== "undefined") { | ||
| document.documentElement.setAttribute("data-theme", theme.mode); | ||
| } | ||
| chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, theme.fontFaces); | ||
| } | ||
| function preferredMode(win) { | ||
| try { | ||
| if (win?.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark"; | ||
| } catch { | ||
| } | ||
| return "light"; | ||
| } | ||
| function coerceMode(value, fallback) { | ||
| return value === "light" || value === "dark" ? value : fallback; | ||
| } | ||
| // src/host/types.ts | ||
| var HostUnsupportedError = class extends Error { | ||
| constructor(feature, host) { | ||
| super(`"${feature}" is not supported by the "${host}" host`); | ||
| this.name = "HostUnsupportedError"; | ||
| } | ||
| }; | ||
| var SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| var MCPUI_READY = "ui-lifecycle-iframe-ready"; | ||
| var MCPUI_RENDER_DATA = "ui-lifecycle-iframe-render-data"; | ||
| var MCPUI_SIZE_CHANGE = "ui-size-change"; | ||
| var MCPUI_LINK = "link"; | ||
| var MCPUI_PROMPT = "prompt"; | ||
| var OPENAI_SET_GLOBALS = "openai:set_globals"; | ||
| var MCPAPP_PROTOCOL_VERSION = "2026-01-26"; | ||
| var MCPAPP_INITIALIZE = "ui/initialize"; | ||
| var MCPAPP_INITIALIZED = "ui/notifications/initialized"; | ||
| var MCPAPP_TOOL_RESULT = "ui/notifications/tool-result"; | ||
| var MCPAPP_HOST_CONTEXT_CHANGED = "ui/notifications/host-context-changed"; | ||
| var MCPAPP_SIZE_CHANGED = "ui/notifications/size-changed"; | ||
| var MCPAPP_OPEN_LINK = "ui/open-link"; | ||
| var MCPAPP_MESSAGE = "ui/message"; | ||
| var MCPAPP_TEARDOWN = "ui/resource-teardown"; | ||
| var MCP_TOOLS_CALL = "tools/call"; | ||
| // src/host/adapters/chatgpt.ts | ||
| function createChatGPTAdapter(win, _options) { | ||
| const openai = () => win.openai; | ||
| let currentData = openai()?.toolOutput ?? null; | ||
| let currentTheme = { | ||
| mode: coerceMode(openai()?.theme, "light"), | ||
| tokens: {} | ||
| }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| const onSetGlobals = (event) => { | ||
| if (destroyed) return; | ||
| const globals = event.detail?.globals; | ||
| if (!globals) return; | ||
| if ("toolOutput" in globals && globals.toolOutput != null) { | ||
| currentData = globals.toolOutput; | ||
| for (const cb of dataCbs) cb(currentData); | ||
| } | ||
| if ("theme" in globals && globals.theme != null) { | ||
| const mode = coerceMode(globals.theme, currentTheme.mode); | ||
| if (mode !== currentTheme.mode) { | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| host: "chatgpt", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| const call = openai()?.callTool; | ||
| if (!call) throw new HostUnsupportedError("callTool", "chatgpt"); | ||
| return await call(name, args ?? {}); | ||
| }, | ||
| sendPrompt(text) { | ||
| const o = openai(); | ||
| const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage; | ||
| send?.({ prompt: text }); | ||
| }, | ||
| openLink(url) { | ||
| const open = openai()?.openExternal; | ||
| if (open) open({ href: url }); | ||
| else win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| const o = openai(); | ||
| return { | ||
| pull: typeof o?.callTool === "function", | ||
| sendPrompt: typeof o?.sendFollowUpMessage === "function" || typeof o?.sendFollowupMessage === "function", | ||
| openLink: true | ||
| }; | ||
| }, | ||
| start() { | ||
| win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals, { passive: true }); | ||
| currentData = openai()?.toolOutput ?? currentData; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/data.ts | ||
| function readInlineData(doc, elementId) { | ||
| if (!doc) return null; | ||
| const el = doc.getElementById(elementId); | ||
| const text = el?.textContent; | ||
| if (!text) return null; | ||
| try { | ||
| const parsed = JSON.parse(text); | ||
| return parsed ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function unwrapRenderData(payload) { | ||
| if (payload == null || typeof payload !== "object") return null; | ||
| const rec = payload; | ||
| const nested = rec.renderData; | ||
| const source = nested != null && typeof nested === "object" ? nested : rec; | ||
| if (source.toolOutput != null) return source.toolOutput; | ||
| if (source.structuredContent != null) return source.structuredContent; | ||
| return source; | ||
| } | ||
| // src/host/adapters/inline.ts | ||
| function createInlineAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let media = null; | ||
| const onSchemeChange = (event) => { | ||
| if (destroyed) return; | ||
| const mode = coerceMode(event.matches ? "dark" : "light", currentTheme.mode); | ||
| if (mode === currentTheme.mode) return; | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| }; | ||
| return { | ||
| host: "generic", | ||
| getData: () => currentData, | ||
| onData() { | ||
| return () => { | ||
| }; | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(_name) { | ||
| throw new HostUnsupportedError("callTool", "generic"); | ||
| }, | ||
| sendPrompt() { | ||
| }, | ||
| openLink(url) { | ||
| win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| return { pull: false, sendPrompt: false, openLink: true }; | ||
| }, | ||
| start() { | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| try { | ||
| media = win.matchMedia?.("(prefers-color-scheme: dark)") ?? null; | ||
| media?.addEventListener?.("change", onSchemeChange); | ||
| } catch { | ||
| media = null; | ||
| } | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| media?.removeEventListener?.("change", onSchemeChange); | ||
| media = null; | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/adapters/mcpapps.ts | ||
| var REQUEST_TIMEOUT_MS = 3e4; | ||
| function createMcpAppsAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| const autoResize = options.autoResize !== false; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let standardConfirmed = false; | ||
| let nextId = 1; | ||
| const pending = /* @__PURE__ */ new Map(); | ||
| let lastReportedHeight = -1; | ||
| let resizeObserver = null; | ||
| let onWindowResize = null; | ||
| const parent = () => win.parent ?? win; | ||
| function post(message) { | ||
| parent().postMessage(message, "*"); | ||
| } | ||
| function postLegacy(message) { | ||
| if (standardConfirmed) return; | ||
| post(message); | ||
| } | ||
| function notify(method, params) { | ||
| post({ jsonrpc: "2.0", method, params: params ?? {} }); | ||
| } | ||
| function request(method, params) { | ||
| const id = nextId++; | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| pending.delete(id); | ||
| reject(new Error(`"${method}" timed out`)); | ||
| }, REQUEST_TIMEOUT_MS); | ||
| pending.set(id, { resolve, reject, timer }); | ||
| post({ jsonrpc: "2.0", id, method, params: params ?? {} }); | ||
| }); | ||
| } | ||
| function emitData(next) { | ||
| if (next == null) return; | ||
| currentData = next; | ||
| for (const cb of dataCbs) cb(next); | ||
| } | ||
| function applyHostContext(ctx) { | ||
| if (!ctx || typeof ctx !== "object") return; | ||
| let { mode, tokens, fontFaces } = currentTheme; | ||
| let changed = false; | ||
| if (ctx.theme != null) { | ||
| const next = coerceMode(ctx.theme, mode); | ||
| if (next !== mode) { | ||
| mode = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| const styles = ctx.styles; | ||
| if (styles?.variables && typeof styles.variables === "object") { | ||
| tokens = { ...tokens, ...styles.variables }; | ||
| changed = true; | ||
| } | ||
| const nextFaces = chunkIA2M4GTK_cjs.foldFontFaces(fontFaces, ctx); | ||
| if (chunkIA2M4GTK_cjs.fontFacesKey(nextFaces) !== chunkIA2M4GTK_cjs.fontFacesKey(fontFaces)) { | ||
| fontFaces = nextFaces; | ||
| changed = true; | ||
| } | ||
| if (changed) { | ||
| currentTheme = { mode, tokens, ...fontFaces ? { fontFaces } : {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| function reportSize(height) { | ||
| if (destroyed) return; | ||
| const h = typeof height === "number" ? height : Math.ceil(win.document.body.scrollHeight); | ||
| if (h === lastReportedHeight) return; | ||
| lastReportedHeight = h; | ||
| notify(MCPAPP_SIZE_CHANGED, { height: h }); | ||
| postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } }); | ||
| } | ||
| function handleResponse(d) { | ||
| const id = Number(d.id); | ||
| const p = pending.get(id); | ||
| if (!p) return; | ||
| pending.delete(id); | ||
| clearTimeout(p.timer); | ||
| if (d.error != null) { | ||
| const err = d.error; | ||
| p.reject(new Error(err.message ?? "request failed")); | ||
| } else { | ||
| p.resolve(d.result); | ||
| } | ||
| } | ||
| function handleNotification(method, params) { | ||
| if (method === MCPAPP_TOOL_RESULT) { | ||
| const structured = params.structuredContent; | ||
| emitData(structured != null ? structured : unwrapRenderData(params)); | ||
| } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) { | ||
| applyHostContext(params); | ||
| } | ||
| } | ||
| function handleRequest(d) { | ||
| if (d.method === MCPAPP_TEARDOWN) { | ||
| post({ jsonrpc: "2.0", id: d.id, result: {} }); | ||
| } | ||
| } | ||
| function handleLegacy(d) { | ||
| if (d.type === MCPUI_RENDER_DATA || d.type === "renderData") { | ||
| const { theme, ...rest } = d.payload ?? {}; | ||
| if (theme != null) applyHostContext({ theme }); | ||
| if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest)); | ||
| } | ||
| } | ||
| const onMessage = (event) => { | ||
| if (destroyed) return; | ||
| if (event.source && event.source !== parent()) return; | ||
| const d = event.data; | ||
| if (!d || typeof d !== "object") return; | ||
| if (d.jsonrpc !== "2.0") { | ||
| handleLegacy(d); | ||
| return; | ||
| } | ||
| if (d.id != null && ("result" in d || "error" in d)) { | ||
| handleResponse(d); | ||
| } else if (typeof d.method === "string") { | ||
| if (d.id != null) handleRequest(d); | ||
| else handleNotification(d.method, d.params ?? {}); | ||
| } | ||
| }; | ||
| function setupResize() { | ||
| onWindowResize = () => reportSize(); | ||
| win.addEventListener("resize", onWindowResize); | ||
| if (typeof win.ResizeObserver !== "undefined") { | ||
| resizeObserver = new win.ResizeObserver(() => reportSize()); | ||
| resizeObserver.observe(win.document.body); | ||
| } | ||
| } | ||
| return { | ||
| host: "claude", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| return await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} }); | ||
| }, | ||
| sendPrompt(text) { | ||
| void request(MCPAPP_MESSAGE, { role: "user", content: [{ type: "text", text }] }).catch( | ||
| () => { | ||
| } | ||
| ); | ||
| postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } }); | ||
| }, | ||
| openLink(url) { | ||
| void request(MCPAPP_OPEN_LINK, { url }).catch(() => { | ||
| }); | ||
| postLegacy({ type: MCPUI_LINK, payload: { url } }); | ||
| }, | ||
| resize(height) { | ||
| reportSize(height); | ||
| }, | ||
| capabilities() { | ||
| return { pull: true, sendPrompt: true, openLink: true }; | ||
| }, | ||
| start() { | ||
| win.addEventListener("message", onMessage); | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| if (autoResize) setupResize(); | ||
| post({ type: MCPUI_READY }); | ||
| request(MCPAPP_INITIALIZE, { | ||
| appInfo: { name: options.name ?? "synapse-ui", version: options.version ?? "0.0.0" }, | ||
| appCapabilities: { availableDisplayModes: ["inline"] }, | ||
| protocolVersion: MCPAPP_PROTOCOL_VERSION | ||
| }).then((result) => { | ||
| if (destroyed) return; | ||
| standardConfirmed = true; | ||
| applyHostContext(result?.hostContext); | ||
| notify(MCPAPP_INITIALIZED, {}); | ||
| lastReportedHeight = -1; | ||
| reportSize(); | ||
| }).catch(() => { | ||
| }); | ||
| reportSize(); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener("message", onMessage); | ||
| if (onWindowResize) win.removeEventListener("resize", onWindowResize); | ||
| onWindowResize = null; | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| for (const p of pending.values()) { | ||
| clearTimeout(p.timer); | ||
| p.reject(new Error("adapter destroyed")); | ||
| } | ||
| pending.clear(); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/detect.ts | ||
| function detectHostKind(win) { | ||
| if (win.openai != null) return "chatgpt"; | ||
| try { | ||
| if (win.parent != null && win.parent !== win) return "claude"; | ||
| } catch { | ||
| return "claude"; | ||
| } | ||
| return "generic"; | ||
| } | ||
| function adapterForKind(kind, win, options) { | ||
| switch (kind) { | ||
| case "chatgpt": | ||
| return createChatGPTAdapter(win); | ||
| case "claude": | ||
| case "nimblebrain": | ||
| return createMcpAppsAdapter(win, options); | ||
| default: | ||
| return createInlineAdapter(win, options); | ||
| } | ||
| } | ||
| function selectAdapter(win, options) { | ||
| const kind = options.host ?? detectHostKind(win); | ||
| return adapterForKind(kind, win, options); | ||
| } | ||
| // src/host/connect.ts | ||
| function connectUI(options = {}) { | ||
| const win = options.window ?? globalThis; | ||
| const adapter = selectAdapter(win, options); | ||
| applyHostTheme(adapter.getTheme()); | ||
| const unsubTheme = adapter.onTheme(applyHostTheme); | ||
| adapter.start(); | ||
| let destroyed = false; | ||
| return { | ||
| data: () => adapter.getData(), | ||
| onData: (cb) => adapter.onData(cb), | ||
| theme: () => adapter.getTheme(), | ||
| onTheme: (cb) => adapter.onTheme(cb), | ||
| callTool: (name, args) => adapter.callTool(name, args), | ||
| sendPrompt: (text) => adapter.sendPrompt(text), | ||
| openLink: (url) => adapter.openLink(url), | ||
| resize: (height) => adapter.resize(height), | ||
| capabilities: () => adapter.capabilities(), | ||
| host: () => adapter.host, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| unsubTheme(); | ||
| adapter.destroy(); | ||
| } | ||
| }; | ||
| } | ||
| exports.HostUnsupportedError = HostUnsupportedError; | ||
| exports.SYNAPSE_DATA_ELEMENT_ID = SYNAPSE_DATA_ELEMENT_ID; | ||
| exports.adapterForKind = adapterForKind; | ||
| exports.applyHostTheme = applyHostTheme; | ||
| exports.coerceMode = coerceMode; | ||
| exports.connectUI = connectUI; | ||
| exports.createChatGPTAdapter = createChatGPTAdapter; | ||
| exports.createInlineAdapter = createInlineAdapter; | ||
| exports.createMcpAppsAdapter = createMcpAppsAdapter; | ||
| exports.detectHostKind = detectHostKind; | ||
| exports.preferredMode = preferredMode; | ||
| exports.selectAdapter = selectAdapter; | ||
| //# sourceMappingURL=chunk-VMUZMVKW.cjs.map | ||
| //# sourceMappingURL=chunk-VMUZMVKW.cjs.map |
| {"version":3,"sources":["../src/host/theme.ts","../src/host/types.ts","../src/host/adapters/chatgpt.ts","../src/host/data.ts","../src/host/adapters/inline.ts","../src/host/adapters/mcpapps.ts","../src/host/detect.ts","../src/host/connect.ts"],"names":["applyTheme","foldFontFaces","fontFacesKey"],"mappings":"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","file":"chunk-VMUZMVKW.cjs","sourcesContent":["import { applyTheme } from \"../theme-defaults.js\";\nimport type { SynapseUITheme } from \"./types.js\";\n\n/**\n * Apply a resolved theme to the DOM for the cross-host client.\n *\n * Two conventions coexist across Synapse components, so the client drives both:\n *\n * - `document.documentElement[data-theme=\"light\"|\"dark\"]` — how self-contained\n * HTML components (Bassethound's report) gate their `--var` palettes, and the\n * lever a host's light/dark signal actually flips.\n * - CSS custom properties via {@link applyTheme} — how the\n * `@nimblebrain/synapse/ui` token components consume theme, backed by the\n * SDK's neutral defaults so every referenced var resolves in both modes.\n *\n * Setting both means an app can use either convention (or a host that supplies\n * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe.\n *\n * Any `fontFaces` the host supplies are loaded here too — a token can name a\n * family but not load it, so the two travel together. A host that sends none\n * leaves the web-safe fallbacks in force.\n */\nexport function applyHostTheme(theme: SynapseUITheme): void {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"data-theme\", theme.mode);\n }\n applyTheme(theme.mode, theme.tokens, theme.fontFaces);\n}\n\n/** Read the OS-level color scheme as a sane default for hosts that don't push a\n * theme until later (mcp-ui) or ever (standalone). */\nexport function preferredMode(win: Window | undefined): \"light\" | \"dark\" {\n try {\n if (win?.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) return \"dark\";\n } catch {\n // matchMedia unavailable (older test envs) — fall through to light.\n }\n return \"light\";\n}\n\n/** Coerce an arbitrary host-supplied theme signal to a mode. Accepts the string\n * form (`\"dark\"`) both hosts use; anything else falls back. */\nexport function coerceMode(value: unknown, fallback: \"light\" | \"dark\"): \"light\" | \"dark\" {\n return value === \"light\" || value === \"dark\" ? value : fallback;\n}\n","/**\n * Cross-host UI client — types and wire constants.\n *\n * The cross-host client (`connectUI`) renders one Synapse-authored component in\n * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude\n * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch\n * a host protocol. This is a **push-first** surface: the tool output that spawned\n * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the\n * pull escape hatch, advertised per host via `capabilities()`.\n *\n * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the\n * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so\n * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas).\n */\n\n/**\n * The host the client resolved to, as reported by `synapse.host()`. An escape\n * hatch — apps should rarely branch on it; `capabilities()` is the supported way\n * to feature-detect. `\"nimblebrain\"` is reserved for the runtime adapter (P3).\n */\nimport type { FontFaceDescriptor } from \"../types.js\";\n\nexport type HostKind = \"chatgpt\" | \"claude\" | \"nimblebrain\" | \"generic\";\n\n/** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS\n * custom properties the host publishes — the MCP Apps adapter reads them from\n * `hostContext.styles.variables`; where a host sends none they stay empty and the\n * SDK's neutral defaults back them. */\nexport interface SynapseUITheme {\n mode: \"light\" | \"dark\";\n tokens: Record<string, string>;\n /** Font faces the host wants loaded. Absent where the host sends none, which\n * leaves the SDK's web-safe fallbacks in force. See `FontFaceDescriptor`. */\n fontFaces?: FontFaceDescriptor[];\n}\n\n/** What the active host actually supports. `data()`/`onData()`/`theme()`/\n * `resize()` work everywhere; these three vary. */\nexport interface HostCapabilities {\n /** `callTool()` can reach the server (widget→server fetch). */\n pull: boolean;\n /** `sendPrompt()` reaches the agent conversation. */\n sendPrompt: boolean;\n /** `openLink()` opens an external URL through the host. */\n openLink: boolean;\n}\n\n/** Thrown by `callTool()` when the active host offers no widget→server call. */\nexport class HostUnsupportedError extends Error {\n constructor(feature: string, host: HostKind) {\n super(`\"${feature}\" is not supported by the \"${host}\" host`);\n this.name = \"HostUnsupportedError\";\n }\n}\n\nexport interface ConnectUIOptions {\n /** App name — informational; forwarded to hosts that accept an appInfo. */\n name?: string;\n /** App semver — informational. */\n version?: string;\n /**\n * Force a host adapter instead of auto-detecting. Used by preview harnesses,\n * SSR, and tests; production apps omit it and let the SDK feature-detect.\n * `\"claude\"` → MCP Apps standard adapter, `\"chatgpt\"` → OpenAI Apps adapter,\n * `\"generic\"` → inline adapter.\n */\n host?: HostKind;\n /**\n * `id` of the `<script type=\"application/json\">` element carrying pushed data\n * baked into the HTML (the mcp-ui / SSR path). Defaults to\n * {@link SYNAPSE_DATA_ELEMENT_ID}.\n */\n dataElementId?: string;\n /**\n * Auto-report content height to the host on layout changes (mcp-ui only).\n * Defaults to `true`. Set `false` to size manually via `resize()`.\n */\n autoResize?: boolean;\n /** Window to bind to. Defaults to the global `window`. Injectable for tests. */\n window?: Window & typeof globalThis;\n}\n\n/**\n * The public cross-host client. Bound to `synapse` by convention.\n *\n * ```ts\n * const synapse = connectUI();\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (may be null → empty state)\n * ```\n */\nexport interface SynapseUIClient {\n /** The current pushed data, or `null` before anything has been delivered. */\n data<T = unknown>(): T | null;\n /** Subscribe to data updates (NOT replayed — read `data()` for the current\n * value). Returns an unsubscribe. */\n onData<T = unknown>(cb: (data: T) => void): () => void;\n\n /** The current resolved theme. */\n theme(): SynapseUITheme;\n /** Subscribe to theme changes. The client already applies the theme to the DOM\n * (`data-theme` attribute + CSS variables) before this fires. */\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n\n /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the\n * host advertises no pull (`capabilities().pull === false`). */\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n /** Send a follow-up message to the agent conversation. No-op where unsupported. */\n sendPrompt(text: string): void;\n /** Open an external URL through the host (falls back to `window.open`). */\n openLink(url: string): void;\n /** Report content height to the host. Omit `height` to measure `document.body`. */\n resize(height?: number): void;\n\n /** What the active host supports. */\n capabilities(): HostCapabilities;\n /** The resolved host — an escape hatch; prefer `capabilities()`. */\n host(): HostKind;\n /** Tear down listeners/observers. */\n destroy(): void;\n}\n\n/**\n * Internal adapter contract. One per host bridge; the client is a thin façade\n * over the selected adapter.\n */\nexport interface HostAdapter {\n readonly host: HostKind;\n getData<T = unknown>(): T | null;\n onData<T = unknown>(cb: (data: T) => void): () => void;\n getTheme(): SynapseUITheme;\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n sendPrompt(text: string): void;\n openLink(url: string): void;\n resize(height?: number): void;\n capabilities(): HostCapabilities;\n /** Begin listening / send the ready handshake / read baked-in data. Called once\n * by `connectUI` synchronously so `getData()` is populated on return. */\n start(): void;\n destroy(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Wire constants — the bridge message shapes each host speaks. Centralized so a\n// host protocol tweak is a one-line change, not a grep-and-pray.\n// ---------------------------------------------------------------------------\n\n/** Default `id` of the baked-in data `<script type=\"application/json\">`. */\nexport const SYNAPSE_DATA_ELEMENT_ID = \"synapse-ui-data\";\n\n/** mcp-ui iframe-lifecycle messages (child → host, host → child). */\nexport const MCPUI_READY = \"ui-lifecycle-iframe-ready\";\nexport const MCPUI_RENDER_DATA = \"ui-lifecycle-iframe-render-data\";\nexport const MCPUI_SIZE_CHANGE = \"ui-size-change\";\n/** mcp-ui action messages (child → host). */\nexport const MCPUI_LINK = \"link\";\nexport const MCPUI_PROMPT = \"prompt\";\n\n/** OpenAI Apps SDK globals-broadcast event (host → child). */\nexport const OPENAI_SET_GLOBALS = \"openai:set_globals\";\n\n// ---------------------------------------------------------------------------\n// MCP Apps standard (SEP-1865) — the convergence bridge, primary for Claude\n// Desktop and the NimbleBrain runtime. The View iframe is an MCP client and the\n// host an MCP server; they exchange raw JSON-RPC 2.0 objects over `postMessage`\n// (no wrapper envelope). Method names are canonical to the ext-apps spec.\n// ---------------------------------------------------------------------------\n\n/** Protocol version exchanged in the `ui/initialize` handshake. */\nexport const MCPAPP_PROTOCOL_VERSION = \"2026-01-26\";\n/** View → host handshake request; the result carries the host context. */\nexport const MCPAPP_INITIALIZE = \"ui/initialize\";\n/** View → host notification sent once after the init result — the host holds all\n * pushes until it arrives. */\nexport const MCPAPP_INITIALIZED = \"ui/notifications/initialized\";\n/** Host → view notification carrying the tool output. `params` IS the\n * `CallToolResult` (data at `params.structuredContent`) — no `result` wrapper. */\nexport const MCPAPP_TOOL_RESULT = \"ui/notifications/tool-result\";\n/** Host → view notification carrying a partial host context (theme, styles, …). */\nexport const MCPAPP_HOST_CONTEXT_CHANGED = \"ui/notifications/host-context-changed\";\n/** View → host notification reporting intrinsic size (`{ width?, height? }`). A\n * host may keep the frame hidden until it receives one, so it is sent promptly. */\nexport const MCPAPP_SIZE_CHANGED = \"ui/notifications/size-changed\";\n/** View → host request: open an external URL (`{ url }`). */\nexport const MCPAPP_OPEN_LINK = \"ui/open-link\";\n/** View → host request: send a follow-up to the conversation\n * (`{ role: \"user\", content: ContentBlock[] }`). */\nexport const MCPAPP_MESSAGE = \"ui/message\";\n/** Host → view request: graceful teardown; the view replies `{}`. */\nexport const MCPAPP_TEARDOWN = \"ui/resource-teardown\";\n/** Standard MCP method the View may call over the same channel (pull). */\nexport const MCP_TOOLS_CALL = \"tools/call\";\n","import { coerceMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n OPENAI_SET_GLOBALS,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * The subset of the OpenAI Apps SDK `window.openai` surface this adapter uses.\n * All members are optional — the SDK version a host ships may not carry every\n * one, so each call is guarded and degraded.\n */\ninterface OpenAiHost {\n toolOutput?: unknown;\n theme?: unknown;\n callTool?: (name: string, args?: Record<string, unknown>) => Promise<unknown>;\n sendFollowUpMessage?: (arg: { prompt: string }) => void;\n /** Older casing seen in the wild — tried as a fallback. */\n sendFollowupMessage?: (arg: { prompt: string }) => void;\n openExternal?: (arg: { href: string }) => void;\n}\n\ninterface SetGlobalsDetail {\n globals?: { toolOutput?: unknown; theme?: unknown };\n}\n\n/**\n * ChatGPT (OpenAI Apps SDK) adapter.\n *\n * Data arrives on `window.openai.toolOutput` (available synchronously before the\n * widget script runs) and updates via the `openai:set_globals` event. Theme is a\n * mode string on the same surface. The host auto-sizes the iframe, so `resize()`\n * is a no-op. `callTool` is available when the host exposes `window.openai.callTool`.\n */\nexport function createChatGPTAdapter(\n win: Window & typeof globalThis,\n _options: ConnectUIOptions,\n): HostAdapter {\n const openai = () => (win as unknown as { openai?: OpenAiHost }).openai;\n\n let currentData: unknown = openai()?.toolOutput ?? null;\n let currentTheme: SynapseUITheme = {\n mode: coerceMode(openai()?.theme, \"light\"),\n tokens: {},\n };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n\n const onSetGlobals = (event: Event) => {\n if (destroyed) return;\n const globals = (event as CustomEvent<SetGlobalsDetail>).detail?.globals;\n if (!globals) return;\n if (\"toolOutput\" in globals && globals.toolOutput != null) {\n currentData = globals.toolOutput;\n for (const cb of dataCbs) cb(currentData);\n }\n if (\"theme\" in globals && globals.theme != null) {\n const mode = coerceMode(globals.theme, currentTheme.mode);\n if (mode !== currentTheme.mode) {\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n };\n\n return {\n host: \"chatgpt\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n const call = openai()?.callTool;\n if (!call) throw new HostUnsupportedError(\"callTool\", \"chatgpt\");\n return (await call(name, args ?? {})) as O;\n },\n sendPrompt(text: string) {\n const o = openai();\n const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage;\n send?.({ prompt: text });\n },\n openLink(url: string) {\n const open = openai()?.openExternal;\n if (open) open({ href: url });\n else win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // ChatGPT auto-sizes the widget iframe — nothing to report.\n },\n capabilities(): HostCapabilities {\n const o = openai();\n return {\n pull: typeof o?.callTool === \"function\",\n sendPrompt:\n typeof o?.sendFollowUpMessage === \"function\" ||\n typeof o?.sendFollowupMessage === \"function\",\n openLink: true,\n };\n },\n start() {\n win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener, { passive: true });\n // Re-read in case the host mutated globals between construction and start.\n currentData = openai()?.toolOutput ?? currentData;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener);\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","/**\n * Read data baked into the HTML as `<script type=\"application/json\" id=…>`.\n *\n * This is how the mcp-ui embedded-resource path and SSR/standalone deliver the\n * pushed tool output with no round-trip: the server helper escapes the payload\n * and substitutes it into the element (see the Python `SynapseUI.render_html`).\n * A data-free template leaves the marker comment in place, which fails\n * `JSON.parse` and reads back as `null` — exactly the \"no data yet\" state.\n */\nexport function readInlineData<T = unknown>(\n doc: Document | undefined,\n elementId: string,\n): T | null {\n if (!doc) return null;\n const el = doc.getElementById(elementId);\n const text = el?.textContent;\n if (!text) return null;\n try {\n const parsed = JSON.parse(text);\n return (parsed ?? null) as T | null;\n } catch {\n // Unreplaced marker or malformed blob — treat as no data.\n return null;\n }\n}\n\n/**\n * Pull the app payload out of an arbitrary render-data envelope. Hosts wrap the\n * tool output differently (`renderData`, `toolOutput`, `structuredContent`, or\n * the bare object), so unwrap the known envelope keys, else pass the object\n * through. Kept framework-generic — no app-specific keys.\n */\nexport function unwrapRenderData<T = unknown>(payload: unknown): T | null {\n if (payload == null || typeof payload !== \"object\") return null;\n const rec = payload as Record<string, unknown>;\n const nested = rec.renderData;\n const source =\n nested != null && typeof nested === \"object\" ? (nested as Record<string, unknown>) : rec;\n if (source.toolOutput != null) return source.toolOutput as T;\n if (source.structuredContent != null) return source.structuredContent as T;\n // No bare `data` envelope key — too ambiguous with an app's own `data` field.\n return source as T;\n}\n","import { readInlineData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * Inline / standalone fallback adapter.\n *\n * No live host bridge — used for SSR, previews, and a plain browser render. Data\n * comes only from the baked-in `<script type=\"application/json\">` blob; there is\n * nothing to push, so `onData` never fires after `start()`. Theme follows the OS\n * color scheme and tracks `prefers-color-scheme` changes. `openLink` opens a new\n * tab; `sendPrompt`/`callTool` have no destination.\n */\nexport function createInlineAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n let media: MediaQueryList | null = null;\n\n const onSchemeChange = (event: MediaQueryListEvent) => {\n if (destroyed) return;\n const mode = coerceMode(event.matches ? \"dark\" : \"light\", currentTheme.mode);\n if (mode === currentTheme.mode) return;\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n };\n\n return {\n host: \"generic\",\n getData: <T>() => currentData as T | null,\n onData() {\n // Static render — no updates after the initial baked-in read.\n return () => {};\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(_name: string): Promise<O> {\n throw new HostUnsupportedError(\"callTool\", \"generic\");\n },\n sendPrompt() {\n // No agent to reach in a standalone render.\n },\n openLink(url: string) {\n win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // No host to size for.\n },\n capabilities(): HostCapabilities {\n return { pull: false, sendPrompt: false, openLink: true };\n },\n start() {\n currentData = readInlineData(win.document, dataElementId);\n try {\n media = win.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n media?.addEventListener?.(\"change\", onSchemeChange);\n } catch {\n media = null;\n }\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n media?.removeEventListener?.(\"change\", onSchemeChange);\n media = null;\n themeCbs.clear();\n },\n };\n}\n","// `detection` and `theme-defaults` are type-only against `@modelcontextprotocol/*`,\n// so importing them keeps this adapter free of the ext-apps runtime (and Zod)\n// that the lean `window.SynapseUI` IIFE deliberately excludes.\nimport { foldFontFaces } from \"../../detection.js\";\nimport { fontFacesKey } from \"../../theme-defaults.js\";\nimport { readInlineData, unwrapRenderData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n MCP_TOOLS_CALL,\n MCPAPP_HOST_CONTEXT_CHANGED,\n MCPAPP_INITIALIZE,\n MCPAPP_INITIALIZED,\n MCPAPP_MESSAGE,\n MCPAPP_OPEN_LINK,\n MCPAPP_PROTOCOL_VERSION,\n MCPAPP_SIZE_CHANGED,\n MCPAPP_TEARDOWN,\n MCPAPP_TOOL_RESULT,\n MCPUI_LINK,\n MCPUI_PROMPT,\n MCPUI_READY,\n MCPUI_RENDER_DATA,\n MCPUI_SIZE_CHANGE,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/** How long a widget→host request (`callTool`, `openLink`, `sendPrompt`) waits\n * for its response before rejecting, so a silent host never hangs a promise. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * MCP Apps standard (SEP-1865) adapter — the convergence bridge for Claude\n * Desktop and other MCP Apps hosts.\n *\n * The View iframe is an MCP client speaking JSON-RPC 2.0 to `window.parent`:\n *\n * 1. posts `ui/initialize` and awaits the host context (theme, style variables);\n * 2. posts `ui/notifications/initialized`, then a `size-changed` — a host keeps\n * the frame hidden until it has both the handshake and a size;\n * 3. receives data via `ui/notifications/tool-result` (`params` IS the\n * `CallToolResult`, so data is at `params.structuredContent`) and theme via\n * `ui/notifications/host-context-changed`.\n *\n * Actions go up as requests: `ui/open-link`, `ui/message` (follow-up), and\n * `tools/call` (pull). The legacy mcp-ui `ui-lifecycle-*` messages are sent and\n * accepted alongside so a pre-standard host still renders — a standard host drops\n * the non-JSON-RPC frames, and a legacy host ignores the JSON-RPC ones.\n */\nexport function createMcpAppsAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n const autoResize = options.autoResize !== false;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n // Set once `ui/initialize` resolves — proof we're on an MCP Apps standard host,\n // which lets us stop mirroring actions to the legacy shim.\n let standardConfirmed = false;\n let nextId = 1;\n const pending = new Map<number, PendingRequest>();\n let lastReportedHeight = -1;\n let resizeObserver: ResizeObserver | null = null;\n let onWindowResize: (() => void) | null = null;\n\n const parent = () => win.parent ?? win;\n\n function post(message: Record<string, unknown>): void {\n parent().postMessage(message, \"*\");\n }\n\n // The legacy mcp-ui frames (render-data in, size/link/prompt out) are a\n // transitional shim for the one host that still speaks it — the NimbleBrain\n // runtime, which shares this adapter via the `nimblebrain` kind until the P3\n // `nimblebrain` adapter lands and this shim is removed. Once the handshake\n // confirms a standard host we stop mirroring, so a host that understood both\n // dialects never acts on an action twice.\n function postLegacy(message: Record<string, unknown>): void {\n if (standardConfirmed) return;\n post(message);\n }\n\n function notify(method: string, params?: Record<string, unknown>): void {\n post({ jsonrpc: \"2.0\", method, params: params ?? {} });\n }\n\n function request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`\"${method}\" timed out`));\n }, REQUEST_TIMEOUT_MS);\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });\n post({ jsonrpc: \"2.0\", id, method, params: params ?? {} });\n });\n }\n\n function emitData(next: unknown): void {\n if (next == null) return;\n currentData = next;\n for (const cb of dataCbs) cb(next);\n }\n\n /** Merge a full or partial host context into the resolved theme (mode, tokens,\n * font faces). Fonts ride the `synapse/fontFaces` extension: absent means\n * unchanged, an explicit (possibly empty) list replaces. ChatGPT supplies\n * only a mode string, so this is the adapter where host typography arrives. */\n function applyHostContext(ctx: Record<string, unknown> | null | undefined): void {\n if (!ctx || typeof ctx !== \"object\") return;\n let { mode, tokens, fontFaces } = currentTheme;\n let changed = false;\n if (ctx.theme != null) {\n const next = coerceMode(ctx.theme, mode);\n if (next !== mode) {\n mode = next;\n changed = true;\n }\n }\n const styles = ctx.styles as { variables?: Record<string, string> } | undefined;\n if (styles?.variables && typeof styles.variables === \"object\") {\n tokens = { ...tokens, ...styles.variables };\n changed = true;\n }\n const nextFaces = foldFontFaces(fontFaces, ctx);\n if (fontFacesKey(nextFaces) !== fontFacesKey(fontFaces)) {\n fontFaces = nextFaces;\n changed = true;\n }\n if (changed) {\n currentTheme = { mode, tokens, ...(fontFaces ? { fontFaces } : {}) };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n\n function reportSize(height?: number): void {\n if (destroyed) return;\n const h = typeof height === \"number\" ? height : Math.ceil(win.document.body.scrollHeight);\n if (h === lastReportedHeight) return;\n lastReportedHeight = h;\n notify(MCPAPP_SIZE_CHANGED, { height: h });\n postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } });\n }\n\n function handleResponse(d: Record<string, unknown>): void {\n // Normalize the echoed id: JSON-RPC requires a same-type echo, but a lax host\n // that returned \"1\" for 1 would otherwise miss the numeric-keyed pending map.\n const id = Number(d.id);\n const p = pending.get(id);\n if (!p) return;\n pending.delete(id);\n clearTimeout(p.timer);\n if (d.error != null) {\n const err = d.error as { message?: string };\n p.reject(new Error(err.message ?? \"request failed\"));\n } else {\n p.resolve(d.result);\n }\n }\n\n function handleNotification(method: string, params: Record<string, unknown>): void {\n if (method === MCPAPP_TOOL_RESULT) {\n // `params` is the CallToolResult: the render data lives at structuredContent.\n const structured = params.structuredContent;\n emitData(structured != null ? structured : unwrapRenderData(params));\n } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) {\n applyHostContext(params);\n }\n }\n\n function handleRequest(d: Record<string, unknown>): void {\n // The one host→view request we honor: acknowledge teardown so the host can\n // dispose the frame cleanly.\n if (d.method === MCPAPP_TEARDOWN) {\n post({ jsonrpc: \"2.0\", id: d.id, result: {} });\n }\n }\n\n /** Legacy mcp-ui render-data (non-JSON-RPC) — a pre-standard host's data path. */\n function handleLegacy(d: Record<string, unknown>): void {\n if (d.type === MCPUI_RENDER_DATA || d.type === \"renderData\") {\n const { theme, ...rest } = (d.payload ?? {}) as Record<string, unknown>;\n if (theme != null) applyHostContext({ theme });\n if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest));\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n if (destroyed) return;\n // Accept only frames from the host window when the browser sets a source.\n if (event.source && event.source !== parent()) return;\n const d = event.data as Record<string, unknown> | null | undefined;\n if (!d || typeof d !== \"object\") return;\n if (d.jsonrpc !== \"2.0\") {\n handleLegacy(d);\n return;\n }\n if (d.id != null && (\"result\" in d || \"error\" in d)) {\n handleResponse(d);\n } else if (typeof d.method === \"string\") {\n if (d.id != null) handleRequest(d);\n else handleNotification(d.method, (d.params ?? {}) as Record<string, unknown>);\n }\n };\n\n function setupResize(): void {\n onWindowResize = () => reportSize();\n win.addEventListener(\"resize\", onWindowResize);\n if (typeof win.ResizeObserver !== \"undefined\") {\n resizeObserver = new win.ResizeObserver(() => reportSize());\n resizeObserver.observe(win.document.body);\n }\n }\n\n return {\n host: \"claude\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n return (await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} })) as O;\n },\n sendPrompt(text: string) {\n // Standard follow-up (ack ignored); legacy mirror for a pre-standard host.\n void request(MCPAPP_MESSAGE, { role: \"user\", content: [{ type: \"text\", text }] }).catch(\n () => {},\n );\n postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } });\n },\n openLink(url: string) {\n void request(MCPAPP_OPEN_LINK, { url }).catch(() => {});\n postLegacy({ type: MCPUI_LINK, payload: { url } });\n },\n resize(height?: number) {\n reportSize(height);\n },\n capabilities(): HostCapabilities {\n // The MCP Apps standard host answers `tools/call` over this bridge, so pull\n // is advertised. A legacy-only host that shares this adapter (nimblebrain,\n // pre-P3) does not, so there callTool rejects only after REQUEST_TIMEOUT_MS\n // rather than failing fast.\n return { pull: true, sendPrompt: true, openLink: true };\n },\n start() {\n win.addEventListener(\"message\", onMessage as EventListener);\n\n // Baked-in data → available on first paint, before the handshake resolves.\n currentData = readInlineData(win.document, dataElementId);\n\n if (autoResize) setupResize();\n\n // Legacy mcp-ui ready (a standard host drops this non-JSON-RPC frame).\n post({ type: MCPUI_READY });\n\n // MCP Apps standard handshake. A legacy-only host never answers, so the\n // legacy render-data path (handleLegacy) still feeds the widget.\n request<{ hostContext?: Record<string, unknown> }>(MCPAPP_INITIALIZE, {\n appInfo: { name: options.name ?? \"synapse-ui\", version: options.version ?? \"0.0.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\"] },\n protocolVersion: MCPAPP_PROTOCOL_VERSION,\n })\n .then((result) => {\n if (destroyed) return;\n standardConfirmed = true;\n applyHostContext(result?.hostContext);\n notify(MCPAPP_INITIALIZED, {});\n // A host keeps the frame hidden until it gets a size after init — force one.\n lastReportedHeight = -1;\n reportSize();\n })\n .catch(() => {\n // Not an MCP Apps host (or it was slow) — the legacy path covers data.\n });\n\n reportSize();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(\"message\", onMessage as EventListener);\n if (onWindowResize) win.removeEventListener(\"resize\", onWindowResize);\n onWindowResize = null;\n resizeObserver?.disconnect();\n resizeObserver = null;\n for (const p of pending.values()) {\n clearTimeout(p.timer);\n p.reject(new Error(\"adapter destroyed\"));\n }\n pending.clear();\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","import { createChatGPTAdapter } from \"./adapters/chatgpt.js\";\nimport { createInlineAdapter } from \"./adapters/inline.js\";\nimport { createMcpAppsAdapter } from \"./adapters/mcpapps.js\";\nimport type { ConnectUIOptions, HostAdapter, HostKind } from \"./types.js\";\n\n/**\n * Minimal window shape the detector inspects. Kept structural so detection is\n * unit-testable with a plain object — no full `Window` fake required.\n */\ninterface DetectableWindow {\n openai?: unknown;\n parent?: unknown;\n self?: unknown;\n}\n\n/**\n * Feature-detect the host from the browsing context.\n *\n * - `window.openai` present → **chatgpt** (OpenAI Apps SDK).\n * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin\n * access that throws) → **claude** (MCP Apps standard postMessage host).\n * - top-level document → **generic** (inline / standalone).\n *\n * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and\n * honors an explicit `options.host` override for previews/SSR/tests.\n */\nexport function detectHostKind(win: DetectableWindow): HostKind {\n if (win.openai != null) return \"chatgpt\";\n try {\n if (win.parent != null && win.parent !== win) return \"claude\";\n } catch {\n // Cross-origin parent access throws → we are framed by another origin.\n return \"claude\";\n }\n return \"generic\";\n}\n\n/** Build the adapter for an explicit host kind (`options.host` or a detected one). */\nexport function adapterForKind(\n kind: HostKind,\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n switch (kind) {\n case \"chatgpt\":\n return createChatGPTAdapter(win, options);\n case \"claude\":\n case \"nimblebrain\":\n // Both speak the MCP Apps standard (SEP-1865). `nimblebrain` shares it for\n // now; a dedicated adapter over the `synapse/*` extension lands in P3.\n return createMcpAppsAdapter(win, options);\n default:\n return createInlineAdapter(win, options);\n }\n}\n\n/** Select and construct the host adapter, honoring `options.host` when set. */\nexport function selectAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const kind = options.host ?? detectHostKind(win as unknown as DetectableWindow);\n return adapterForKind(kind, win, options);\n}\n","import { selectAdapter } from \"./detect.js\";\nimport { applyHostTheme } from \"./theme.js\";\nimport type { ConnectUIOptions, SynapseUIClient } from \"./types.js\";\n\n/**\n * Connect a Synapse-authored component to whatever host it renders in — ChatGPT\n * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one\n * push-first API. Feature-detects the host, selects an adapter, and applies the\n * host theme to the DOM before returning.\n *\n * Synchronous: `data()` is populated from baked-in data (where present) on\n * return, and pushed updates arrive via `onData`. Bind the result to `synapse`:\n *\n * ```ts\n * const synapse = connectUI({ name: \"my-widget\", version: \"1.0.0\" });\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (null → empty state)\n * ```\n */\nexport function connectUI(options: ConnectUIOptions = {}): SynapseUIClient {\n const win = options.window ?? (globalThis as unknown as Window & typeof globalThis);\n const adapter = selectAdapter(win, options);\n\n // The client owns theme application: adapters resolve mode/tokens; this puts\n // them on the DOM (data-theme + CSS vars) so apps never wire theme by hand.\n applyHostTheme(adapter.getTheme());\n const unsubTheme = adapter.onTheme(applyHostTheme);\n\n adapter.start();\n\n let destroyed = false;\n\n return {\n data: <T>() => adapter.getData<T>(),\n onData: <T>(cb: (data: T) => void) => adapter.onData<T>(cb),\n theme: () => adapter.getTheme(),\n onTheme: (cb) => adapter.onTheme(cb),\n callTool: <O>(name: string, args?: Record<string, unknown>) => adapter.callTool<O>(name, args),\n sendPrompt: (text: string) => adapter.sendPrompt(text),\n openLink: (url: string) => adapter.openLink(url),\n resize: (height?: number) => adapter.resize(height),\n capabilities: () => adapter.capabilities(),\n host: () => adapter.host,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubTheme();\n adapter.destroy();\n },\n };\n}\n"]} |
| import { F as FontFaceDescriptor } from './types-BSZasM8q.cjs'; | ||
| /** | ||
| * Cross-host UI client — types and wire constants. | ||
| * | ||
| * The cross-host client (`connectUI`) renders one Synapse-authored component in | ||
| * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude | ||
| * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch | ||
| * a host protocol. This is a **push-first** surface: the tool output that spawned | ||
| * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the | ||
| * pull escape hatch, advertised per host via `capabilities()`. | ||
| * | ||
| * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the | ||
| * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so | ||
| * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas). | ||
| */ | ||
| /** | ||
| * The host the client resolved to, as reported by `synapse.host()`. An escape | ||
| * hatch — apps should rarely branch on it; `capabilities()` is the supported way | ||
| * to feature-detect. `"nimblebrain"` is reserved for the runtime adapter (P3). | ||
| */ | ||
| type HostKind = "chatgpt" | "claude" | "nimblebrain" | "generic"; | ||
| /** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS | ||
| * custom properties the host publishes — the MCP Apps adapter reads them from | ||
| * `hostContext.styles.variables`; where a host sends none they stay empty and the | ||
| * SDK's neutral defaults back them. */ | ||
| interface SynapseUITheme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| /** Font faces the host wants loaded. Absent where the host sends none, which | ||
| * leaves the SDK's web-safe fallbacks in force. See `FontFaceDescriptor`. */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| /** What the active host actually supports. `data()`/`onData()`/`theme()`/ | ||
| * `resize()` work everywhere; these three vary. */ | ||
| interface HostCapabilities { | ||
| /** `callTool()` can reach the server (widget→server fetch). */ | ||
| pull: boolean; | ||
| /** `sendPrompt()` reaches the agent conversation. */ | ||
| sendPrompt: boolean; | ||
| /** `openLink()` opens an external URL through the host. */ | ||
| openLink: boolean; | ||
| } | ||
| /** Thrown by `callTool()` when the active host offers no widget→server call. */ | ||
| declare class HostUnsupportedError extends Error { | ||
| constructor(feature: string, host: HostKind); | ||
| } | ||
| interface ConnectUIOptions { | ||
| /** App name — informational; forwarded to hosts that accept an appInfo. */ | ||
| name?: string; | ||
| /** App semver — informational. */ | ||
| version?: string; | ||
| /** | ||
| * Force a host adapter instead of auto-detecting. Used by preview harnesses, | ||
| * SSR, and tests; production apps omit it and let the SDK feature-detect. | ||
| * `"claude"` → MCP Apps standard adapter, `"chatgpt"` → OpenAI Apps adapter, | ||
| * `"generic"` → inline adapter. | ||
| */ | ||
| host?: HostKind; | ||
| /** | ||
| * `id` of the `<script type="application/json">` element carrying pushed data | ||
| * baked into the HTML (the mcp-ui / SSR path). Defaults to | ||
| * {@link SYNAPSE_DATA_ELEMENT_ID}. | ||
| */ | ||
| dataElementId?: string; | ||
| /** | ||
| * Auto-report content height to the host on layout changes (mcp-ui only). | ||
| * Defaults to `true`. Set `false` to size manually via `resize()`. | ||
| */ | ||
| autoResize?: boolean; | ||
| /** Window to bind to. Defaults to the global `window`. Injectable for tests. */ | ||
| window?: Window & typeof globalThis; | ||
| } | ||
| /** | ||
| * The public cross-host client. Bound to `synapse` by convention. | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI(); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (may be null → empty state) | ||
| * ``` | ||
| */ | ||
| interface SynapseUIClient { | ||
| /** The current pushed data, or `null` before anything has been delivered. */ | ||
| data<T = unknown>(): T | null; | ||
| /** Subscribe to data updates (NOT replayed — read `data()` for the current | ||
| * value). Returns an unsubscribe. */ | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| /** The current resolved theme. */ | ||
| theme(): SynapseUITheme; | ||
| /** Subscribe to theme changes. The client already applies the theme to the DOM | ||
| * (`data-theme` attribute + CSS variables) before this fires. */ | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the | ||
| * host advertises no pull (`capabilities().pull === false`). */ | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| /** Send a follow-up message to the agent conversation. No-op where unsupported. */ | ||
| sendPrompt(text: string): void; | ||
| /** Open an external URL through the host (falls back to `window.open`). */ | ||
| openLink(url: string): void; | ||
| /** Report content height to the host. Omit `height` to measure `document.body`. */ | ||
| resize(height?: number): void; | ||
| /** What the active host supports. */ | ||
| capabilities(): HostCapabilities; | ||
| /** The resolved host — an escape hatch; prefer `capabilities()`. */ | ||
| host(): HostKind; | ||
| /** Tear down listeners/observers. */ | ||
| destroy(): void; | ||
| } | ||
| /** | ||
| * Internal adapter contract. One per host bridge; the client is a thin façade | ||
| * over the selected adapter. | ||
| */ | ||
| interface HostAdapter { | ||
| readonly host: HostKind; | ||
| getData<T = unknown>(): T | null; | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| getTheme(): SynapseUITheme; | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| sendPrompt(text: string): void; | ||
| openLink(url: string): void; | ||
| resize(height?: number): void; | ||
| capabilities(): HostCapabilities; | ||
| /** Begin listening / send the ready handshake / read baked-in data. Called once | ||
| * by `connectUI` synchronously so `getData()` is populated on return. */ | ||
| start(): void; | ||
| destroy(): void; | ||
| } | ||
| /** Default `id` of the baked-in data `<script type="application/json">`. */ | ||
| declare const SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| /** | ||
| * Connect a Synapse-authored component to whatever host it renders in — ChatGPT | ||
| * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one | ||
| * push-first API. Feature-detects the host, selects an adapter, and applies the | ||
| * host theme to the DOM before returning. | ||
| * | ||
| * Synchronous: `data()` is populated from baked-in data (where present) on | ||
| * return, and pushed updates arrive via `onData`. Bind the result to `synapse`: | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI({ name: "my-widget", version: "1.0.0" }); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (null → empty state) | ||
| * ``` | ||
| */ | ||
| declare function connectUI(options?: ConnectUIOptions): SynapseUIClient; | ||
| /** | ||
| * Minimal window shape the detector inspects. Kept structural so detection is | ||
| * unit-testable with a plain object — no full `Window` fake required. | ||
| */ | ||
| interface DetectableWindow { | ||
| openai?: unknown; | ||
| parent?: unknown; | ||
| self?: unknown; | ||
| } | ||
| /** | ||
| * Feature-detect the host from the browsing context. | ||
| * | ||
| * - `window.openai` present → **chatgpt** (OpenAI Apps SDK). | ||
| * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin | ||
| * access that throws) → **claude** (MCP Apps standard postMessage host). | ||
| * - top-level document → **generic** (inline / standalone). | ||
| * | ||
| * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and | ||
| * honors an explicit `options.host` override for previews/SSR/tests. | ||
| */ | ||
| declare function detectHostKind(win: DetectableWindow): HostKind; | ||
| /** Build the adapter for an explicit host kind (`options.host` or a detected one). */ | ||
| declare function adapterForKind(kind: HostKind, win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| /** Select and construct the host adapter, honoring `options.host` when set. */ | ||
| declare function selectAdapter(win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| export { type ConnectUIOptions as C, type HostCapabilities as H, SYNAPSE_DATA_ELEMENT_ID as S, type HostKind as a, HostUnsupportedError as b, type SynapseUIClient as c, type SynapseUITheme as d, connectUI as e, detectHostKind as f, type HostAdapter as g, adapterForKind as h, selectAdapter as s }; |
| import { F as FontFaceDescriptor } from './types-BSZasM8q.js'; | ||
| /** | ||
| * Cross-host UI client — types and wire constants. | ||
| * | ||
| * The cross-host client (`connectUI`) renders one Synapse-authored component in | ||
| * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude | ||
| * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch | ||
| * a host protocol. This is a **push-first** surface: the tool output that spawned | ||
| * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the | ||
| * pull escape hatch, advertised per host via `capabilities()`. | ||
| * | ||
| * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the | ||
| * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so | ||
| * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas). | ||
| */ | ||
| /** | ||
| * The host the client resolved to, as reported by `synapse.host()`. An escape | ||
| * hatch — apps should rarely branch on it; `capabilities()` is the supported way | ||
| * to feature-detect. `"nimblebrain"` is reserved for the runtime adapter (P3). | ||
| */ | ||
| type HostKind = "chatgpt" | "claude" | "nimblebrain" | "generic"; | ||
| /** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS | ||
| * custom properties the host publishes — the MCP Apps adapter reads them from | ||
| * `hostContext.styles.variables`; where a host sends none they stay empty and the | ||
| * SDK's neutral defaults back them. */ | ||
| interface SynapseUITheme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| /** Font faces the host wants loaded. Absent where the host sends none, which | ||
| * leaves the SDK's web-safe fallbacks in force. See `FontFaceDescriptor`. */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| /** What the active host actually supports. `data()`/`onData()`/`theme()`/ | ||
| * `resize()` work everywhere; these three vary. */ | ||
| interface HostCapabilities { | ||
| /** `callTool()` can reach the server (widget→server fetch). */ | ||
| pull: boolean; | ||
| /** `sendPrompt()` reaches the agent conversation. */ | ||
| sendPrompt: boolean; | ||
| /** `openLink()` opens an external URL through the host. */ | ||
| openLink: boolean; | ||
| } | ||
| /** Thrown by `callTool()` when the active host offers no widget→server call. */ | ||
| declare class HostUnsupportedError extends Error { | ||
| constructor(feature: string, host: HostKind); | ||
| } | ||
| interface ConnectUIOptions { | ||
| /** App name — informational; forwarded to hosts that accept an appInfo. */ | ||
| name?: string; | ||
| /** App semver — informational. */ | ||
| version?: string; | ||
| /** | ||
| * Force a host adapter instead of auto-detecting. Used by preview harnesses, | ||
| * SSR, and tests; production apps omit it and let the SDK feature-detect. | ||
| * `"claude"` → MCP Apps standard adapter, `"chatgpt"` → OpenAI Apps adapter, | ||
| * `"generic"` → inline adapter. | ||
| */ | ||
| host?: HostKind; | ||
| /** | ||
| * `id` of the `<script type="application/json">` element carrying pushed data | ||
| * baked into the HTML (the mcp-ui / SSR path). Defaults to | ||
| * {@link SYNAPSE_DATA_ELEMENT_ID}. | ||
| */ | ||
| dataElementId?: string; | ||
| /** | ||
| * Auto-report content height to the host on layout changes (mcp-ui only). | ||
| * Defaults to `true`. Set `false` to size manually via `resize()`. | ||
| */ | ||
| autoResize?: boolean; | ||
| /** Window to bind to. Defaults to the global `window`. Injectable for tests. */ | ||
| window?: Window & typeof globalThis; | ||
| } | ||
| /** | ||
| * The public cross-host client. Bound to `synapse` by convention. | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI(); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (may be null → empty state) | ||
| * ``` | ||
| */ | ||
| interface SynapseUIClient { | ||
| /** The current pushed data, or `null` before anything has been delivered. */ | ||
| data<T = unknown>(): T | null; | ||
| /** Subscribe to data updates (NOT replayed — read `data()` for the current | ||
| * value). Returns an unsubscribe. */ | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| /** The current resolved theme. */ | ||
| theme(): SynapseUITheme; | ||
| /** Subscribe to theme changes. The client already applies the theme to the DOM | ||
| * (`data-theme` attribute + CSS variables) before this fires. */ | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the | ||
| * host advertises no pull (`capabilities().pull === false`). */ | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| /** Send a follow-up message to the agent conversation. No-op where unsupported. */ | ||
| sendPrompt(text: string): void; | ||
| /** Open an external URL through the host (falls back to `window.open`). */ | ||
| openLink(url: string): void; | ||
| /** Report content height to the host. Omit `height` to measure `document.body`. */ | ||
| resize(height?: number): void; | ||
| /** What the active host supports. */ | ||
| capabilities(): HostCapabilities; | ||
| /** The resolved host — an escape hatch; prefer `capabilities()`. */ | ||
| host(): HostKind; | ||
| /** Tear down listeners/observers. */ | ||
| destroy(): void; | ||
| } | ||
| /** | ||
| * Internal adapter contract. One per host bridge; the client is a thin façade | ||
| * over the selected adapter. | ||
| */ | ||
| interface HostAdapter { | ||
| readonly host: HostKind; | ||
| getData<T = unknown>(): T | null; | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| getTheme(): SynapseUITheme; | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| sendPrompt(text: string): void; | ||
| openLink(url: string): void; | ||
| resize(height?: number): void; | ||
| capabilities(): HostCapabilities; | ||
| /** Begin listening / send the ready handshake / read baked-in data. Called once | ||
| * by `connectUI` synchronously so `getData()` is populated on return. */ | ||
| start(): void; | ||
| destroy(): void; | ||
| } | ||
| /** Default `id` of the baked-in data `<script type="application/json">`. */ | ||
| declare const SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| /** | ||
| * Connect a Synapse-authored component to whatever host it renders in — ChatGPT | ||
| * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one | ||
| * push-first API. Feature-detects the host, selects an adapter, and applies the | ||
| * host theme to the DOM before returning. | ||
| * | ||
| * Synchronous: `data()` is populated from baked-in data (where present) on | ||
| * return, and pushed updates arrive via `onData`. Bind the result to `synapse`: | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI({ name: "my-widget", version: "1.0.0" }); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (null → empty state) | ||
| * ``` | ||
| */ | ||
| declare function connectUI(options?: ConnectUIOptions): SynapseUIClient; | ||
| /** | ||
| * Minimal window shape the detector inspects. Kept structural so detection is | ||
| * unit-testable with a plain object — no full `Window` fake required. | ||
| */ | ||
| interface DetectableWindow { | ||
| openai?: unknown; | ||
| parent?: unknown; | ||
| self?: unknown; | ||
| } | ||
| /** | ||
| * Feature-detect the host from the browsing context. | ||
| * | ||
| * - `window.openai` present → **chatgpt** (OpenAI Apps SDK). | ||
| * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin | ||
| * access that throws) → **claude** (MCP Apps standard postMessage host). | ||
| * - top-level document → **generic** (inline / standalone). | ||
| * | ||
| * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and | ||
| * honors an explicit `options.host` override for previews/SSR/tests. | ||
| */ | ||
| declare function detectHostKind(win: DetectableWindow): HostKind; | ||
| /** Build the adapter for an explicit host kind (`options.host` or a detected one). */ | ||
| declare function adapterForKind(kind: HostKind, win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| /** Select and construct the host adapter, honoring `options.host` when set. */ | ||
| declare function selectAdapter(win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| export { type ConnectUIOptions as C, type HostCapabilities as H, SYNAPSE_DATA_ELEMENT_ID as S, type HostKind as a, HostUnsupportedError as b, type SynapseUIClient as c, type SynapseUITheme as d, connectUI as e, detectHostKind as f, type HostAdapter as g, adapterForKind as h, selectAdapter as s }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| /** | ||
| * One `@font-face` a host asks the app to load. | ||
| * | ||
| * The token contract can name a font (`--font-sans: 'Hanken Grotesk', system-ui`) | ||
| * but cannot *load* one — a CSS custom property carries a family name, never the | ||
| * `@font-face` rule behind it. An app iframe is its own document and inherits no | ||
| * `@font-face` from the host page, so a host that only sends tokens is naming a | ||
| * family the app has no way to render. This descriptor is the missing half. | ||
| * | ||
| * The SDK ships **no font data** — it applies what the host sends. A host that | ||
| * sends none leaves the web-safe fallbacks in `ui/tokens.ts` in force. | ||
| */ | ||
| interface FontFaceDescriptor { | ||
| /** Family name, matching the one used in the host's `--font-*` token value. */ | ||
| family: string; | ||
| /** | ||
| * CSS `src` descriptor. Local and absolute URLs work identically — | ||
| * `url('/fonts/x.woff2') format('woff2')` or `url('https://cdn.example/x.woff2')`. | ||
| * Whatever origin this names must satisfy the app iframe's `font-src` CSP. | ||
| */ | ||
| src: string; | ||
| /** `font-weight` descriptor — a single weight (`400`) or a variable range (`400 700`). */ | ||
| weight?: string; | ||
| /** `font-style` descriptor (`normal`, `italic`, …). */ | ||
| style?: string; | ||
| /** `font-display` descriptor. Defaults to `swap` so text paints in the fallback first. */ | ||
| display?: FontDisplayValue; | ||
| } | ||
| /** The CSS `font-display` values. Closed set — anything else is ignored. */ | ||
| type FontDisplayValue = "auto" | "block" | "swap" | "fallback" | "optional"; | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| /** | ||
| * Font faces the host wants loaded into the app document. Optional — a host | ||
| * that omits them leaves the web-safe fallbacks in force. Arrives over the | ||
| * wire as the `synapse/fontFaces` host-context extension. | ||
| */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| /** Font faces the host wants loaded. See {@link FontFaceDescriptor}. */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FontFaceDescriptor as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, FileResult as i, FontDisplayValue as j, NotifyPayload as k, StateAcknowledgement as l, StoreDispatch as m, SynapseTheme as n, TaskHandle as o, TasksCapability as p, Theme as q, ToolCallResult as r, ToolResultData as s }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| /** | ||
| * One `@font-face` a host asks the app to load. | ||
| * | ||
| * The token contract can name a font (`--font-sans: 'Hanken Grotesk', system-ui`) | ||
| * but cannot *load* one — a CSS custom property carries a family name, never the | ||
| * `@font-face` rule behind it. An app iframe is its own document and inherits no | ||
| * `@font-face` from the host page, so a host that only sends tokens is naming a | ||
| * family the app has no way to render. This descriptor is the missing half. | ||
| * | ||
| * The SDK ships **no font data** — it applies what the host sends. A host that | ||
| * sends none leaves the web-safe fallbacks in `ui/tokens.ts` in force. | ||
| */ | ||
| interface FontFaceDescriptor { | ||
| /** Family name, matching the one used in the host's `--font-*` token value. */ | ||
| family: string; | ||
| /** | ||
| * CSS `src` descriptor. Local and absolute URLs work identically — | ||
| * `url('/fonts/x.woff2') format('woff2')` or `url('https://cdn.example/x.woff2')`. | ||
| * Whatever origin this names must satisfy the app iframe's `font-src` CSP. | ||
| */ | ||
| src: string; | ||
| /** `font-weight` descriptor — a single weight (`400`) or a variable range (`400 700`). */ | ||
| weight?: string; | ||
| /** `font-style` descriptor (`normal`, `italic`, …). */ | ||
| style?: string; | ||
| /** `font-display` descriptor. Defaults to `swap` so text paints in the fallback first. */ | ||
| display?: FontDisplayValue; | ||
| } | ||
| /** The CSS `font-display` values. Closed set — anything else is ignored. */ | ||
| type FontDisplayValue = "auto" | "block" | "swap" | "fallback" | "optional"; | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| /** | ||
| * Font faces the host wants loaded into the app document. Optional — a host | ||
| * that omits them leaves the web-safe fallbacks in force. Arrives over the | ||
| * wire as the `synapse/fontFaces` host-context extension. | ||
| */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| /** Font faces the host wants loaded. See {@link FontFaceDescriptor}. */ | ||
| fontFaces?: FontFaceDescriptor[]; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FontFaceDescriptor as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, FileResult as i, FontDisplayValue as j, NotifyPayload as k, StateAcknowledgement as l, StoreDispatch as m, SynapseTheme as n, TaskHandle as o, TasksCapability as p, Theme as q, ToolCallResult as r, ToolResultData as s }; |
@@ -1,2 +0,2 @@ | ||
| import { T as ToolDefinition } from '../types-C9utGXv_.cjs'; | ||
| import { T as ToolDefinition } from '../types-BSZasM8q.cjs'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -3,0 +3,0 @@ import '@modelcontextprotocol/sdk/types.js'; |
@@ -1,2 +0,2 @@ | ||
| import { T as ToolDefinition } from '../types-C9utGXv_.js'; | ||
| import { T as ToolDefinition } from '../types-BSZasM8q.js'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -3,0 +3,0 @@ import '@modelcontextprotocol/sdk/types.js'; |
+14
-14
| 'use strict'; | ||
| var chunkANUZ7WFQ_cjs = require('../chunk-ANUZ7WFQ.cjs'); | ||
| require('../chunk-KTXASIFJ.cjs'); | ||
| var chunkVMUZMVKW_cjs = require('../chunk-VMUZMVKW.cjs'); | ||
| require('../chunk-IA2M4GTK.cjs'); | ||
@@ -10,49 +10,49 @@ | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.HostUnsupportedError; } | ||
| get: function () { return chunkVMUZMVKW_cjs.HostUnsupportedError; } | ||
| }); | ||
| Object.defineProperty(exports, "SYNAPSE_DATA_ELEMENT_ID", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.SYNAPSE_DATA_ELEMENT_ID; } | ||
| get: function () { return chunkVMUZMVKW_cjs.SYNAPSE_DATA_ELEMENT_ID; } | ||
| }); | ||
| Object.defineProperty(exports, "adapterForKind", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.adapterForKind; } | ||
| get: function () { return chunkVMUZMVKW_cjs.adapterForKind; } | ||
| }); | ||
| Object.defineProperty(exports, "applyHostTheme", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.applyHostTheme; } | ||
| get: function () { return chunkVMUZMVKW_cjs.applyHostTheme; } | ||
| }); | ||
| Object.defineProperty(exports, "coerceMode", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.coerceMode; } | ||
| get: function () { return chunkVMUZMVKW_cjs.coerceMode; } | ||
| }); | ||
| Object.defineProperty(exports, "connectUI", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.connectUI; } | ||
| get: function () { return chunkVMUZMVKW_cjs.connectUI; } | ||
| }); | ||
| Object.defineProperty(exports, "createChatGPTAdapter", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.createChatGPTAdapter; } | ||
| get: function () { return chunkVMUZMVKW_cjs.createChatGPTAdapter; } | ||
| }); | ||
| Object.defineProperty(exports, "createInlineAdapter", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.createInlineAdapter; } | ||
| get: function () { return chunkVMUZMVKW_cjs.createInlineAdapter; } | ||
| }); | ||
| Object.defineProperty(exports, "createMcpAppsAdapter", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.createMcpAppsAdapter; } | ||
| get: function () { return chunkVMUZMVKW_cjs.createMcpAppsAdapter; } | ||
| }); | ||
| Object.defineProperty(exports, "detectHostKind", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.detectHostKind; } | ||
| get: function () { return chunkVMUZMVKW_cjs.detectHostKind; } | ||
| }); | ||
| Object.defineProperty(exports, "preferredMode", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.preferredMode; } | ||
| get: function () { return chunkVMUZMVKW_cjs.preferredMode; } | ||
| }); | ||
| Object.defineProperty(exports, "selectAdapter", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.selectAdapter; } | ||
| get: function () { return chunkVMUZMVKW_cjs.selectAdapter; } | ||
| }); | ||
| //# sourceMappingURL=index.cjs.map | ||
| //# sourceMappingURL=index.cjs.map |
@@ -1,3 +0,6 @@ | ||
| import { C as ConnectUIOptions, g as HostAdapter, d as SynapseUITheme } from '../detect-DSnLT-_m.cjs'; | ||
| export { H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, h as adapterForKind, e as connectUI, f as detectHostKind, s as selectAdapter } from '../detect-DSnLT-_m.cjs'; | ||
| import { C as ConnectUIOptions, g as HostAdapter, d as SynapseUITheme } from '../detect-C_qmYsh9.cjs'; | ||
| export { H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, h as adapterForKind, e as connectUI, f as detectHostKind, s as selectAdapter } from '../detect-C_qmYsh9.cjs'; | ||
| import '../types-BSZasM8q.cjs'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
| import '@modelcontextprotocol/sdk/types.js'; | ||
@@ -53,3 +56,3 @@ /** | ||
| * lever a host's light/dark signal actually flips. | ||
| * - CSS custom properties via {@link applyThemeVariables} — how the | ||
| * - CSS custom properties via {@link applyTheme} — how the | ||
| * `@nimblebrain/synapse/ui` token components consume theme, backed by the | ||
@@ -60,2 +63,6 @@ * SDK's neutral defaults so every referenced var resolves in both modes. | ||
| * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe. | ||
| * | ||
| * Any `fontFaces` the host supplies are loaded here too — a token can name a | ||
| * family but not load it, so the two travel together. A host that sends none | ||
| * leaves the web-safe fallbacks in force. | ||
| */ | ||
@@ -62,0 +69,0 @@ declare function applyHostTheme(theme: SynapseUITheme): void; |
+10
-3
@@ -1,3 +0,6 @@ | ||
| import { C as ConnectUIOptions, g as HostAdapter, d as SynapseUITheme } from '../detect-DSnLT-_m.js'; | ||
| export { H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, h as adapterForKind, e as connectUI, f as detectHostKind, s as selectAdapter } from '../detect-DSnLT-_m.js'; | ||
| import { C as ConnectUIOptions, g as HostAdapter, d as SynapseUITheme } from '../detect-Dfd0upoz.js'; | ||
| export { H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, h as adapterForKind, e as connectUI, f as detectHostKind, s as selectAdapter } from '../detect-Dfd0upoz.js'; | ||
| import '../types-BSZasM8q.js'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
| import '@modelcontextprotocol/sdk/types.js'; | ||
@@ -53,3 +56,3 @@ /** | ||
| * lever a host's light/dark signal actually flips. | ||
| * - CSS custom properties via {@link applyThemeVariables} — how the | ||
| * - CSS custom properties via {@link applyTheme} — how the | ||
| * `@nimblebrain/synapse/ui` token components consume theme, backed by the | ||
@@ -60,2 +63,6 @@ * SDK's neutral defaults so every referenced var resolves in both modes. | ||
| * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe. | ||
| * | ||
| * Any `fontFaces` the host supplies are loaded here too — a token can name a | ||
| * family but not load it, so the two travel together. A host that sends none | ||
| * leaves the web-safe fallbacks in force. | ||
| */ | ||
@@ -62,0 +69,0 @@ declare function applyHostTheme(theme: SynapseUITheme): void; |
@@ -1,4 +0,4 @@ | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, adapterForKind, applyHostTheme, coerceMode, connectUI, createChatGPTAdapter, createInlineAdapter, createMcpAppsAdapter, detectHostKind, preferredMode, selectAdapter } from '../chunk-JVKQNMAP.js'; | ||
| import '../chunk-VXTMI266.js'; | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, adapterForKind, applyHostTheme, coerceMode, connectUI, createChatGPTAdapter, createInlineAdapter, createMcpAppsAdapter, detectHostKind, preferredMode, selectAdapter } from '../chunk-5AR3IROW.js'; | ||
| import '../chunk-DOKVNVTI.js'; | ||
| //# sourceMappingURL=index.js.map | ||
| //# sourceMappingURL=index.js.map |
+13
-9
| 'use strict'; | ||
| var chunkANUZ7WFQ_cjs = require('./chunk-ANUZ7WFQ.cjs'); | ||
| var chunkA4ENX6XW_cjs = require('./chunk-A4ENX6XW.cjs'); | ||
| require('./chunk-KTXASIFJ.cjs'); | ||
| var chunkVMUZMVKW_cjs = require('./chunk-VMUZMVKW.cjs'); | ||
| var chunkQHS7BJLI_cjs = require('./chunk-QHS7BJLI.cjs'); | ||
| var chunkIA2M4GTK_cjs = require('./chunk-IA2M4GTK.cjs'); | ||
@@ -85,26 +85,30 @@ // src/store.ts | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.HostUnsupportedError; } | ||
| get: function () { return chunkVMUZMVKW_cjs.HostUnsupportedError; } | ||
| }); | ||
| Object.defineProperty(exports, "SYNAPSE_DATA_ELEMENT_ID", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.SYNAPSE_DATA_ELEMENT_ID; } | ||
| get: function () { return chunkVMUZMVKW_cjs.SYNAPSE_DATA_ELEMENT_ID; } | ||
| }); | ||
| Object.defineProperty(exports, "connectUI", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.connectUI; } | ||
| get: function () { return chunkVMUZMVKW_cjs.connectUI; } | ||
| }); | ||
| Object.defineProperty(exports, "detectHostKind", { | ||
| enumerable: true, | ||
| get: function () { return chunkANUZ7WFQ_cjs.detectHostKind; } | ||
| get: function () { return chunkVMUZMVKW_cjs.detectHostKind; } | ||
| }); | ||
| Object.defineProperty(exports, "connect", { | ||
| enumerable: true, | ||
| get: function () { return chunkA4ENX6XW_cjs.connect; } | ||
| get: function () { return chunkQHS7BJLI_cjs.connect; } | ||
| }); | ||
| Object.defineProperty(exports, "createSynapse", { | ||
| enumerable: true, | ||
| get: function () { return chunkA4ENX6XW_cjs.createSynapse; } | ||
| get: function () { return chunkQHS7BJLI_cjs.createSynapse; } | ||
| }); | ||
| Object.defineProperty(exports, "FONT_FACES_CONTEXT_KEY", { | ||
| enumerable: true, | ||
| get: function () { return chunkIA2M4GTK_cjs.FONT_FACES_CONTEXT_KEY; } | ||
| }); | ||
| exports.createStore = createStore; | ||
| //# sourceMappingURL=index.cjs.map | ||
| //# sourceMappingURL=index.cjs.map |
+16
-4
| export { CreateTaskResult, ReadResourceRequest, ReadResourceResult, Task, TaskStatus } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-C9utGXv_.cjs'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-C9utGXv_.cjs'; | ||
| export { C as ConnectUIOptions, H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, d as SynapseUITheme, e as connectUI, f as detectHostKind } from './detect-DSnLT-_m.cjs'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-BSZasM8q.cjs'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, i as FileResult, j as FontDisplayValue, F as FontFaceDescriptor, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, k as NotifyPayload, R as RequestFileOptions, l as StateAcknowledgement, m as StoreDispatch, n as SynapseTheme, o as TaskHandle, p as TasksCapability, q as Theme, r as ToolCallResult, T as ToolDefinition, s as ToolResultData, V as VisibleState } from './types-BSZasM8q.cjs'; | ||
| export { C as ConnectUIOptions, H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, d as SynapseUITheme, e as connectUI, f as detectHostKind } from './detect-C_qmYsh9.cjs'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -27,2 +27,14 @@ | ||
| /** | ||
| * Host-context key carrying `@font-face` descriptors. | ||
| * | ||
| * A NimbleBrain extension (`synapse/` prefix), because the ext-apps spec has no | ||
| * equivalent: `styles.variables` is a flat `Record<McpUiStyleVariableKey, string>` | ||
| * that can name a font family but cannot carry the rule that loads it. | ||
| * `McpUiHostContext` declares `[key: string]: unknown` for forward | ||
| * compatibility, so riding an extra key is spec-legal, and hosts that don't send | ||
| * it degrade to the web-safe token fallbacks. | ||
| */ | ||
| declare const FONT_FACES_CONTEXT_KEY = "synapse/fontFaces"; | ||
| /** | ||
| * Create a typed state store with optional persistence and agent visibility. | ||
@@ -38,2 +50,2 @@ * | ||
| export { ActionReducer, App, ConnectOptions, Store, StoreConfig, Synapse, SynapseOptions, connect, createStore, createSynapse }; | ||
| export { ActionReducer, App, ConnectOptions, FONT_FACES_CONTEXT_KEY, Store, StoreConfig, Synapse, SynapseOptions, connect, createStore, createSynapse }; |
+16
-4
| export { CreateTaskResult, ReadResourceRequest, ReadResourceResult, Task, TaskStatus } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-C9utGXv_.js'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-C9utGXv_.js'; | ||
| export { C as ConnectUIOptions, H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, d as SynapseUITheme, e as connectUI, f as detectHostKind } from './detect-DSnLT-_m.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-BSZasM8q.js'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, i as FileResult, j as FontDisplayValue, F as FontFaceDescriptor, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, k as NotifyPayload, R as RequestFileOptions, l as StateAcknowledgement, m as StoreDispatch, n as SynapseTheme, o as TaskHandle, p as TasksCapability, q as Theme, r as ToolCallResult, T as ToolDefinition, s as ToolResultData, V as VisibleState } from './types-BSZasM8q.js'; | ||
| export { C as ConnectUIOptions, H as HostCapabilities, a as HostKind, b as HostUnsupportedError, S as SYNAPSE_DATA_ELEMENT_ID, c as SynapseUIClient, d as SynapseUITheme, e as connectUI, f as detectHostKind } from './detect-Dfd0upoz.js'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -27,2 +27,14 @@ | ||
| /** | ||
| * Host-context key carrying `@font-face` descriptors. | ||
| * | ||
| * A NimbleBrain extension (`synapse/` prefix), because the ext-apps spec has no | ||
| * equivalent: `styles.variables` is a flat `Record<McpUiStyleVariableKey, string>` | ||
| * that can name a font family but cannot carry the rule that loads it. | ||
| * `McpUiHostContext` declares `[key: string]: unknown` for forward | ||
| * compatibility, so riding an extra key is spec-legal, and hosts that don't send | ||
| * it degrade to the web-safe token fallbacks. | ||
| */ | ||
| declare const FONT_FACES_CONTEXT_KEY = "synapse/fontFaces"; | ||
| /** | ||
| * Create a typed state store with optional persistence and agent visibility. | ||
@@ -38,2 +50,2 @@ * | ||
| export { ActionReducer, App, ConnectOptions, Store, StoreConfig, Synapse, SynapseOptions, connect, createStore, createSynapse }; | ||
| export { ActionReducer, App, ConnectOptions, FONT_FACES_CONTEXT_KEY, Store, StoreConfig, Synapse, SynapseOptions, connect, createStore, createSynapse }; |
+3
-3
@@ -1,4 +0,4 @@ | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, connectUI, detectHostKind } from './chunk-JVKQNMAP.js'; | ||
| export { connect, createSynapse } from './chunk-XGJAADVV.js'; | ||
| import './chunk-VXTMI266.js'; | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, connectUI, detectHostKind } from './chunk-5AR3IROW.js'; | ||
| export { connect, createSynapse } from './chunk-AGN3NDOK.js'; | ||
| export { FONT_FACES_CONTEXT_KEY } from './chunk-DOKVNVTI.js'; | ||
@@ -5,0 +5,0 @@ // src/store.ts |
| 'use strict'; | ||
| var chunkA4ENX6XW_cjs = require('../chunk-A4ENX6XW.cjs'); | ||
| var chunkKTXASIFJ_cjs = require('../chunk-KTXASIFJ.cjs'); | ||
| var chunkQHS7BJLI_cjs = require('../chunk-QHS7BJLI.cjs'); | ||
| var chunkIA2M4GTK_cjs = require('../chunk-IA2M4GTK.cjs'); | ||
| var react = require('react'); | ||
@@ -15,3 +15,3 @@ var jsxRuntime = require('react/jsx-runtime'); | ||
| connectingRef.current = true; | ||
| chunkA4ENX6XW_cjs.connect({ name, version, autoResize }).then((a) => { | ||
| chunkQHS7BJLI_cjs.connect({ name, version, autoResize }).then((a) => { | ||
| setApp(a); | ||
@@ -76,3 +76,3 @@ }); | ||
| if (ref.current === null || ref.current.destroyed) { | ||
| ref.current = chunkA4ENX6XW_cjs.createSynapse(options); | ||
| ref.current = chunkQHS7BJLI_cjs.createSynapse(options); | ||
| } | ||
@@ -91,3 +91,3 @@ return /* @__PURE__ */ jsxRuntime.jsxs(SynapseContext.Provider, { value: ref.current, children: [ | ||
| react.useEffect(() => { | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens); | ||
| chunkIA2M4GTK_cjs.applyTheme(theme.mode, theme.tokens, theme.fontFaces); | ||
| }, [theme]); | ||
@@ -94,0 +94,0 @@ return null; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../../src/react/app-provider.tsx","../../src/react/connect-hooks.ts","../../src/react/provider.tsx","../../src/react/hooks.ts"],"names":["createContext","useState","useRef","useEffect","connect","useContext","useCallback","createSynapse","jsx","applyThemeVariables","useSyncExternalStore","isTerminal"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { connect } from \"../connect.js\";\nimport type { App, ConnectOptions } from \"../types.js\";\n\nconst AppContext = createContext<App | null>(null);\n\nexport interface AppProviderProps extends ConnectOptions {\n children: ReactNode;\n}\n\nexport function AppProvider({ children, name, version, autoResize }: AppProviderProps) {\n const [app, setApp] = useState<App | null>(null);\n const connectingRef = useRef(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: connect once on mount\n useEffect(() => {\n if (connectingRef.current) return;\n connectingRef.current = true;\n\n connect({ name, version, autoResize }).then((a) => {\n setApp(a);\n });\n }, []);\n\n if (!app) return null; // Don't render children until connected\n\n return <AppContext.Provider value={app}>{children}</AppContext.Provider>;\n}\n\nexport function useAppContext(): App {\n const ctx = useContext(AppContext);\n if (!ctx) {\n throw new Error(\n \"useApp must be used within an <AppProvider>. Wrap your component tree with <AppProvider>.\",\n );\n }\n return ctx;\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { App, Theme, ToolResultData } from \"../types.js\";\nimport { AppProvider, useAppContext } from \"./app-provider.js\";\n\nexport { AppProvider };\n\nexport function useApp(): App {\n return useAppContext();\n}\n\nexport function useToolResult(): ToolResultData | null {\n const app = useAppContext();\n const [data, setData] = useState<ToolResultData | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-result\", (result: ToolResultData) => {\n setData(result);\n });\n }, [app]);\n\n return data;\n}\n\nexport function useToolInput(): Record<string, unknown> | null {\n const app = useAppContext();\n const [input, setInput] = useState<Record<string, unknown> | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-input\", (args: Record<string, unknown>) => {\n setInput(args);\n });\n }, [app]);\n\n return input;\n}\n\nexport function useConnectTheme(): Theme {\n const app = useAppContext();\n const [theme, setTheme] = useState<Theme>(() => app.theme);\n\n useEffect(() => {\n setTheme(app.theme); // Sync in case it changed between render and effect\n return app.on(\"theme-changed\", (t: Theme) => {\n setTheme(t);\n });\n }, [app]);\n\n return theme;\n}\n\nexport function useResize(): (width?: number, height?: number) => void {\n const app = useAppContext();\n return useCallback((width?: number, height?: number) => app.resize(width, height), [app]);\n}\n","import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { createSynapse } from \"../core.js\";\nimport { applyThemeVariables } from \"../theme-defaults.js\";\nimport type { Synapse, SynapseOptions, SynapseTheme } from \"../types.js\";\n\nconst SynapseContext = createContext<Synapse | null>(null);\n\nexport interface SynapseProviderProps extends SynapseOptions {\n children: ReactNode;\n}\n\nexport function SynapseProvider({ children, ...options }: SynapseProviderProps) {\n // Use a ref so the same Synapse instance survives StrictMode's\n // unmount/remount cycle. We intentionally do NOT destroy on unmount\n // because StrictMode re-mounts immediately and the transport must\n // stay alive. The instance is GC'd when the provider is truly removed.\n const ref = useRef<Synapse | null>(null);\n\n if (ref.current === null || ref.current.destroyed) {\n ref.current = createSynapse(options);\n }\n\n return (\n <SynapseContext.Provider value={ref.current}>\n <ThemeInjector synapse={ref.current} />\n {children}\n </SynapseContext.Provider>\n );\n}\n\n/** Injects theme CSS variables onto document.documentElement whenever the theme changes. */\nfunction ThemeInjector({ synapse }: { synapse: Synapse }) {\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n useEffect(() => {\n // Neutral defaults for the mode back any var the host omits, then host\n // values win — the same single path used by the handshake in core.ts.\n applyThemeVariables(theme.mode, theme.tokens);\n }, [theme]);\n\n return null;\n}\n\nexport function useSynapseContext(): Synapse {\n const ctx = useContext(SynapseContext);\n if (!ctx) {\n throw new Error(\n \"useSynapse must be used within a <SynapseProvider>. \" +\n \"Wrap your app component tree with <SynapseProvider>.\",\n );\n }\n return ctx;\n}\n","import type { McpUiHostContext } from \"@modelcontextprotocol/ext-apps\";\nimport type { Task, TaskStatus } from \"@modelcontextprotocol/sdk/types.js\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type {\n ActionReducer,\n AgentAction,\n CallToolAsTaskOptions,\n DataChangedEvent,\n FileResult,\n RequestFileOptions,\n Store,\n StoreDispatch,\n Synapse,\n SynapseTheme,\n TaskHandle,\n ToolCallResult,\n} from \"../types.js\";\nimport { SynapseProvider, useSynapseContext } from \"./provider.js\";\n\n// Re-export provider components\nexport { SynapseProvider };\n\nexport function useSynapse(): Synapse {\n return useSynapseContext();\n}\n\nexport function useCallTool<TOutput = unknown>(\n toolName: string,\n): {\n call: (args?: Record<string, unknown>) => Promise<ToolCallResult<TOutput>>;\n isPending: boolean;\n error: Error | null;\n data: TOutput | null;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TOutput | null>(null);\n const callIdRef = useRef(0);\n\n const call = useCallback(\n async (args?: Record<string, unknown>): Promise<ToolCallResult<TOutput>> => {\n const id = ++callIdRef.current;\n setIsPending(true);\n setError(null);\n\n try {\n const result = await synapse.callTool<Record<string, unknown>, TOutput>(toolName, args);\n // Stale guard: only update if this is still the latest call\n if (id === callIdRef.current) {\n setData(result.data);\n setIsPending(false);\n }\n return result;\n } catch (err) {\n if (id === callIdRef.current) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n setIsPending(false);\n }\n throw err;\n }\n },\n [synapse, toolName],\n );\n\n return { call, isPending, error, data };\n}\n\nexport function useDataSync(callback: (event: DataChangedEvent) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onDataChanged((event) => callbackRef.current(event));\n }, [synapse]);\n}\n\n/**\n * Subscribe to agent actions — typed, declarative commands from the server.\n *\n * Actions are emitted by tools as deterministic side effects (e.g., \"navigate\n * to the board I just created\"). The UI decides how to handle each action type.\n *\n * @example\n * ```tsx\n * useAgentAction((action) => {\n * if (action.type === \"navigate\") {\n * const { entity, id } = action.payload as NavigatePayload;\n * if (entity === \"board\") setSelectedBoardId(id);\n * }\n * });\n * ```\n */\nexport function useAgentAction(callback: (action: AgentAction) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onAction((action) => callbackRef.current(action));\n }, [synapse]);\n}\n\n/**\n * Subscribe to the full ext-apps host context.\n *\n * Returns the host context bag — spec-standardized fields (`theme`, `styles`,\n * `displayMode`, `toolInfo`) plus any host extensions. Re-renders on every\n * `ui/notifications/host-context-changed` notification.\n *\n * Prefer `useTheme()` when only theming matters (it filters no-op fires and\n * returns a typed `SynapseTheme`). Reach for `useHostContext()` for non-theme\n * fields like host-specific extensions, e.g. on NimbleBrain:\n *\n * ```tsx\n * const { workspace } = useHostContext<{ workspace?: { id: string } }>();\n * ```\n */\nexport function useHostContext<T extends McpUiHostContext = McpUiHostContext>(): T {\n const synapse = useSynapseContext();\n const [ctx, setCtx] = useState<T>(() => synapse.getHostContext() as T);\n\n useEffect(() => {\n // Sync in case context changed between render and effect\n setCtx(synapse.getHostContext() as T);\n return synapse.onHostContextChanged((c) => setCtx(c as T));\n }, [synapse]);\n\n return ctx;\n}\n\n/**\n * Re-renders only when the derived `SynapseTheme` actually changes (mode,\n * primaryColor, or any token value). Routes through `synapse.onThemeChanged`\n * — which is itself a `themesEqual`-filtered selector over the unified host\n * context — so a host-context update that doesn't move the theme (e.g. a\n * workspace switch) does NOT cause this hook's consumers to re-render.\n *\n * Building this on `useHostContext` instead would skip the filter: every\n * host-context-changed notification produces a new context reference, which\n * would propagate through `useState` and force a re-render even when the\n * derived theme is unchanged.\n */\nexport function useTheme(): SynapseTheme {\n const synapse = useSynapseContext();\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n // Sync in case theme changed between render and effect\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n return theme;\n}\n\nexport function useAction(): (action: string, params?: Record<string, unknown>) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (action: string, params?: Record<string, unknown>) => synapse.action(action, params),\n [synapse],\n );\n}\n\nexport function useChat(): (\n message: string,\n context?: { action?: string; entity?: string },\n) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (message: string, context?: { action?: string; entity?: string }) =>\n synapse.chat(message, context),\n [synapse],\n );\n}\n\n/**\n * Push the app's visible state to the agent via ext-apps `ui/update-model-context`.\n *\n * **Imperative** (no args) — returns a push function you call manually:\n * ```tsx\n * const push = useVisibleState();\n * push({ board: selectedBoard }, \"Viewing board X\");\n * ```\n *\n * **Declarative** (factory + deps) — auto-pushes when deps change:\n * ```tsx\n * useVisibleState(() => ({\n * state: { board: selectedBoard },\n * summary: `Viewing \"${selectedBoard?.name}\"`,\n * }), [selectedBoard]);\n * ```\n */\nexport function useVisibleState(): (state: Record<string, unknown>, summary?: string) => void;\nexport function useVisibleState(\n factory: () => { state: Record<string, unknown>; summary?: string },\n deps: unknown[],\n): void;\nexport function useVisibleState(\n factory?: () => { state: Record<string, unknown>; summary?: string },\n deps?: unknown[],\n): ((state: Record<string, unknown>, summary?: string) => void) | undefined {\n const synapse = useSynapseContext();\n const push = useCallback(\n (state: Record<string, unknown>, summary?: string) => synapse.setVisibleState(state, summary),\n [synapse],\n );\n\n // Declarative mode: auto-push when deps change.\n // The deps array is caller-provided (mirrors useMemo/useEffect pattern).\n const factoryRef = useRef(factory);\n factoryRef.current = factory;\n useEffect(() => {\n if (!factoryRef.current) return;\n const { state, summary } = factoryRef.current();\n push(state, summary);\n }, [...(deps ?? []), push]);\n\n if (!factory) return push;\n}\n\nexport function useFileUpload(): {\n pickFile: (options?: RequestFileOptions) => Promise<FileResult | null>;\n pickFiles: (options?: RequestFileOptions) => Promise<FileResult[]>;\n isPending: boolean;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n\n const pickFile = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFile(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n const pickFiles = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFiles(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n return { pickFile, pickFiles, isPending };\n}\n\nexport function useStore<TState, TActions extends Record<string, ActionReducer<TState, any>>>(\n store: Store<TState, TActions>,\n): {\n state: TState;\n dispatch: StoreDispatch<TActions>;\n} {\n const state = useSyncExternalStore(\n (onStoreChange) => store.subscribe(onStoreChange),\n () => store.getState(),\n () => store.getState(),\n );\n\n return { state, dispatch: store.dispatch };\n}\n\n// -----------------------------------------------------------------------------\n// useCallToolAsTask — lifecycle wrapper around `synapse.callToolAsTask`\n// -----------------------------------------------------------------------------\n\n/**\n * Spec terminal status values for the MCP 2025-11-25 tasks utility.\n *\n * Typed via `satisfies TaskStatus` so a rename of any member of the\n * spec enum (`completed`/`failed`/`cancelled`) trips `tsc` — we never\n * hand-type these as bare string literals in comparisons.\n */\nconst COMPLETED_STATUS = \"completed\" satisfies TaskStatus;\nconst FAILED_STATUS = \"failed\" satisfies TaskStatus;\nconst CANCELLED_STATUS = \"cancelled\" satisfies TaskStatus;\n\nconst TERMINAL_STATUSES: ReadonlySet<TaskStatus> = new Set<TaskStatus>([\n COMPLETED_STATUS,\n FAILED_STATUS,\n CANCELLED_STATUS,\n]);\n\n/**\n * Fallback poll cadence used when the receiver's `CreateTaskResult.task`\n * carries no `pollInterval`. Chosen to roughly match the 5s cadence\n * described in Task 005 (\"a sensible default like 5s if pollInterval is\n * absent\") — the effective fire delay is this value × 1.5 ≈ 7.5s, well\n * below default TTLs but long enough to avoid hammering hosts that do\n * emit `notifications/tasks/status`.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst POLL_FALLBACK_MULTIPLIER = 1.5;\n\n/**\n * Stop the poll fallback after this many consecutive `refresh()` failures.\n * Bridge teardown / TTL eviction / network outage all manifest as repeated\n * `tasks/get` rejections. Without a guard the timer re-arms forever; with\n * it we stop polling silently after `MAX_REFRESH_FAILURES` strikes — the\n * blocking `result()` path remains the authoritative source of truth, so\n * giving up on polling never loses the terminal value.\n */\nconst MAX_REFRESH_FAILURES = 5;\n\nexport interface UseCallToolAsTaskResult<TInput, TOutput> {\n /**\n * Start (or re-start) a task-augmented tool call. Returns the\n * resolved `TaskHandle` so callers can `await fire(...)` if they\n * want to know when the server has accepted the task, but reading\n * `task`/`result`/`error` from the hook is usually enough.\n *\n * Re-firing while a previous task is still in flight detaches this\n * hook from the prior handle (stops polling, unsubscribes) but does\n * NOT cancel the server-side task — the task keeps running and its\n * result may still be fetched elsewhere (e.g. on page revisit).\n */\n fire(args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>;\n /** Latest `Task` state, or `null` before `fire()` has been called. */\n task: Task | null;\n /** Populated once `handle.result()` resolves non-error. */\n result: ToolCallResult<TOutput> | null;\n /** Populated on rejection or when `result.isError === true`. */\n error: Error | null;\n /** `true` while the task is non-terminal (`working` / `input_required`). */\n isWorking: boolean;\n /** `true` when `task.status ∈ {completed, failed, cancelled}`. */\n isTerminal: boolean;\n /**\n * Cancel the active task via `tasks/cancel`. No-op when no task is\n * active. Swallowed errors surface via `error`.\n */\n cancel(): Promise<void>;\n}\n\n/**\n * React hook wrapper around `synapse.callToolAsTask`.\n *\n * Handles the full MCP 2025-11-25 task lifecycle:\n *\n * 1. `fire(args, options?)` sends the task-augmented `tools/call` and\n * stores the returned `TaskHandle` in a ref.\n * 2. Subscribes to `handle.onStatus` — updates `task` whenever the\n * host emits `notifications/tasks/status` (OPTIONAL per spec).\n * 3. Starts a polling fallback: if no status notification arrives\n * within `pollInterval × 1.5` (defaulting to ~7.5s), calls\n * `handle.refresh()` for canonical state. Stops on terminal.\n * 4. Awaits `handle.result()` in the background — resolves to either\n * `result` (success / `isError: false`) or `error` (network reject\n * OR `result.isError === true`).\n *\n * Cleanup (unmount or re-fire) unsubscribes from status events and\n * clears the poll timer, but does NOT cancel the server-side task —\n * the caller may remount and recover state by firing again, and tasks\n * outlive iframe teardown until TTL elapses.\n */\nexport function useCallToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(\n toolName: string,\n): UseCallToolAsTaskResult<TInput, TOutput> {\n const synapse = useSynapseContext();\n\n const [task, setTask] = useState<Task | null>(null);\n const [result, setResult] = useState<ToolCallResult<TOutput> | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Per-fire generation counter. Every `fire()` increments; any\n // asynchronous callback (status listener, poll timer, `result()`\n // resolution) captures the gen at schedule time and bails if the\n // current gen has moved past it. This is the single source of truth\n // for \"is this work still relevant?\" — more robust than comparing\n // TaskHandle identity because handles can be detached by re-fire.\n const genRef = useRef(0);\n\n // Active handle + its teardown handles. We keep both in refs so the\n // hook's stable `fire`/`cancel` callbacks can reach the current\n // lifecycle state without re-binding on every render.\n const handleRef = useRef<TaskHandle<TOutput> | null>(null);\n const unsubscribeRef = useRef<(() => void) | null>(null);\n const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // `pollInterval × 1.5`, captured per-fire. Falls back to the 5s\n // default when the host didn't provide a `pollInterval` in the\n // initial CreateTaskResult.task.\n const pollDelayRef = useRef<number>(DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER);\n\n // Track the latest known status out-of-band so the poll callback\n // can decide whether to keep polling without depending on the\n // `task` React state (which lags a render behind setState).\n const terminalRef = useRef<boolean>(false);\n\n // Consecutive `refresh()` failure count for the active fire. Reset\n // on every successful refresh, status notification, or new fire.\n const refreshFailureCountRef = useRef<number>(0);\n\n const clearPollTimer = useCallback(() => {\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n }, []);\n\n const detachCurrent = useCallback(() => {\n clearPollTimer();\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n }, [clearPollTimer]);\n\n const scheduleNextPoll = useCallback(\n (gen: number) => {\n clearPollTimer();\n if (terminalRef.current) return;\n pollTimerRef.current = setTimeout(() => {\n // Bail if this fire has been superseded or torn down.\n if (gen !== genRef.current) return;\n const h = handleRef.current;\n if (!h) return;\n if (terminalRef.current) return;\n // `refresh()` is the canonical source for `createdAt` /\n // `lastUpdatedAt` / `ttl` — notification-derived Tasks carry\n // placeholders per the router in `task-handle.ts`.\n h.refresh().then(\n (fresh) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(fresh);\n const isTerminal = TERMINAL_STATUSES.has(fresh.status);\n terminalRef.current = isTerminal;\n if (!isTerminal) scheduleNextPoll(gen);\n },\n () => {\n // Swallow refresh errors — the blocking `tasks/result` is\n // the authoritative path; polling is best-effort. But guard\n // against runaway re-arming: if the bridge is gone or the\n // task TTL has elapsed, every refresh rejects. Stop after\n // MAX_REFRESH_FAILURES consecutive strikes; result() will\n // still surface a terminal value or error when it settles.\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current += 1;\n if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) return;\n if (!terminalRef.current) scheduleNextPoll(gen);\n },\n );\n }, pollDelayRef.current);\n },\n [clearPollTimer],\n );\n\n const fire = useCallback(\n async (args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>> => {\n // Detach any in-flight prior task BEFORE incrementing the gen so\n // its callbacks see the new gen and bail. (Incrementing then\n // detaching would also work, but detach-first makes the order\n // obvious: stop listening, bump generation, start fresh.)\n detachCurrent();\n const gen = ++genRef.current;\n\n // Reset per-fire state. Don't wipe `task` yet — `callToolAsTask`\n // is async; showing the previous terminal state briefly is less\n // jarring than flicker to null → working. We clear on resolution.\n setResult(null);\n setError(null);\n terminalRef.current = false;\n refreshFailureCountRef.current = 0;\n\n let handle: TaskHandle<TOutput>;\n try {\n handle = await synapse.callToolAsTask<TInput, TOutput>(toolName, args, options);\n } catch (err) {\n if (gen !== genRef.current) throw err;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n throw err;\n }\n\n // Caller superseded the fire between request and response —\n // don't attach listeners, but still return the handle so the\n // awaiter can observe it.\n if (gen !== genRef.current) return handle;\n\n handleRef.current = handle;\n\n // Derive the fallback poll delay from the receiver's advertised\n // `pollInterval`. Spec allows it to be absent; we then use the\n // 5s default described in Task 005.\n const hintedInterval = handle.task.pollInterval;\n pollDelayRef.current =\n typeof hintedInterval === \"number\" && hintedInterval > 0\n ? hintedInterval * POLL_FALLBACK_MULTIPLIER\n : DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER;\n\n setTask(handle.task);\n terminalRef.current = TERMINAL_STATUSES.has(handle.task.status);\n\n // Subscribe to `notifications/tasks/status`. Each notification\n // resets the poll countdown (that's the whole point of the\n // \"notification OR polling\" contract — if notifications flow,\n // we don't poll; if they don't, the timer fires).\n unsubscribeRef.current = handle.onStatus((updated) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(updated);\n const isTerminal = TERMINAL_STATUSES.has(updated.status);\n terminalRef.current = isTerminal;\n if (isTerminal) {\n clearPollTimer();\n } else {\n scheduleNextPoll(gen);\n }\n });\n\n // Kick off the blocking result fetch — this is the authoritative\n // terminal value regardless of whether notifications or polls\n // landed in between. By spec, `tasks/result` blocks until the task\n // reaches a terminal status, so when this settles we KNOW the task\n // is terminal — stop polling and synthesize a terminal `task`\n // status so derived flags (`isTerminal`, `isWorking`) match the\n // populated `result` / `error` immediately.\n handle.result().then(\n (res) => {\n if (gen !== genRef.current) return;\n terminalRef.current = true;\n clearPollTimer();\n // Synthesize the terminal Task: failed if `isError`, otherwise\n // completed. The next status notification or refresh would\n // confirm this, but we want internal state consistent the\n // instant `result` is populated — a \"result populated while\n // isWorking=true\" render is incoherent for consumers.\n setTask((prev) => {\n const status: TaskStatus = res.isError ? \"failed\" : \"completed\";\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status, lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status,\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n if (res.isError) {\n // Spec: `CallToolResult.isError === true` is a tool-level\n // error, not a protocol error. Surface via `error` for\n // consumers who treat it as a failure, but also populate\n // `result` so callers inspecting the raw content block\n // still have access.\n setResult(res);\n const msg =\n typeof res.data === \"string\" && res.data.length > 0\n ? res.data\n : `Tool \"${toolName}\" returned isError: true`;\n setError(new Error(msg));\n } else {\n setResult(res);\n }\n },\n (err) => {\n if (gen !== genRef.current) return;\n // result() rejection means the `tasks/result` RPC failed\n // (transport error, taskId not found, bridge teardown). We\n // can't know the server-side task's actual final state, but\n // we know polling won't recover here either — same transport.\n // Mark terminal and synthesize `failed` status for UX\n // coherence; the populated `error` tells the consumer what\n // specifically went wrong.\n terminalRef.current = true;\n clearPollTimer();\n setTask((prev) => {\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status: \"failed\", lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status: \"failed\",\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n },\n );\n\n // Start the poll fallback only if we aren't already terminal.\n if (!terminalRef.current) scheduleNextPoll(gen);\n\n return handle;\n },\n [synapse, toolName, detachCurrent, clearPollTimer, scheduleNextPoll],\n );\n\n const cancel = useCallback(async (): Promise<void> => {\n const h = handleRef.current;\n if (!h) return;\n const gen = genRef.current;\n try {\n const cancelled = await h.cancel();\n if (gen !== genRef.current) return;\n setTask(cancelled);\n terminalRef.current = TERMINAL_STATUSES.has(cancelled.status);\n clearPollTimer();\n } catch (err) {\n if (gen !== genRef.current) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n }\n }, [clearPollTimer]);\n\n // Cleanup on unmount: stop polling, drop the status subscription.\n // Deliberately do NOT call `handle.cancel()` — per Task 005, the\n // server-side task keeps running so a remount can recover state.\n useEffect(() => {\n return () => {\n genRef.current += 1;\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n };\n }, []);\n\n const isTerminal = task !== null && TERMINAL_STATUSES.has(task.status);\n const isWorking = task !== null && !isTerminal;\n\n return { fire, task, result, error, isWorking, isTerminal, cancel };\n}\n"]} | ||
| {"version":3,"sources":["../../src/react/app-provider.tsx","../../src/react/connect-hooks.ts","../../src/react/provider.tsx","../../src/react/hooks.ts"],"names":["createContext","useState","useRef","useEffect","connect","useContext","useCallback","createSynapse","jsx","applyTheme","useSyncExternalStore","isTerminal"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { connect } from \"../connect.js\";\nimport type { App, ConnectOptions } from \"../types.js\";\n\nconst AppContext = createContext<App | null>(null);\n\nexport interface AppProviderProps extends ConnectOptions {\n children: ReactNode;\n}\n\nexport function AppProvider({ children, name, version, autoResize }: AppProviderProps) {\n const [app, setApp] = useState<App | null>(null);\n const connectingRef = useRef(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: connect once on mount\n useEffect(() => {\n if (connectingRef.current) return;\n connectingRef.current = true;\n\n connect({ name, version, autoResize }).then((a) => {\n setApp(a);\n });\n }, []);\n\n if (!app) return null; // Don't render children until connected\n\n return <AppContext.Provider value={app}>{children}</AppContext.Provider>;\n}\n\nexport function useAppContext(): App {\n const ctx = useContext(AppContext);\n if (!ctx) {\n throw new Error(\n \"useApp must be used within an <AppProvider>. Wrap your component tree with <AppProvider>.\",\n );\n }\n return ctx;\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { App, Theme, ToolResultData } from \"../types.js\";\nimport { AppProvider, useAppContext } from \"./app-provider.js\";\n\nexport { AppProvider };\n\nexport function useApp(): App {\n return useAppContext();\n}\n\nexport function useToolResult(): ToolResultData | null {\n const app = useAppContext();\n const [data, setData] = useState<ToolResultData | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-result\", (result: ToolResultData) => {\n setData(result);\n });\n }, [app]);\n\n return data;\n}\n\nexport function useToolInput(): Record<string, unknown> | null {\n const app = useAppContext();\n const [input, setInput] = useState<Record<string, unknown> | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-input\", (args: Record<string, unknown>) => {\n setInput(args);\n });\n }, [app]);\n\n return input;\n}\n\nexport function useConnectTheme(): Theme {\n const app = useAppContext();\n const [theme, setTheme] = useState<Theme>(() => app.theme);\n\n useEffect(() => {\n setTheme(app.theme); // Sync in case it changed between render and effect\n return app.on(\"theme-changed\", (t: Theme) => {\n setTheme(t);\n });\n }, [app]);\n\n return theme;\n}\n\nexport function useResize(): (width?: number, height?: number) => void {\n const app = useAppContext();\n return useCallback((width?: number, height?: number) => app.resize(width, height), [app]);\n}\n","import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { createSynapse } from \"../core.js\";\nimport { applyTheme } from \"../theme-defaults.js\";\nimport type { Synapse, SynapseOptions, SynapseTheme } from \"../types.js\";\n\nconst SynapseContext = createContext<Synapse | null>(null);\n\nexport interface SynapseProviderProps extends SynapseOptions {\n children: ReactNode;\n}\n\nexport function SynapseProvider({ children, ...options }: SynapseProviderProps) {\n // Use a ref so the same Synapse instance survives StrictMode's\n // unmount/remount cycle. We intentionally do NOT destroy on unmount\n // because StrictMode re-mounts immediately and the transport must\n // stay alive. The instance is GC'd when the provider is truly removed.\n const ref = useRef<Synapse | null>(null);\n\n if (ref.current === null || ref.current.destroyed) {\n ref.current = createSynapse(options);\n }\n\n return (\n <SynapseContext.Provider value={ref.current}>\n <ThemeInjector synapse={ref.current} />\n {children}\n </SynapseContext.Provider>\n );\n}\n\n/** Injects theme CSS variables onto document.documentElement whenever the theme changes. */\nfunction ThemeInjector({ synapse }: { synapse: Synapse }) {\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n useEffect(() => {\n // Neutral defaults for the mode back any var the host omits, then host\n // values win — the same single path used by the handshake in core.ts.\n applyTheme(theme.mode, theme.tokens, theme.fontFaces);\n }, [theme]);\n\n return null;\n}\n\nexport function useSynapseContext(): Synapse {\n const ctx = useContext(SynapseContext);\n if (!ctx) {\n throw new Error(\n \"useSynapse must be used within a <SynapseProvider>. \" +\n \"Wrap your app component tree with <SynapseProvider>.\",\n );\n }\n return ctx;\n}\n","import type { McpUiHostContext } from \"@modelcontextprotocol/ext-apps\";\nimport type { Task, TaskStatus } from \"@modelcontextprotocol/sdk/types.js\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type {\n ActionReducer,\n AgentAction,\n CallToolAsTaskOptions,\n DataChangedEvent,\n FileResult,\n RequestFileOptions,\n Store,\n StoreDispatch,\n Synapse,\n SynapseTheme,\n TaskHandle,\n ToolCallResult,\n} from \"../types.js\";\nimport { SynapseProvider, useSynapseContext } from \"./provider.js\";\n\n// Re-export provider components\nexport { SynapseProvider };\n\nexport function useSynapse(): Synapse {\n return useSynapseContext();\n}\n\nexport function useCallTool<TOutput = unknown>(\n toolName: string,\n): {\n call: (args?: Record<string, unknown>) => Promise<ToolCallResult<TOutput>>;\n isPending: boolean;\n error: Error | null;\n data: TOutput | null;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TOutput | null>(null);\n const callIdRef = useRef(0);\n\n const call = useCallback(\n async (args?: Record<string, unknown>): Promise<ToolCallResult<TOutput>> => {\n const id = ++callIdRef.current;\n setIsPending(true);\n setError(null);\n\n try {\n const result = await synapse.callTool<Record<string, unknown>, TOutput>(toolName, args);\n // Stale guard: only update if this is still the latest call\n if (id === callIdRef.current) {\n setData(result.data);\n setIsPending(false);\n }\n return result;\n } catch (err) {\n if (id === callIdRef.current) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n setIsPending(false);\n }\n throw err;\n }\n },\n [synapse, toolName],\n );\n\n return { call, isPending, error, data };\n}\n\nexport function useDataSync(callback: (event: DataChangedEvent) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onDataChanged((event) => callbackRef.current(event));\n }, [synapse]);\n}\n\n/**\n * Subscribe to agent actions — typed, declarative commands from the server.\n *\n * Actions are emitted by tools as deterministic side effects (e.g., \"navigate\n * to the board I just created\"). The UI decides how to handle each action type.\n *\n * @example\n * ```tsx\n * useAgentAction((action) => {\n * if (action.type === \"navigate\") {\n * const { entity, id } = action.payload as NavigatePayload;\n * if (entity === \"board\") setSelectedBoardId(id);\n * }\n * });\n * ```\n */\nexport function useAgentAction(callback: (action: AgentAction) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onAction((action) => callbackRef.current(action));\n }, [synapse]);\n}\n\n/**\n * Subscribe to the full ext-apps host context.\n *\n * Returns the host context bag — spec-standardized fields (`theme`, `styles`,\n * `displayMode`, `toolInfo`) plus any host extensions. Re-renders on every\n * `ui/notifications/host-context-changed` notification.\n *\n * Prefer `useTheme()` when only theming matters (it filters no-op fires and\n * returns a typed `SynapseTheme`). Reach for `useHostContext()` for non-theme\n * fields like host-specific extensions, e.g. on NimbleBrain:\n *\n * ```tsx\n * const { workspace } = useHostContext<{ workspace?: { id: string } }>();\n * ```\n */\nexport function useHostContext<T extends McpUiHostContext = McpUiHostContext>(): T {\n const synapse = useSynapseContext();\n const [ctx, setCtx] = useState<T>(() => synapse.getHostContext() as T);\n\n useEffect(() => {\n // Sync in case context changed between render and effect\n setCtx(synapse.getHostContext() as T);\n return synapse.onHostContextChanged((c) => setCtx(c as T));\n }, [synapse]);\n\n return ctx;\n}\n\n/**\n * Re-renders only when the derived `SynapseTheme` actually changes (mode,\n * primaryColor, or any token value). Routes through `synapse.onThemeChanged`\n * — which is itself a `themesEqual`-filtered selector over the unified host\n * context — so a host-context update that doesn't move the theme (e.g. a\n * workspace switch) does NOT cause this hook's consumers to re-render.\n *\n * Building this on `useHostContext` instead would skip the filter: every\n * host-context-changed notification produces a new context reference, which\n * would propagate through `useState` and force a re-render even when the\n * derived theme is unchanged.\n */\nexport function useTheme(): SynapseTheme {\n const synapse = useSynapseContext();\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n // Sync in case theme changed between render and effect\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n return theme;\n}\n\nexport function useAction(): (action: string, params?: Record<string, unknown>) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (action: string, params?: Record<string, unknown>) => synapse.action(action, params),\n [synapse],\n );\n}\n\nexport function useChat(): (\n message: string,\n context?: { action?: string; entity?: string },\n) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (message: string, context?: { action?: string; entity?: string }) =>\n synapse.chat(message, context),\n [synapse],\n );\n}\n\n/**\n * Push the app's visible state to the agent via ext-apps `ui/update-model-context`.\n *\n * **Imperative** (no args) — returns a push function you call manually:\n * ```tsx\n * const push = useVisibleState();\n * push({ board: selectedBoard }, \"Viewing board X\");\n * ```\n *\n * **Declarative** (factory + deps) — auto-pushes when deps change:\n * ```tsx\n * useVisibleState(() => ({\n * state: { board: selectedBoard },\n * summary: `Viewing \"${selectedBoard?.name}\"`,\n * }), [selectedBoard]);\n * ```\n */\nexport function useVisibleState(): (state: Record<string, unknown>, summary?: string) => void;\nexport function useVisibleState(\n factory: () => { state: Record<string, unknown>; summary?: string },\n deps: unknown[],\n): void;\nexport function useVisibleState(\n factory?: () => { state: Record<string, unknown>; summary?: string },\n deps?: unknown[],\n): ((state: Record<string, unknown>, summary?: string) => void) | undefined {\n const synapse = useSynapseContext();\n const push = useCallback(\n (state: Record<string, unknown>, summary?: string) => synapse.setVisibleState(state, summary),\n [synapse],\n );\n\n // Declarative mode: auto-push when deps change.\n // The deps array is caller-provided (mirrors useMemo/useEffect pattern).\n const factoryRef = useRef(factory);\n factoryRef.current = factory;\n useEffect(() => {\n if (!factoryRef.current) return;\n const { state, summary } = factoryRef.current();\n push(state, summary);\n }, [...(deps ?? []), push]);\n\n if (!factory) return push;\n}\n\nexport function useFileUpload(): {\n pickFile: (options?: RequestFileOptions) => Promise<FileResult | null>;\n pickFiles: (options?: RequestFileOptions) => Promise<FileResult[]>;\n isPending: boolean;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n\n const pickFile = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFile(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n const pickFiles = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFiles(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n return { pickFile, pickFiles, isPending };\n}\n\nexport function useStore<TState, TActions extends Record<string, ActionReducer<TState, any>>>(\n store: Store<TState, TActions>,\n): {\n state: TState;\n dispatch: StoreDispatch<TActions>;\n} {\n const state = useSyncExternalStore(\n (onStoreChange) => store.subscribe(onStoreChange),\n () => store.getState(),\n () => store.getState(),\n );\n\n return { state, dispatch: store.dispatch };\n}\n\n// -----------------------------------------------------------------------------\n// useCallToolAsTask — lifecycle wrapper around `synapse.callToolAsTask`\n// -----------------------------------------------------------------------------\n\n/**\n * Spec terminal status values for the MCP 2025-11-25 tasks utility.\n *\n * Typed via `satisfies TaskStatus` so a rename of any member of the\n * spec enum (`completed`/`failed`/`cancelled`) trips `tsc` — we never\n * hand-type these as bare string literals in comparisons.\n */\nconst COMPLETED_STATUS = \"completed\" satisfies TaskStatus;\nconst FAILED_STATUS = \"failed\" satisfies TaskStatus;\nconst CANCELLED_STATUS = \"cancelled\" satisfies TaskStatus;\n\nconst TERMINAL_STATUSES: ReadonlySet<TaskStatus> = new Set<TaskStatus>([\n COMPLETED_STATUS,\n FAILED_STATUS,\n CANCELLED_STATUS,\n]);\n\n/**\n * Fallback poll cadence used when the receiver's `CreateTaskResult.task`\n * carries no `pollInterval`. Chosen to roughly match the 5s cadence\n * described in Task 005 (\"a sensible default like 5s if pollInterval is\n * absent\") — the effective fire delay is this value × 1.5 ≈ 7.5s, well\n * below default TTLs but long enough to avoid hammering hosts that do\n * emit `notifications/tasks/status`.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst POLL_FALLBACK_MULTIPLIER = 1.5;\n\n/**\n * Stop the poll fallback after this many consecutive `refresh()` failures.\n * Bridge teardown / TTL eviction / network outage all manifest as repeated\n * `tasks/get` rejections. Without a guard the timer re-arms forever; with\n * it we stop polling silently after `MAX_REFRESH_FAILURES` strikes — the\n * blocking `result()` path remains the authoritative source of truth, so\n * giving up on polling never loses the terminal value.\n */\nconst MAX_REFRESH_FAILURES = 5;\n\nexport interface UseCallToolAsTaskResult<TInput, TOutput> {\n /**\n * Start (or re-start) a task-augmented tool call. Returns the\n * resolved `TaskHandle` so callers can `await fire(...)` if they\n * want to know when the server has accepted the task, but reading\n * `task`/`result`/`error` from the hook is usually enough.\n *\n * Re-firing while a previous task is still in flight detaches this\n * hook from the prior handle (stops polling, unsubscribes) but does\n * NOT cancel the server-side task — the task keeps running and its\n * result may still be fetched elsewhere (e.g. on page revisit).\n */\n fire(args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>;\n /** Latest `Task` state, or `null` before `fire()` has been called. */\n task: Task | null;\n /** Populated once `handle.result()` resolves non-error. */\n result: ToolCallResult<TOutput> | null;\n /** Populated on rejection or when `result.isError === true`. */\n error: Error | null;\n /** `true` while the task is non-terminal (`working` / `input_required`). */\n isWorking: boolean;\n /** `true` when `task.status ∈ {completed, failed, cancelled}`. */\n isTerminal: boolean;\n /**\n * Cancel the active task via `tasks/cancel`. No-op when no task is\n * active. Swallowed errors surface via `error`.\n */\n cancel(): Promise<void>;\n}\n\n/**\n * React hook wrapper around `synapse.callToolAsTask`.\n *\n * Handles the full MCP 2025-11-25 task lifecycle:\n *\n * 1. `fire(args, options?)` sends the task-augmented `tools/call` and\n * stores the returned `TaskHandle` in a ref.\n * 2. Subscribes to `handle.onStatus` — updates `task` whenever the\n * host emits `notifications/tasks/status` (OPTIONAL per spec).\n * 3. Starts a polling fallback: if no status notification arrives\n * within `pollInterval × 1.5` (defaulting to ~7.5s), calls\n * `handle.refresh()` for canonical state. Stops on terminal.\n * 4. Awaits `handle.result()` in the background — resolves to either\n * `result` (success / `isError: false`) or `error` (network reject\n * OR `result.isError === true`).\n *\n * Cleanup (unmount or re-fire) unsubscribes from status events and\n * clears the poll timer, but does NOT cancel the server-side task —\n * the caller may remount and recover state by firing again, and tasks\n * outlive iframe teardown until TTL elapses.\n */\nexport function useCallToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(\n toolName: string,\n): UseCallToolAsTaskResult<TInput, TOutput> {\n const synapse = useSynapseContext();\n\n const [task, setTask] = useState<Task | null>(null);\n const [result, setResult] = useState<ToolCallResult<TOutput> | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Per-fire generation counter. Every `fire()` increments; any\n // asynchronous callback (status listener, poll timer, `result()`\n // resolution) captures the gen at schedule time and bails if the\n // current gen has moved past it. This is the single source of truth\n // for \"is this work still relevant?\" — more robust than comparing\n // TaskHandle identity because handles can be detached by re-fire.\n const genRef = useRef(0);\n\n // Active handle + its teardown handles. We keep both in refs so the\n // hook's stable `fire`/`cancel` callbacks can reach the current\n // lifecycle state without re-binding on every render.\n const handleRef = useRef<TaskHandle<TOutput> | null>(null);\n const unsubscribeRef = useRef<(() => void) | null>(null);\n const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // `pollInterval × 1.5`, captured per-fire. Falls back to the 5s\n // default when the host didn't provide a `pollInterval` in the\n // initial CreateTaskResult.task.\n const pollDelayRef = useRef<number>(DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER);\n\n // Track the latest known status out-of-band so the poll callback\n // can decide whether to keep polling without depending on the\n // `task` React state (which lags a render behind setState).\n const terminalRef = useRef<boolean>(false);\n\n // Consecutive `refresh()` failure count for the active fire. Reset\n // on every successful refresh, status notification, or new fire.\n const refreshFailureCountRef = useRef<number>(0);\n\n const clearPollTimer = useCallback(() => {\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n }, []);\n\n const detachCurrent = useCallback(() => {\n clearPollTimer();\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n }, [clearPollTimer]);\n\n const scheduleNextPoll = useCallback(\n (gen: number) => {\n clearPollTimer();\n if (terminalRef.current) return;\n pollTimerRef.current = setTimeout(() => {\n // Bail if this fire has been superseded or torn down.\n if (gen !== genRef.current) return;\n const h = handleRef.current;\n if (!h) return;\n if (terminalRef.current) return;\n // `refresh()` is the canonical source for `createdAt` /\n // `lastUpdatedAt` / `ttl` — notification-derived Tasks carry\n // placeholders per the router in `task-handle.ts`.\n h.refresh().then(\n (fresh) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(fresh);\n const isTerminal = TERMINAL_STATUSES.has(fresh.status);\n terminalRef.current = isTerminal;\n if (!isTerminal) scheduleNextPoll(gen);\n },\n () => {\n // Swallow refresh errors — the blocking `tasks/result` is\n // the authoritative path; polling is best-effort. But guard\n // against runaway re-arming: if the bridge is gone or the\n // task TTL has elapsed, every refresh rejects. Stop after\n // MAX_REFRESH_FAILURES consecutive strikes; result() will\n // still surface a terminal value or error when it settles.\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current += 1;\n if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) return;\n if (!terminalRef.current) scheduleNextPoll(gen);\n },\n );\n }, pollDelayRef.current);\n },\n [clearPollTimer],\n );\n\n const fire = useCallback(\n async (args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>> => {\n // Detach any in-flight prior task BEFORE incrementing the gen so\n // its callbacks see the new gen and bail. (Incrementing then\n // detaching would also work, but detach-first makes the order\n // obvious: stop listening, bump generation, start fresh.)\n detachCurrent();\n const gen = ++genRef.current;\n\n // Reset per-fire state. Don't wipe `task` yet — `callToolAsTask`\n // is async; showing the previous terminal state briefly is less\n // jarring than flicker to null → working. We clear on resolution.\n setResult(null);\n setError(null);\n terminalRef.current = false;\n refreshFailureCountRef.current = 0;\n\n let handle: TaskHandle<TOutput>;\n try {\n handle = await synapse.callToolAsTask<TInput, TOutput>(toolName, args, options);\n } catch (err) {\n if (gen !== genRef.current) throw err;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n throw err;\n }\n\n // Caller superseded the fire between request and response —\n // don't attach listeners, but still return the handle so the\n // awaiter can observe it.\n if (gen !== genRef.current) return handle;\n\n handleRef.current = handle;\n\n // Derive the fallback poll delay from the receiver's advertised\n // `pollInterval`. Spec allows it to be absent; we then use the\n // 5s default described in Task 005.\n const hintedInterval = handle.task.pollInterval;\n pollDelayRef.current =\n typeof hintedInterval === \"number\" && hintedInterval > 0\n ? hintedInterval * POLL_FALLBACK_MULTIPLIER\n : DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER;\n\n setTask(handle.task);\n terminalRef.current = TERMINAL_STATUSES.has(handle.task.status);\n\n // Subscribe to `notifications/tasks/status`. Each notification\n // resets the poll countdown (that's the whole point of the\n // \"notification OR polling\" contract — if notifications flow,\n // we don't poll; if they don't, the timer fires).\n unsubscribeRef.current = handle.onStatus((updated) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(updated);\n const isTerminal = TERMINAL_STATUSES.has(updated.status);\n terminalRef.current = isTerminal;\n if (isTerminal) {\n clearPollTimer();\n } else {\n scheduleNextPoll(gen);\n }\n });\n\n // Kick off the blocking result fetch — this is the authoritative\n // terminal value regardless of whether notifications or polls\n // landed in between. By spec, `tasks/result` blocks until the task\n // reaches a terminal status, so when this settles we KNOW the task\n // is terminal — stop polling and synthesize a terminal `task`\n // status so derived flags (`isTerminal`, `isWorking`) match the\n // populated `result` / `error` immediately.\n handle.result().then(\n (res) => {\n if (gen !== genRef.current) return;\n terminalRef.current = true;\n clearPollTimer();\n // Synthesize the terminal Task: failed if `isError`, otherwise\n // completed. The next status notification or refresh would\n // confirm this, but we want internal state consistent the\n // instant `result` is populated — a \"result populated while\n // isWorking=true\" render is incoherent for consumers.\n setTask((prev) => {\n const status: TaskStatus = res.isError ? \"failed\" : \"completed\";\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status, lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status,\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n if (res.isError) {\n // Spec: `CallToolResult.isError === true` is a tool-level\n // error, not a protocol error. Surface via `error` for\n // consumers who treat it as a failure, but also populate\n // `result` so callers inspecting the raw content block\n // still have access.\n setResult(res);\n const msg =\n typeof res.data === \"string\" && res.data.length > 0\n ? res.data\n : `Tool \"${toolName}\" returned isError: true`;\n setError(new Error(msg));\n } else {\n setResult(res);\n }\n },\n (err) => {\n if (gen !== genRef.current) return;\n // result() rejection means the `tasks/result` RPC failed\n // (transport error, taskId not found, bridge teardown). We\n // can't know the server-side task's actual final state, but\n // we know polling won't recover here either — same transport.\n // Mark terminal and synthesize `failed` status for UX\n // coherence; the populated `error` tells the consumer what\n // specifically went wrong.\n terminalRef.current = true;\n clearPollTimer();\n setTask((prev) => {\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status: \"failed\", lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status: \"failed\",\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n },\n );\n\n // Start the poll fallback only if we aren't already terminal.\n if (!terminalRef.current) scheduleNextPoll(gen);\n\n return handle;\n },\n [synapse, toolName, detachCurrent, clearPollTimer, scheduleNextPoll],\n );\n\n const cancel = useCallback(async (): Promise<void> => {\n const h = handleRef.current;\n if (!h) return;\n const gen = genRef.current;\n try {\n const cancelled = await h.cancel();\n if (gen !== genRef.current) return;\n setTask(cancelled);\n terminalRef.current = TERMINAL_STATUSES.has(cancelled.status);\n clearPollTimer();\n } catch (err) {\n if (gen !== genRef.current) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n }\n }, [clearPollTimer]);\n\n // Cleanup on unmount: stop polling, drop the status subscription.\n // Deliberately do NOT call `handle.cancel()` — per Task 005, the\n // server-side task keeps running so a remount can recover state.\n useEffect(() => {\n return () => {\n genRef.current += 1;\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n };\n }, []);\n\n const isTerminal = task !== null && TERMINAL_STATUSES.has(task.status);\n const isWorking = task !== null && !isTerminal;\n\n return { fire, task, result, error, isWorking, isTerminal, cancel };\n}\n"]} |
| import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { ReactNode } from 'react'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-C9utGXv_.cjs'; | ||
| import { C as ConnectOptions, A as App, q as Theme, s as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, o as TaskHandle, r as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, i as FileResult, b as ActionReducer, d as Store, m as StoreDispatch, a as Synapse, n as SynapseTheme } from '../types-BSZasM8q.cjs'; | ||
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ import { Task } from '@modelcontextprotocol/sdk/types.js'; |
| import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { ReactNode } from 'react'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-C9utGXv_.js'; | ||
| import { C as ConnectOptions, A as App, q as Theme, s as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, o as TaskHandle, r as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, i as FileResult, b as ActionReducer, d as Store, m as StoreDispatch, a as Synapse, n as SynapseTheme } from '../types-BSZasM8q.js'; | ||
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ import { Task } from '@modelcontextprotocol/sdk/types.js'; |
@@ -1,3 +0,3 @@ | ||
| import { connect, createSynapse } from '../chunk-XGJAADVV.js'; | ||
| import { applyThemeVariables } from '../chunk-VXTMI266.js'; | ||
| import { connect, createSynapse } from '../chunk-AGN3NDOK.js'; | ||
| import { applyTheme } from '../chunk-DOKVNVTI.js'; | ||
| import { createContext, useState, useRef, useEffect, useCallback, useSyncExternalStore, useContext } from 'react'; | ||
@@ -87,3 +87,3 @@ import { jsx, jsxs } from 'react/jsx-runtime'; | ||
| useEffect(() => { | ||
| applyThemeVariables(theme.mode, theme.tokens); | ||
| applyTheme(theme.mode, theme.tokens, theme.fontFaces); | ||
| }, [theme]); | ||
@@ -90,0 +90,0 @@ return null; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"sources":["../../src/react/app-provider.tsx","../../src/react/connect-hooks.ts","../../src/react/provider.tsx","../../src/react/hooks.ts"],"names":["useState","useEffect","createContext","useRef","jsx","useContext","useCallback","isTerminal"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { connect } from \"../connect.js\";\nimport type { App, ConnectOptions } from \"../types.js\";\n\nconst AppContext = createContext<App | null>(null);\n\nexport interface AppProviderProps extends ConnectOptions {\n children: ReactNode;\n}\n\nexport function AppProvider({ children, name, version, autoResize }: AppProviderProps) {\n const [app, setApp] = useState<App | null>(null);\n const connectingRef = useRef(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: connect once on mount\n useEffect(() => {\n if (connectingRef.current) return;\n connectingRef.current = true;\n\n connect({ name, version, autoResize }).then((a) => {\n setApp(a);\n });\n }, []);\n\n if (!app) return null; // Don't render children until connected\n\n return <AppContext.Provider value={app}>{children}</AppContext.Provider>;\n}\n\nexport function useAppContext(): App {\n const ctx = useContext(AppContext);\n if (!ctx) {\n throw new Error(\n \"useApp must be used within an <AppProvider>. Wrap your component tree with <AppProvider>.\",\n );\n }\n return ctx;\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { App, Theme, ToolResultData } from \"../types.js\";\nimport { AppProvider, useAppContext } from \"./app-provider.js\";\n\nexport { AppProvider };\n\nexport function useApp(): App {\n return useAppContext();\n}\n\nexport function useToolResult(): ToolResultData | null {\n const app = useAppContext();\n const [data, setData] = useState<ToolResultData | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-result\", (result: ToolResultData) => {\n setData(result);\n });\n }, [app]);\n\n return data;\n}\n\nexport function useToolInput(): Record<string, unknown> | null {\n const app = useAppContext();\n const [input, setInput] = useState<Record<string, unknown> | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-input\", (args: Record<string, unknown>) => {\n setInput(args);\n });\n }, [app]);\n\n return input;\n}\n\nexport function useConnectTheme(): Theme {\n const app = useAppContext();\n const [theme, setTheme] = useState<Theme>(() => app.theme);\n\n useEffect(() => {\n setTheme(app.theme); // Sync in case it changed between render and effect\n return app.on(\"theme-changed\", (t: Theme) => {\n setTheme(t);\n });\n }, [app]);\n\n return theme;\n}\n\nexport function useResize(): (width?: number, height?: number) => void {\n const app = useAppContext();\n return useCallback((width?: number, height?: number) => app.resize(width, height), [app]);\n}\n","import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { createSynapse } from \"../core.js\";\nimport { applyThemeVariables } from \"../theme-defaults.js\";\nimport type { Synapse, SynapseOptions, SynapseTheme } from \"../types.js\";\n\nconst SynapseContext = createContext<Synapse | null>(null);\n\nexport interface SynapseProviderProps extends SynapseOptions {\n children: ReactNode;\n}\n\nexport function SynapseProvider({ children, ...options }: SynapseProviderProps) {\n // Use a ref so the same Synapse instance survives StrictMode's\n // unmount/remount cycle. We intentionally do NOT destroy on unmount\n // because StrictMode re-mounts immediately and the transport must\n // stay alive. The instance is GC'd when the provider is truly removed.\n const ref = useRef<Synapse | null>(null);\n\n if (ref.current === null || ref.current.destroyed) {\n ref.current = createSynapse(options);\n }\n\n return (\n <SynapseContext.Provider value={ref.current}>\n <ThemeInjector synapse={ref.current} />\n {children}\n </SynapseContext.Provider>\n );\n}\n\n/** Injects theme CSS variables onto document.documentElement whenever the theme changes. */\nfunction ThemeInjector({ synapse }: { synapse: Synapse }) {\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n useEffect(() => {\n // Neutral defaults for the mode back any var the host omits, then host\n // values win — the same single path used by the handshake in core.ts.\n applyThemeVariables(theme.mode, theme.tokens);\n }, [theme]);\n\n return null;\n}\n\nexport function useSynapseContext(): Synapse {\n const ctx = useContext(SynapseContext);\n if (!ctx) {\n throw new Error(\n \"useSynapse must be used within a <SynapseProvider>. \" +\n \"Wrap your app component tree with <SynapseProvider>.\",\n );\n }\n return ctx;\n}\n","import type { McpUiHostContext } from \"@modelcontextprotocol/ext-apps\";\nimport type { Task, TaskStatus } from \"@modelcontextprotocol/sdk/types.js\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type {\n ActionReducer,\n AgentAction,\n CallToolAsTaskOptions,\n DataChangedEvent,\n FileResult,\n RequestFileOptions,\n Store,\n StoreDispatch,\n Synapse,\n SynapseTheme,\n TaskHandle,\n ToolCallResult,\n} from \"../types.js\";\nimport { SynapseProvider, useSynapseContext } from \"./provider.js\";\n\n// Re-export provider components\nexport { SynapseProvider };\n\nexport function useSynapse(): Synapse {\n return useSynapseContext();\n}\n\nexport function useCallTool<TOutput = unknown>(\n toolName: string,\n): {\n call: (args?: Record<string, unknown>) => Promise<ToolCallResult<TOutput>>;\n isPending: boolean;\n error: Error | null;\n data: TOutput | null;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TOutput | null>(null);\n const callIdRef = useRef(0);\n\n const call = useCallback(\n async (args?: Record<string, unknown>): Promise<ToolCallResult<TOutput>> => {\n const id = ++callIdRef.current;\n setIsPending(true);\n setError(null);\n\n try {\n const result = await synapse.callTool<Record<string, unknown>, TOutput>(toolName, args);\n // Stale guard: only update if this is still the latest call\n if (id === callIdRef.current) {\n setData(result.data);\n setIsPending(false);\n }\n return result;\n } catch (err) {\n if (id === callIdRef.current) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n setIsPending(false);\n }\n throw err;\n }\n },\n [synapse, toolName],\n );\n\n return { call, isPending, error, data };\n}\n\nexport function useDataSync(callback: (event: DataChangedEvent) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onDataChanged((event) => callbackRef.current(event));\n }, [synapse]);\n}\n\n/**\n * Subscribe to agent actions — typed, declarative commands from the server.\n *\n * Actions are emitted by tools as deterministic side effects (e.g., \"navigate\n * to the board I just created\"). The UI decides how to handle each action type.\n *\n * @example\n * ```tsx\n * useAgentAction((action) => {\n * if (action.type === \"navigate\") {\n * const { entity, id } = action.payload as NavigatePayload;\n * if (entity === \"board\") setSelectedBoardId(id);\n * }\n * });\n * ```\n */\nexport function useAgentAction(callback: (action: AgentAction) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onAction((action) => callbackRef.current(action));\n }, [synapse]);\n}\n\n/**\n * Subscribe to the full ext-apps host context.\n *\n * Returns the host context bag — spec-standardized fields (`theme`, `styles`,\n * `displayMode`, `toolInfo`) plus any host extensions. Re-renders on every\n * `ui/notifications/host-context-changed` notification.\n *\n * Prefer `useTheme()` when only theming matters (it filters no-op fires and\n * returns a typed `SynapseTheme`). Reach for `useHostContext()` for non-theme\n * fields like host-specific extensions, e.g. on NimbleBrain:\n *\n * ```tsx\n * const { workspace } = useHostContext<{ workspace?: { id: string } }>();\n * ```\n */\nexport function useHostContext<T extends McpUiHostContext = McpUiHostContext>(): T {\n const synapse = useSynapseContext();\n const [ctx, setCtx] = useState<T>(() => synapse.getHostContext() as T);\n\n useEffect(() => {\n // Sync in case context changed between render and effect\n setCtx(synapse.getHostContext() as T);\n return synapse.onHostContextChanged((c) => setCtx(c as T));\n }, [synapse]);\n\n return ctx;\n}\n\n/**\n * Re-renders only when the derived `SynapseTheme` actually changes (mode,\n * primaryColor, or any token value). Routes through `synapse.onThemeChanged`\n * — which is itself a `themesEqual`-filtered selector over the unified host\n * context — so a host-context update that doesn't move the theme (e.g. a\n * workspace switch) does NOT cause this hook's consumers to re-render.\n *\n * Building this on `useHostContext` instead would skip the filter: every\n * host-context-changed notification produces a new context reference, which\n * would propagate through `useState` and force a re-render even when the\n * derived theme is unchanged.\n */\nexport function useTheme(): SynapseTheme {\n const synapse = useSynapseContext();\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n // Sync in case theme changed between render and effect\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n return theme;\n}\n\nexport function useAction(): (action: string, params?: Record<string, unknown>) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (action: string, params?: Record<string, unknown>) => synapse.action(action, params),\n [synapse],\n );\n}\n\nexport function useChat(): (\n message: string,\n context?: { action?: string; entity?: string },\n) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (message: string, context?: { action?: string; entity?: string }) =>\n synapse.chat(message, context),\n [synapse],\n );\n}\n\n/**\n * Push the app's visible state to the agent via ext-apps `ui/update-model-context`.\n *\n * **Imperative** (no args) — returns a push function you call manually:\n * ```tsx\n * const push = useVisibleState();\n * push({ board: selectedBoard }, \"Viewing board X\");\n * ```\n *\n * **Declarative** (factory + deps) — auto-pushes when deps change:\n * ```tsx\n * useVisibleState(() => ({\n * state: { board: selectedBoard },\n * summary: `Viewing \"${selectedBoard?.name}\"`,\n * }), [selectedBoard]);\n * ```\n */\nexport function useVisibleState(): (state: Record<string, unknown>, summary?: string) => void;\nexport function useVisibleState(\n factory: () => { state: Record<string, unknown>; summary?: string },\n deps: unknown[],\n): void;\nexport function useVisibleState(\n factory?: () => { state: Record<string, unknown>; summary?: string },\n deps?: unknown[],\n): ((state: Record<string, unknown>, summary?: string) => void) | undefined {\n const synapse = useSynapseContext();\n const push = useCallback(\n (state: Record<string, unknown>, summary?: string) => synapse.setVisibleState(state, summary),\n [synapse],\n );\n\n // Declarative mode: auto-push when deps change.\n // The deps array is caller-provided (mirrors useMemo/useEffect pattern).\n const factoryRef = useRef(factory);\n factoryRef.current = factory;\n useEffect(() => {\n if (!factoryRef.current) return;\n const { state, summary } = factoryRef.current();\n push(state, summary);\n }, [...(deps ?? []), push]);\n\n if (!factory) return push;\n}\n\nexport function useFileUpload(): {\n pickFile: (options?: RequestFileOptions) => Promise<FileResult | null>;\n pickFiles: (options?: RequestFileOptions) => Promise<FileResult[]>;\n isPending: boolean;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n\n const pickFile = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFile(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n const pickFiles = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFiles(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n return { pickFile, pickFiles, isPending };\n}\n\nexport function useStore<TState, TActions extends Record<string, ActionReducer<TState, any>>>(\n store: Store<TState, TActions>,\n): {\n state: TState;\n dispatch: StoreDispatch<TActions>;\n} {\n const state = useSyncExternalStore(\n (onStoreChange) => store.subscribe(onStoreChange),\n () => store.getState(),\n () => store.getState(),\n );\n\n return { state, dispatch: store.dispatch };\n}\n\n// -----------------------------------------------------------------------------\n// useCallToolAsTask — lifecycle wrapper around `synapse.callToolAsTask`\n// -----------------------------------------------------------------------------\n\n/**\n * Spec terminal status values for the MCP 2025-11-25 tasks utility.\n *\n * Typed via `satisfies TaskStatus` so a rename of any member of the\n * spec enum (`completed`/`failed`/`cancelled`) trips `tsc` — we never\n * hand-type these as bare string literals in comparisons.\n */\nconst COMPLETED_STATUS = \"completed\" satisfies TaskStatus;\nconst FAILED_STATUS = \"failed\" satisfies TaskStatus;\nconst CANCELLED_STATUS = \"cancelled\" satisfies TaskStatus;\n\nconst TERMINAL_STATUSES: ReadonlySet<TaskStatus> = new Set<TaskStatus>([\n COMPLETED_STATUS,\n FAILED_STATUS,\n CANCELLED_STATUS,\n]);\n\n/**\n * Fallback poll cadence used when the receiver's `CreateTaskResult.task`\n * carries no `pollInterval`. Chosen to roughly match the 5s cadence\n * described in Task 005 (\"a sensible default like 5s if pollInterval is\n * absent\") — the effective fire delay is this value × 1.5 ≈ 7.5s, well\n * below default TTLs but long enough to avoid hammering hosts that do\n * emit `notifications/tasks/status`.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst POLL_FALLBACK_MULTIPLIER = 1.5;\n\n/**\n * Stop the poll fallback after this many consecutive `refresh()` failures.\n * Bridge teardown / TTL eviction / network outage all manifest as repeated\n * `tasks/get` rejections. Without a guard the timer re-arms forever; with\n * it we stop polling silently after `MAX_REFRESH_FAILURES` strikes — the\n * blocking `result()` path remains the authoritative source of truth, so\n * giving up on polling never loses the terminal value.\n */\nconst MAX_REFRESH_FAILURES = 5;\n\nexport interface UseCallToolAsTaskResult<TInput, TOutput> {\n /**\n * Start (or re-start) a task-augmented tool call. Returns the\n * resolved `TaskHandle` so callers can `await fire(...)` if they\n * want to know when the server has accepted the task, but reading\n * `task`/`result`/`error` from the hook is usually enough.\n *\n * Re-firing while a previous task is still in flight detaches this\n * hook from the prior handle (stops polling, unsubscribes) but does\n * NOT cancel the server-side task — the task keeps running and its\n * result may still be fetched elsewhere (e.g. on page revisit).\n */\n fire(args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>;\n /** Latest `Task` state, or `null` before `fire()` has been called. */\n task: Task | null;\n /** Populated once `handle.result()` resolves non-error. */\n result: ToolCallResult<TOutput> | null;\n /** Populated on rejection or when `result.isError === true`. */\n error: Error | null;\n /** `true` while the task is non-terminal (`working` / `input_required`). */\n isWorking: boolean;\n /** `true` when `task.status ∈ {completed, failed, cancelled}`. */\n isTerminal: boolean;\n /**\n * Cancel the active task via `tasks/cancel`. No-op when no task is\n * active. Swallowed errors surface via `error`.\n */\n cancel(): Promise<void>;\n}\n\n/**\n * React hook wrapper around `synapse.callToolAsTask`.\n *\n * Handles the full MCP 2025-11-25 task lifecycle:\n *\n * 1. `fire(args, options?)` sends the task-augmented `tools/call` and\n * stores the returned `TaskHandle` in a ref.\n * 2. Subscribes to `handle.onStatus` — updates `task` whenever the\n * host emits `notifications/tasks/status` (OPTIONAL per spec).\n * 3. Starts a polling fallback: if no status notification arrives\n * within `pollInterval × 1.5` (defaulting to ~7.5s), calls\n * `handle.refresh()` for canonical state. Stops on terminal.\n * 4. Awaits `handle.result()` in the background — resolves to either\n * `result` (success / `isError: false`) or `error` (network reject\n * OR `result.isError === true`).\n *\n * Cleanup (unmount or re-fire) unsubscribes from status events and\n * clears the poll timer, but does NOT cancel the server-side task —\n * the caller may remount and recover state by firing again, and tasks\n * outlive iframe teardown until TTL elapses.\n */\nexport function useCallToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(\n toolName: string,\n): UseCallToolAsTaskResult<TInput, TOutput> {\n const synapse = useSynapseContext();\n\n const [task, setTask] = useState<Task | null>(null);\n const [result, setResult] = useState<ToolCallResult<TOutput> | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Per-fire generation counter. Every `fire()` increments; any\n // asynchronous callback (status listener, poll timer, `result()`\n // resolution) captures the gen at schedule time and bails if the\n // current gen has moved past it. This is the single source of truth\n // for \"is this work still relevant?\" — more robust than comparing\n // TaskHandle identity because handles can be detached by re-fire.\n const genRef = useRef(0);\n\n // Active handle + its teardown handles. We keep both in refs so the\n // hook's stable `fire`/`cancel` callbacks can reach the current\n // lifecycle state without re-binding on every render.\n const handleRef = useRef<TaskHandle<TOutput> | null>(null);\n const unsubscribeRef = useRef<(() => void) | null>(null);\n const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // `pollInterval × 1.5`, captured per-fire. Falls back to the 5s\n // default when the host didn't provide a `pollInterval` in the\n // initial CreateTaskResult.task.\n const pollDelayRef = useRef<number>(DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER);\n\n // Track the latest known status out-of-band so the poll callback\n // can decide whether to keep polling without depending on the\n // `task` React state (which lags a render behind setState).\n const terminalRef = useRef<boolean>(false);\n\n // Consecutive `refresh()` failure count for the active fire. Reset\n // on every successful refresh, status notification, or new fire.\n const refreshFailureCountRef = useRef<number>(0);\n\n const clearPollTimer = useCallback(() => {\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n }, []);\n\n const detachCurrent = useCallback(() => {\n clearPollTimer();\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n }, [clearPollTimer]);\n\n const scheduleNextPoll = useCallback(\n (gen: number) => {\n clearPollTimer();\n if (terminalRef.current) return;\n pollTimerRef.current = setTimeout(() => {\n // Bail if this fire has been superseded or torn down.\n if (gen !== genRef.current) return;\n const h = handleRef.current;\n if (!h) return;\n if (terminalRef.current) return;\n // `refresh()` is the canonical source for `createdAt` /\n // `lastUpdatedAt` / `ttl` — notification-derived Tasks carry\n // placeholders per the router in `task-handle.ts`.\n h.refresh().then(\n (fresh) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(fresh);\n const isTerminal = TERMINAL_STATUSES.has(fresh.status);\n terminalRef.current = isTerminal;\n if (!isTerminal) scheduleNextPoll(gen);\n },\n () => {\n // Swallow refresh errors — the blocking `tasks/result` is\n // the authoritative path; polling is best-effort. But guard\n // against runaway re-arming: if the bridge is gone or the\n // task TTL has elapsed, every refresh rejects. Stop after\n // MAX_REFRESH_FAILURES consecutive strikes; result() will\n // still surface a terminal value or error when it settles.\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current += 1;\n if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) return;\n if (!terminalRef.current) scheduleNextPoll(gen);\n },\n );\n }, pollDelayRef.current);\n },\n [clearPollTimer],\n );\n\n const fire = useCallback(\n async (args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>> => {\n // Detach any in-flight prior task BEFORE incrementing the gen so\n // its callbacks see the new gen and bail. (Incrementing then\n // detaching would also work, but detach-first makes the order\n // obvious: stop listening, bump generation, start fresh.)\n detachCurrent();\n const gen = ++genRef.current;\n\n // Reset per-fire state. Don't wipe `task` yet — `callToolAsTask`\n // is async; showing the previous terminal state briefly is less\n // jarring than flicker to null → working. We clear on resolution.\n setResult(null);\n setError(null);\n terminalRef.current = false;\n refreshFailureCountRef.current = 0;\n\n let handle: TaskHandle<TOutput>;\n try {\n handle = await synapse.callToolAsTask<TInput, TOutput>(toolName, args, options);\n } catch (err) {\n if (gen !== genRef.current) throw err;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n throw err;\n }\n\n // Caller superseded the fire between request and response —\n // don't attach listeners, but still return the handle so the\n // awaiter can observe it.\n if (gen !== genRef.current) return handle;\n\n handleRef.current = handle;\n\n // Derive the fallback poll delay from the receiver's advertised\n // `pollInterval`. Spec allows it to be absent; we then use the\n // 5s default described in Task 005.\n const hintedInterval = handle.task.pollInterval;\n pollDelayRef.current =\n typeof hintedInterval === \"number\" && hintedInterval > 0\n ? hintedInterval * POLL_FALLBACK_MULTIPLIER\n : DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER;\n\n setTask(handle.task);\n terminalRef.current = TERMINAL_STATUSES.has(handle.task.status);\n\n // Subscribe to `notifications/tasks/status`. Each notification\n // resets the poll countdown (that's the whole point of the\n // \"notification OR polling\" contract — if notifications flow,\n // we don't poll; if they don't, the timer fires).\n unsubscribeRef.current = handle.onStatus((updated) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(updated);\n const isTerminal = TERMINAL_STATUSES.has(updated.status);\n terminalRef.current = isTerminal;\n if (isTerminal) {\n clearPollTimer();\n } else {\n scheduleNextPoll(gen);\n }\n });\n\n // Kick off the blocking result fetch — this is the authoritative\n // terminal value regardless of whether notifications or polls\n // landed in between. By spec, `tasks/result` blocks until the task\n // reaches a terminal status, so when this settles we KNOW the task\n // is terminal — stop polling and synthesize a terminal `task`\n // status so derived flags (`isTerminal`, `isWorking`) match the\n // populated `result` / `error` immediately.\n handle.result().then(\n (res) => {\n if (gen !== genRef.current) return;\n terminalRef.current = true;\n clearPollTimer();\n // Synthesize the terminal Task: failed if `isError`, otherwise\n // completed. The next status notification or refresh would\n // confirm this, but we want internal state consistent the\n // instant `result` is populated — a \"result populated while\n // isWorking=true\" render is incoherent for consumers.\n setTask((prev) => {\n const status: TaskStatus = res.isError ? \"failed\" : \"completed\";\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status, lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status,\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n if (res.isError) {\n // Spec: `CallToolResult.isError === true` is a tool-level\n // error, not a protocol error. Surface via `error` for\n // consumers who treat it as a failure, but also populate\n // `result` so callers inspecting the raw content block\n // still have access.\n setResult(res);\n const msg =\n typeof res.data === \"string\" && res.data.length > 0\n ? res.data\n : `Tool \"${toolName}\" returned isError: true`;\n setError(new Error(msg));\n } else {\n setResult(res);\n }\n },\n (err) => {\n if (gen !== genRef.current) return;\n // result() rejection means the `tasks/result` RPC failed\n // (transport error, taskId not found, bridge teardown). We\n // can't know the server-side task's actual final state, but\n // we know polling won't recover here either — same transport.\n // Mark terminal and synthesize `failed` status for UX\n // coherence; the populated `error` tells the consumer what\n // specifically went wrong.\n terminalRef.current = true;\n clearPollTimer();\n setTask((prev) => {\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status: \"failed\", lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status: \"failed\",\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n },\n );\n\n // Start the poll fallback only if we aren't already terminal.\n if (!terminalRef.current) scheduleNextPoll(gen);\n\n return handle;\n },\n [synapse, toolName, detachCurrent, clearPollTimer, scheduleNextPoll],\n );\n\n const cancel = useCallback(async (): Promise<void> => {\n const h = handleRef.current;\n if (!h) return;\n const gen = genRef.current;\n try {\n const cancelled = await h.cancel();\n if (gen !== genRef.current) return;\n setTask(cancelled);\n terminalRef.current = TERMINAL_STATUSES.has(cancelled.status);\n clearPollTimer();\n } catch (err) {\n if (gen !== genRef.current) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n }\n }, [clearPollTimer]);\n\n // Cleanup on unmount: stop polling, drop the status subscription.\n // Deliberately do NOT call `handle.cancel()` — per Task 005, the\n // server-side task keeps running so a remount can recover state.\n useEffect(() => {\n return () => {\n genRef.current += 1;\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n };\n }, []);\n\n const isTerminal = task !== null && TERMINAL_STATUSES.has(task.status);\n const isWorking = task !== null && !isTerminal;\n\n return { fire, task, result, error, isWorking, isTerminal, cancel };\n}\n"]} | ||
| {"version":3,"sources":["../../src/react/app-provider.tsx","../../src/react/connect-hooks.ts","../../src/react/provider.tsx","../../src/react/hooks.ts"],"names":["useState","useEffect","createContext","useRef","jsx","useContext","useCallback","isTerminal"],"mappings":";;;;;AAIA,IAAM,UAAA,GAAa,cAA0B,IAAI,CAAA;AAM1C,SAAS,YAAY,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,YAAW,EAAqB;AACrF,EAAA,MAAM,CAAC,GAAA,EAAK,MAAM,CAAA,GAAI,SAAqB,IAAI,CAAA;AAC/C,EAAA,MAAM,aAAA,GAAgB,OAAO,KAAK,CAAA;AAGlC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,cAAc,OAAA,EAAS;AAC3B,IAAA,aAAA,CAAc,OAAA,GAAU,IAAA;AAExB,IAAA,OAAA,CAAQ,EAAE,MAAM,OAAA,EAAS,UAAA,EAAY,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM;AACjD,MAAA,MAAA,CAAO,CAAC,CAAA;AAAA,IACV,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AAEjB,EAAA,2BAAQ,UAAA,CAAW,QAAA,EAAX,EAAoB,KAAA,EAAO,KAAM,QAAA,EAAS,CAAA;AACpD;AAEO,SAAS,aAAA,GAAqB;AACnC,EAAA,MAAM,GAAA,GAAM,WAAW,UAAU,CAAA;AACjC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;;;AC/BO,SAAS,MAAA,GAAc;AAC5B,EAAA,OAAO,aAAA,EAAc;AACvB;AAEO,SAAS,aAAA,GAAuC;AACrD,EAAA,MAAM,MAAM,aAAA,EAAc;AAC1B,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIA,SAAgC,IAAI,CAAA;AAE5D,EAAAC,UAAU,MAAM;AACd,IAAA,OAAO,GAAA,CAAI,EAAA,CAAG,aAAA,EAAe,CAAC,MAAA,KAA2B;AACvD,MAAA,OAAA,CAAQ,MAAM,CAAA;AAAA,IAChB,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,YAAA,GAA+C;AAC7D,EAAA,MAAM,MAAM,aAAA,EAAc;AAC1B,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAID,SAAyC,IAAI,CAAA;AAEvE,EAAAC,UAAU,MAAM;AACd,IAAA,OAAO,GAAA,CAAI,EAAA,CAAG,YAAA,EAAc,CAAC,IAAA,KAAkC;AAC7D,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,eAAA,GAAyB;AACvC,EAAA,MAAM,MAAM,aAAA,EAAc;AAC1B,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,IAAID,QAAAA,CAAgB,MAAM,IAAI,KAAK,CAAA;AAEzD,EAAAC,UAAU,MAAM;AACd,IAAA,QAAA,CAAS,IAAI,KAAK,CAAA;AAClB,IAAA,OAAO,GAAA,CAAI,EAAA,CAAG,eAAA,EAAiB,CAAC,CAAA,KAAa;AAC3C,MAAA,QAAA,CAAS,CAAC,CAAA;AAAA,IACZ,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAER,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,SAAA,GAAuD;AACrE,EAAA,MAAM,MAAM,aAAA,EAAc;AAC1B,EAAA,OAAO,WAAA,CAAY,CAAC,KAAA,EAAgB,MAAA,KAAoB,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO,MAAM,CAAA,EAAG,CAAC,GAAG,CAAC,CAAA;AAC1F;AChDA,IAAM,cAAA,GAAiBC,cAA8B,IAAI,CAAA;AAMlD,SAAS,eAAA,CAAgB,EAAE,QAAA,EAAU,GAAG,SAAQ,EAAyB;AAK9E,EAAA,MAAM,GAAA,GAAMC,OAAuB,IAAI,CAAA;AAEvC,EAAA,IAAI,GAAA,CAAI,OAAA,KAAY,IAAA,IAAQ,GAAA,CAAI,QAAQ,SAAA,EAAW;AACjD,IAAA,GAAA,CAAI,OAAA,GAAU,cAAc,OAAO,CAAA;AAAA,EACrC;AAEA,EAAA,4BACG,cAAA,CAAe,QAAA,EAAf,EAAwB,KAAA,EAAO,IAAI,OAAA,EAClC,QAAA,EAAA;AAAA,oBAAAC,GAAAA,CAAC,aAAA,EAAA,EAAc,OAAA,EAAS,GAAA,CAAI,OAAA,EAAS,CAAA;AAAA,IACpC;AAAA,GAAA,EACH,CAAA;AAEJ;AAGA,SAAS,aAAA,CAAc,EAAE,OAAA,EAAQ,EAAyB;AACxD,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAIJ,SAAuB,MAAM,OAAA,CAAQ,UAAU,CAAA;AAEzE,EAAAC,UAAU,MAAM;AACd,IAAA,QAAA,CAAS,OAAA,CAAQ,UAAU,CAAA;AAC3B,IAAA,OAAO,OAAA,CAAQ,eAAe,QAAQ,CAAA;AAAA,EACxC,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAAA,UAAU,MAAM;AAGd,IAAA,UAAA,CAAW,KAAA,CAAM,IAAA,EAAM,KAAA,CAAM,MAAA,EAAQ,MAAM,SAAS,CAAA;AAAA,EACtD,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,OAAO,IAAA;AACT;AAEO,SAAS,iBAAA,GAA6B;AAC3C,EAAA,MAAM,GAAA,GAAMI,WAAW,cAAc,CAAA;AACrC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;;;ACnCO,SAAS,UAAA,GAAsB;AACpC,EAAA,OAAO,iBAAA,EAAkB;AAC3B;AAEO,SAAS,YACd,QAAA,EAMA;AACA,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIL,SAAS,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,SAAuB,IAAI,CAAA;AACrD,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIA,SAAyB,IAAI,CAAA;AACrD,EAAA,MAAM,SAAA,GAAYG,OAAO,CAAC,CAAA;AAE1B,EAAA,MAAM,IAAA,GAAOG,WAAAA;AAAA,IACX,OAAO,IAAA,KAAqE;AAC1E,MAAA,MAAM,EAAA,GAAK,EAAE,SAAA,CAAU,OAAA;AACvB,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,QAAA,CAAS,IAAI,CAAA;AAEb,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,QAAA,CAA2C,UAAU,IAAI,CAAA;AAEtF,QAAA,IAAI,EAAA,KAAO,UAAU,OAAA,EAAS;AAC5B,UAAA,OAAA,CAAQ,OAAO,IAAI,CAAA;AACnB,UAAA,YAAA,CAAa,KAAK,CAAA;AAAA,QACpB;AACA,QAAA,OAAO,MAAA;AAAA,MACT,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,EAAA,KAAO,UAAU,OAAA,EAAS;AAC5B,UAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,UAAA,QAAA,CAAS,CAAC,CAAA;AACV,UAAA,YAAA,CAAa,KAAK,CAAA;AAAA,QACpB;AACA,QAAA,MAAM,GAAA;AAAA,MACR;AAAA,IACF,CAAA;AAAA,IACA,CAAC,SAAS,QAAQ;AAAA,GACpB;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,SAAA,EAAW,KAAA,EAAO,IAAA,EAAK;AACxC;AAEO,SAAS,YAAY,QAAA,EAAmD;AAC7E,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,WAAA,GAAcH,OAAO,QAAQ,CAAA;AACnC,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AAEtB,EAAAF,UAAU,MAAM;AACd,IAAA,OAAO,QAAQ,aAAA,CAAc,CAAC,UAAU,WAAA,CAAY,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EACpE,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AACd;AAkBO,SAAS,eAAe,QAAA,EAA+C;AAC5E,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,WAAA,GAAcE,OAAO,QAAQ,CAAA;AACnC,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AAEtB,EAAAF,UAAU,MAAM;AACd,IAAA,OAAO,QAAQ,QAAA,CAAS,CAAC,WAAW,WAAA,CAAY,OAAA,CAAQ,MAAM,CAAC,CAAA;AAAA,EACjE,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AACd;AAiBO,SAAS,cAAA,GAAmE;AACjF,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,CAAC,KAAK,MAAM,CAAA,GAAID,SAAY,MAAM,OAAA,CAAQ,gBAAqB,CAAA;AAErE,EAAAC,UAAU,MAAM;AAEd,IAAA,MAAA,CAAO,OAAA,CAAQ,gBAAqB,CAAA;AACpC,IAAA,OAAO,QAAQ,oBAAA,CAAqB,CAAC,CAAA,KAAM,MAAA,CAAO,CAAM,CAAC,CAAA;AAAA,EAC3D,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,GAAA;AACT;AAcO,SAAS,QAAA,GAAyB;AACvC,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAID,SAAuB,MAAM,OAAA,CAAQ,UAAU,CAAA;AAEzE,EAAAC,UAAU,MAAM;AAEd,IAAA,QAAA,CAAS,OAAA,CAAQ,UAAU,CAAA;AAC3B,IAAA,OAAO,OAAA,CAAQ,eAAe,QAAQ,CAAA;AAAA,EACxC,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,SAAA,GAAwE;AACtF,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,OAAOK,WAAAA;AAAA,IACL,CAAC,MAAA,EAAgB,MAAA,KAAqC,OAAA,CAAQ,MAAA,CAAO,QAAQ,MAAM,CAAA;AAAA,IACnF,CAAC,OAAO;AAAA,GACV;AACF;AAEO,SAAS,OAAA,GAGN;AACR,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,OAAOA,WAAAA;AAAA,IACL,CAAC,OAAA,EAAiB,OAAA,KAChB,OAAA,CAAQ,IAAA,CAAK,SAAS,OAAO,CAAA;AAAA,IAC/B,CAAC,OAAO;AAAA,GACV;AACF;AAwBO,SAAS,eAAA,CACd,SACA,IAAA,EAC0E;AAC1E,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,IAAA,GAAOA,WAAAA;AAAA,IACX,CAAC,KAAA,EAAgC,OAAA,KAAqB,OAAA,CAAQ,eAAA,CAAgB,OAAO,OAAO,CAAA;AAAA,IAC5F,CAAC,OAAO;AAAA,GACV;AAIA,EAAA,MAAM,UAAA,GAAaH,OAAO,OAAO,CAAA;AACjC,EAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AACrB,EAAAF,UAAU,MAAM;AACd,IAAA,IAAI,CAAC,WAAW,OAAA,EAAS;AACzB,IAAA,MAAM,EAAE,KAAA,EAAO,OAAA,EAAQ,GAAI,WAAW,OAAA,EAAQ;AAC9C,IAAA,IAAA,CAAK,OAAO,OAAO,CAAA;AAAA,EACrB,GAAG,CAAC,GAAI,QAAQ,EAAC,EAAI,IAAI,CAAC,CAAA;AAE1B,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACvB;AAEO,SAAS,aAAA,GAId;AACA,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAID,SAAS,KAAK,CAAA;AAEhD,EAAA,MAAM,QAAA,GAAWM,WAAAA;AAAA,IACf,OAAO,OAAA,KAAiC;AACtC,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,IAAI;AACF,QAAA,OAAO,MAAM,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA;AAAA,MACvC,CAAA,SAAE;AACA,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACpB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,MAAM,SAAA,GAAYA,WAAAA;AAAA,IAChB,OAAO,OAAA,KAAiC;AACtC,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,IAAI;AACF,QAAA,OAAO,MAAM,OAAA,CAAQ,SAAA,CAAU,OAAO,CAAA;AAAA,MACxC,CAAA,SAAE;AACA,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACpB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,OAAO;AAAA,GACV;AAEA,EAAA,OAAO,EAAE,QAAA,EAAU,SAAA,EAAW,SAAA,EAAU;AAC1C;AAEO,SAAS,SACd,KAAA,EAIA;AACA,EAAA,MAAM,KAAA,GAAQ,oBAAA;AAAA,IACZ,CAAC,aAAA,KAAkB,KAAA,CAAM,SAAA,CAAU,aAAa,CAAA;AAAA,IAChD,MAAM,MAAM,QAAA,EAAS;AAAA,IACrB,MAAM,MAAM,QAAA;AAAS,GACvB;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,KAAA,CAAM,QAAA,EAAS;AAC3C;AAaA,IAAM,gBAAA,GAAmB,WAAA;AACzB,IAAM,aAAA,GAAgB,QAAA;AACtB,IAAM,gBAAA,GAAmB,WAAA;AAEzB,IAAM,iBAAA,uBAAiD,GAAA,CAAgB;AAAA,EACrE,gBAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF,CAAC,CAAA;AAUD,IAAM,wBAAA,GAA2B,GAAA;AACjC,IAAM,wBAAA,GAA2B,GAAA;AAUjC,IAAM,oBAAA,GAAuB,CAAA;AAqDtB,SAAS,kBACd,QAAA,EAC0C;AAC1C,EAAA,MAAM,UAAU,iBAAA,EAAkB;AAElC,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIN,SAAsB,IAAI,CAAA;AAClD,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAIA,SAAyC,IAAI,CAAA;AACzE,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,SAAuB,IAAI,CAAA;AAQrD,EAAA,MAAM,MAAA,GAASG,OAAO,CAAC,CAAA;AAKvB,EAAA,MAAM,SAAA,GAAYA,OAAmC,IAAI,CAAA;AACzD,EAAA,MAAM,cAAA,GAAiBA,OAA4B,IAAI,CAAA;AACvD,EAAA,MAAM,YAAA,GAAeA,OAA6C,IAAI,CAAA;AAItE,EAAA,MAAM,YAAA,GAAeA,MAAAA,CAAe,wBAAA,GAA2B,wBAAwB,CAAA;AAKvF,EAAA,MAAM,WAAA,GAAcA,OAAgB,KAAK,CAAA;AAIzC,EAAA,MAAM,sBAAA,GAAyBA,OAAe,CAAC,CAAA;AAE/C,EAAA,MAAM,cAAA,GAAiBG,YAAY,MAAM;AACvC,IAAA,IAAI,YAAA,CAAa,YAAY,IAAA,EAAM;AACjC,MAAA,YAAA,CAAa,aAAa,OAAO,CAAA;AACjC,MAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAAA,IACzB;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,aAAA,GAAgBA,YAAY,MAAM;AACtC,IAAA,cAAA,EAAe;AACf,IAAA,IAAI,eAAe,OAAA,EAAS;AAC1B,MAAA,cAAA,CAAe,OAAA,EAAQ;AACvB,MAAA,cAAA,CAAe,OAAA,GAAU,IAAA;AAAA,IAC3B;AACA,IAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AAAA,EACtB,CAAA,EAAG,CAAC,cAAc,CAAC,CAAA;AAEnB,EAAA,MAAM,gBAAA,GAAmBA,WAAAA;AAAA,IACvB,CAAC,GAAA,KAAgB;AACf,MAAA,cAAA,EAAe;AACf,MAAA,IAAI,YAAY,OAAA,EAAS;AACzB,MAAA,YAAA,CAAa,OAAA,GAAU,WAAW,MAAM;AAEtC,QAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,QAAA,MAAM,IAAI,SAAA,CAAU,OAAA;AACpB,QAAA,IAAI,CAAC,CAAA,EAAG;AACR,QAAA,IAAI,YAAY,OAAA,EAAS;AAIzB,QAAA,CAAA,CAAE,SAAQ,CAAE,IAAA;AAAA,UACV,CAAC,KAAA,KAAU;AACT,YAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,YAAA,sBAAA,CAAuB,OAAA,GAAU,CAAA;AACjC,YAAA,OAAA,CAAQ,KAAK,CAAA;AACb,YAAA,MAAMC,WAAAA,GAAa,iBAAA,CAAkB,GAAA,CAAI,KAAA,CAAM,MAAM,CAAA;AACrD,YAAA,WAAA,CAAY,OAAA,GAAUA,WAAAA;AACtB,YAAA,IAAI,CAACA,WAAAA,EAAY,gBAAA,CAAiB,GAAG,CAAA;AAAA,UACvC,CAAA;AAAA,UACA,MAAM;AAOJ,YAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,YAAA,sBAAA,CAAuB,OAAA,IAAW,CAAA;AAClC,YAAA,IAAI,sBAAA,CAAuB,WAAW,oBAAA,EAAsB;AAC5D,YAAA,IAAI,CAAC,WAAA,CAAY,OAAA,EAAS,gBAAA,CAAiB,GAAG,CAAA;AAAA,UAChD;AAAA,SACF;AAAA,MACF,CAAA,EAAG,aAAa,OAAO,CAAA;AAAA,IACzB,CAAA;AAAA,IACA,CAAC,cAAc;AAAA,GACjB;AAEA,EAAA,MAAM,IAAA,GAAOD,WAAAA;AAAA,IACX,OAAO,MAAe,OAAA,KAAkE;AAKtF,MAAA,aAAA,EAAc;AACd,MAAA,MAAM,GAAA,GAAM,EAAE,MAAA,CAAO,OAAA;AAKrB,MAAA,SAAA,CAAU,IAAI,CAAA;AACd,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,WAAA,CAAY,OAAA,GAAU,KAAA;AACtB,MAAA,sBAAA,CAAuB,OAAA,GAAU,CAAA;AAEjC,MAAA,IAAI,MAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAA,GAAS,MAAM,OAAA,CAAQ,cAAA,CAAgC,QAAA,EAAU,MAAM,OAAO,CAAA;AAAA,MAChF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,GAAA,KAAQ,MAAA,CAAO,OAAA,EAAS,MAAM,GAAA;AAClC,QAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,QAAA,QAAA,CAAS,CAAC,CAAA;AACV,QAAA,MAAM,GAAA;AAAA,MACR;AAKA,MAAA,IAAI,GAAA,KAAQ,MAAA,CAAO,OAAA,EAAS,OAAO,MAAA;AAEnC,MAAA,SAAA,CAAU,OAAA,GAAU,MAAA;AAKpB,MAAA,MAAM,cAAA,GAAiB,OAAO,IAAA,CAAK,YAAA;AACnC,MAAA,YAAA,CAAa,OAAA,GACX,OAAO,cAAA,KAAmB,QAAA,IAAY,iBAAiB,CAAA,GACnD,cAAA,GAAiB,2BACjB,wBAAA,GAA2B,wBAAA;AAEjC,MAAA,OAAA,CAAQ,OAAO,IAAI,CAAA;AACnB,MAAA,WAAA,CAAY,OAAA,GAAU,iBAAA,CAAkB,GAAA,CAAI,MAAA,CAAO,KAAK,MAAM,CAAA;AAM9D,MAAA,cAAA,CAAe,OAAA,GAAU,MAAA,CAAO,QAAA,CAAS,CAAC,OAAA,KAAY;AACpD,QAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,QAAA,sBAAA,CAAuB,OAAA,GAAU,CAAA;AACjC,QAAA,OAAA,CAAQ,OAAO,CAAA;AACf,QAAA,MAAMC,WAAAA,GAAa,iBAAA,CAAkB,GAAA,CAAI,OAAA,CAAQ,MAAM,CAAA;AACvD,QAAA,WAAA,CAAY,OAAA,GAAUA,WAAAA;AACtB,QAAA,IAAIA,WAAAA,EAAY;AACd,UAAA,cAAA,EAAe;AAAA,QACjB,CAAA,MAAO;AACL,UAAA,gBAAA,CAAiB,GAAG,CAAA;AAAA,QACtB;AAAA,MACF,CAAC,CAAA;AASD,MAAA,MAAA,CAAO,QAAO,CAAE,IAAA;AAAA,QACd,CAAC,GAAA,KAAQ;AACP,UAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,UAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,UAAA,cAAA,EAAe;AAMf,UAAA,OAAA,CAAQ,CAAC,IAAA,KAAS;AAChB,YAAA,MAAM,MAAA,GAAqB,GAAA,CAAI,OAAA,GAAU,QAAA,GAAW,WAAA;AACpD,YAAA,MAAM,GAAA,GAAA,iBAAM,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACnC,YAAA,OAAO,OACH,EAAE,GAAG,MAAM,MAAA,EAAQ,aAAA,EAAe,KAAI,GACtC;AAAA,cACE,MAAA,EAAQ,OAAO,IAAA,CAAK,MAAA;AAAA,cACpB,MAAA;AAAA,cACA,GAAA,EAAK,OAAO,IAAA,CAAK,GAAA;AAAA,cACjB,SAAA,EAAW,OAAO,IAAA,CAAK,SAAA;AAAA,cACvB,aAAA,EAAe;AAAA,aACjB;AAAA,UACN,CAAC,CAAA;AACD,UAAA,IAAI,IAAI,OAAA,EAAS;AAMf,YAAA,SAAA,CAAU,GAAG,CAAA;AACb,YAAA,MAAM,GAAA,GACJ,OAAO,GAAA,CAAI,IAAA,KAAS,QAAA,IAAY,GAAA,CAAI,IAAA,CAAK,MAAA,GAAS,CAAA,GAC9C,GAAA,CAAI,IAAA,GACJ,CAAA,MAAA,EAAS,QAAQ,CAAA,wBAAA,CAAA;AACvB,YAAA,QAAA,CAAS,IAAI,KAAA,CAAM,GAAG,CAAC,CAAA;AAAA,UACzB,CAAA,MAAO;AACL,YAAA,SAAA,CAAU,GAAG,CAAA;AAAA,UACf;AAAA,QACF,CAAA;AAAA,QACA,CAAC,GAAA,KAAQ;AACP,UAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAQ5B,UAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AACtB,UAAA,cAAA,EAAe;AACf,UAAA,OAAA,CAAQ,CAAC,IAAA,KAAS;AAChB,YAAA,MAAM,GAAA,GAAA,iBAAM,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AACnC,YAAA,OAAO,IAAA,GACH,EAAE,GAAG,IAAA,EAAM,QAAQ,QAAA,EAAU,aAAA,EAAe,KAAI,GAChD;AAAA,cACE,MAAA,EAAQ,OAAO,IAAA,CAAK,MAAA;AAAA,cACpB,MAAA,EAAQ,QAAA;AAAA,cACR,GAAA,EAAK,OAAO,IAAA,CAAK,GAAA;AAAA,cACjB,SAAA,EAAW,OAAO,IAAA,CAAK,SAAA;AAAA,cACvB,aAAA,EAAe;AAAA,aACjB;AAAA,UACN,CAAC,CAAA;AACD,UAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,UAAA,QAAA,CAAS,CAAC,CAAA;AAAA,QACZ;AAAA,OACF;AAGA,MAAA,IAAI,CAAC,WAAA,CAAY,OAAA,EAAS,gBAAA,CAAiB,GAAG,CAAA;AAE9C,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,CAAC,OAAA,EAAS,QAAA,EAAU,aAAA,EAAe,gBAAgB,gBAAgB;AAAA,GACrE;AAEA,EAAA,MAAM,MAAA,GAASD,YAAY,YAA2B;AACpD,IAAA,MAAM,IAAI,SAAA,CAAU,OAAA;AACpB,IAAA,IAAI,CAAC,CAAA,EAAG;AACR,IAAA,MAAM,MAAM,MAAA,CAAO,OAAA;AACnB,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,MAAM,CAAA,CAAE,MAAA,EAAO;AACjC,MAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,MAAA,OAAA,CAAQ,SAAS,CAAA;AACjB,MAAA,WAAA,CAAY,OAAA,GAAU,iBAAA,CAAkB,GAAA,CAAI,SAAA,CAAU,MAAM,CAAA;AAC5D,MAAA,cAAA,EAAe;AAAA,IACjB,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,GAAA,KAAQ,OAAO,OAAA,EAAS;AAC5B,MAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,MAAA,QAAA,CAAS,CAAC,CAAA;AAAA,IACZ;AAAA,EACF,CAAA,EAAG,CAAC,cAAc,CAAC,CAAA;AAKnB,EAAAL,UAAU,MAAM;AACd,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,CAAO,OAAA,IAAW,CAAA;AAClB,MAAA,IAAI,YAAA,CAAa,YAAY,IAAA,EAAM;AACjC,QAAA,YAAA,CAAa,aAAa,OAAO,CAAA;AACjC,QAAA,YAAA,CAAa,OAAA,GAAU,IAAA;AAAA,MACzB;AACA,MAAA,IAAI,eAAe,OAAA,EAAS;AAC1B,QAAA,cAAA,CAAe,OAAA,EAAQ;AACvB,QAAA,cAAA,CAAe,OAAA,GAAU,IAAA;AAAA,MAC3B;AACA,MAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AAAA,IACtB,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,aAAa,IAAA,KAAS,IAAA,IAAQ,iBAAA,CAAkB,GAAA,CAAI,KAAK,MAAM,CAAA;AACrE,EAAA,MAAM,SAAA,GAAY,IAAA,KAAS,IAAA,IAAQ,CAAC,UAAA;AAEpC,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,QAAQ,KAAA,EAAO,SAAA,EAAW,YAAY,MAAA,EAAO;AACpE","file":"index.js","sourcesContent":["import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { connect } from \"../connect.js\";\nimport type { App, ConnectOptions } from \"../types.js\";\n\nconst AppContext = createContext<App | null>(null);\n\nexport interface AppProviderProps extends ConnectOptions {\n children: ReactNode;\n}\n\nexport function AppProvider({ children, name, version, autoResize }: AppProviderProps) {\n const [app, setApp] = useState<App | null>(null);\n const connectingRef = useRef(false);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: connect once on mount\n useEffect(() => {\n if (connectingRef.current) return;\n connectingRef.current = true;\n\n connect({ name, version, autoResize }).then((a) => {\n setApp(a);\n });\n }, []);\n\n if (!app) return null; // Don't render children until connected\n\n return <AppContext.Provider value={app}>{children}</AppContext.Provider>;\n}\n\nexport function useAppContext(): App {\n const ctx = useContext(AppContext);\n if (!ctx) {\n throw new Error(\n \"useApp must be used within an <AppProvider>. Wrap your component tree with <AppProvider>.\",\n );\n }\n return ctx;\n}\n","import { useCallback, useEffect, useState } from \"react\";\nimport type { App, Theme, ToolResultData } from \"../types.js\";\nimport { AppProvider, useAppContext } from \"./app-provider.js\";\n\nexport { AppProvider };\n\nexport function useApp(): App {\n return useAppContext();\n}\n\nexport function useToolResult(): ToolResultData | null {\n const app = useAppContext();\n const [data, setData] = useState<ToolResultData | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-result\", (result: ToolResultData) => {\n setData(result);\n });\n }, [app]);\n\n return data;\n}\n\nexport function useToolInput(): Record<string, unknown> | null {\n const app = useAppContext();\n const [input, setInput] = useState<Record<string, unknown> | null>(null);\n\n useEffect(() => {\n return app.on(\"tool-input\", (args: Record<string, unknown>) => {\n setInput(args);\n });\n }, [app]);\n\n return input;\n}\n\nexport function useConnectTheme(): Theme {\n const app = useAppContext();\n const [theme, setTheme] = useState<Theme>(() => app.theme);\n\n useEffect(() => {\n setTheme(app.theme); // Sync in case it changed between render and effect\n return app.on(\"theme-changed\", (t: Theme) => {\n setTheme(t);\n });\n }, [app]);\n\n return theme;\n}\n\nexport function useResize(): (width?: number, height?: number) => void {\n const app = useAppContext();\n return useCallback((width?: number, height?: number) => app.resize(width, height), [app]);\n}\n","import { createContext, type ReactNode, useContext, useEffect, useRef, useState } from \"react\";\nimport { createSynapse } from \"../core.js\";\nimport { applyTheme } from \"../theme-defaults.js\";\nimport type { Synapse, SynapseOptions, SynapseTheme } from \"../types.js\";\n\nconst SynapseContext = createContext<Synapse | null>(null);\n\nexport interface SynapseProviderProps extends SynapseOptions {\n children: ReactNode;\n}\n\nexport function SynapseProvider({ children, ...options }: SynapseProviderProps) {\n // Use a ref so the same Synapse instance survives StrictMode's\n // unmount/remount cycle. We intentionally do NOT destroy on unmount\n // because StrictMode re-mounts immediately and the transport must\n // stay alive. The instance is GC'd when the provider is truly removed.\n const ref = useRef<Synapse | null>(null);\n\n if (ref.current === null || ref.current.destroyed) {\n ref.current = createSynapse(options);\n }\n\n return (\n <SynapseContext.Provider value={ref.current}>\n <ThemeInjector synapse={ref.current} />\n {children}\n </SynapseContext.Provider>\n );\n}\n\n/** Injects theme CSS variables onto document.documentElement whenever the theme changes. */\nfunction ThemeInjector({ synapse }: { synapse: Synapse }) {\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n useEffect(() => {\n // Neutral defaults for the mode back any var the host omits, then host\n // values win — the same single path used by the handshake in core.ts.\n applyTheme(theme.mode, theme.tokens, theme.fontFaces);\n }, [theme]);\n\n return null;\n}\n\nexport function useSynapseContext(): Synapse {\n const ctx = useContext(SynapseContext);\n if (!ctx) {\n throw new Error(\n \"useSynapse must be used within a <SynapseProvider>. \" +\n \"Wrap your app component tree with <SynapseProvider>.\",\n );\n }\n return ctx;\n}\n","import type { McpUiHostContext } from \"@modelcontextprotocol/ext-apps\";\nimport type { Task, TaskStatus } from \"@modelcontextprotocol/sdk/types.js\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type {\n ActionReducer,\n AgentAction,\n CallToolAsTaskOptions,\n DataChangedEvent,\n FileResult,\n RequestFileOptions,\n Store,\n StoreDispatch,\n Synapse,\n SynapseTheme,\n TaskHandle,\n ToolCallResult,\n} from \"../types.js\";\nimport { SynapseProvider, useSynapseContext } from \"./provider.js\";\n\n// Re-export provider components\nexport { SynapseProvider };\n\nexport function useSynapse(): Synapse {\n return useSynapseContext();\n}\n\nexport function useCallTool<TOutput = unknown>(\n toolName: string,\n): {\n call: (args?: Record<string, unknown>) => Promise<ToolCallResult<TOutput>>;\n isPending: boolean;\n error: Error | null;\n data: TOutput | null;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [data, setData] = useState<TOutput | null>(null);\n const callIdRef = useRef(0);\n\n const call = useCallback(\n async (args?: Record<string, unknown>): Promise<ToolCallResult<TOutput>> => {\n const id = ++callIdRef.current;\n setIsPending(true);\n setError(null);\n\n try {\n const result = await synapse.callTool<Record<string, unknown>, TOutput>(toolName, args);\n // Stale guard: only update if this is still the latest call\n if (id === callIdRef.current) {\n setData(result.data);\n setIsPending(false);\n }\n return result;\n } catch (err) {\n if (id === callIdRef.current) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n setIsPending(false);\n }\n throw err;\n }\n },\n [synapse, toolName],\n );\n\n return { call, isPending, error, data };\n}\n\nexport function useDataSync(callback: (event: DataChangedEvent) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onDataChanged((event) => callbackRef.current(event));\n }, [synapse]);\n}\n\n/**\n * Subscribe to agent actions — typed, declarative commands from the server.\n *\n * Actions are emitted by tools as deterministic side effects (e.g., \"navigate\n * to the board I just created\"). The UI decides how to handle each action type.\n *\n * @example\n * ```tsx\n * useAgentAction((action) => {\n * if (action.type === \"navigate\") {\n * const { entity, id } = action.payload as NavigatePayload;\n * if (entity === \"board\") setSelectedBoardId(id);\n * }\n * });\n * ```\n */\nexport function useAgentAction(callback: (action: AgentAction) => void): void {\n const synapse = useSynapseContext();\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n return synapse.onAction((action) => callbackRef.current(action));\n }, [synapse]);\n}\n\n/**\n * Subscribe to the full ext-apps host context.\n *\n * Returns the host context bag — spec-standardized fields (`theme`, `styles`,\n * `displayMode`, `toolInfo`) plus any host extensions. Re-renders on every\n * `ui/notifications/host-context-changed` notification.\n *\n * Prefer `useTheme()` when only theming matters (it filters no-op fires and\n * returns a typed `SynapseTheme`). Reach for `useHostContext()` for non-theme\n * fields like host-specific extensions, e.g. on NimbleBrain:\n *\n * ```tsx\n * const { workspace } = useHostContext<{ workspace?: { id: string } }>();\n * ```\n */\nexport function useHostContext<T extends McpUiHostContext = McpUiHostContext>(): T {\n const synapse = useSynapseContext();\n const [ctx, setCtx] = useState<T>(() => synapse.getHostContext() as T);\n\n useEffect(() => {\n // Sync in case context changed between render and effect\n setCtx(synapse.getHostContext() as T);\n return synapse.onHostContextChanged((c) => setCtx(c as T));\n }, [synapse]);\n\n return ctx;\n}\n\n/**\n * Re-renders only when the derived `SynapseTheme` actually changes (mode,\n * primaryColor, or any token value). Routes through `synapse.onThemeChanged`\n * — which is itself a `themesEqual`-filtered selector over the unified host\n * context — so a host-context update that doesn't move the theme (e.g. a\n * workspace switch) does NOT cause this hook's consumers to re-render.\n *\n * Building this on `useHostContext` instead would skip the filter: every\n * host-context-changed notification produces a new context reference, which\n * would propagate through `useState` and force a re-render even when the\n * derived theme is unchanged.\n */\nexport function useTheme(): SynapseTheme {\n const synapse = useSynapseContext();\n const [theme, setTheme] = useState<SynapseTheme>(() => synapse.getTheme());\n\n useEffect(() => {\n // Sync in case theme changed between render and effect\n setTheme(synapse.getTheme());\n return synapse.onThemeChanged(setTheme);\n }, [synapse]);\n\n return theme;\n}\n\nexport function useAction(): (action: string, params?: Record<string, unknown>) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (action: string, params?: Record<string, unknown>) => synapse.action(action, params),\n [synapse],\n );\n}\n\nexport function useChat(): (\n message: string,\n context?: { action?: string; entity?: string },\n) => void {\n const synapse = useSynapseContext();\n return useCallback(\n (message: string, context?: { action?: string; entity?: string }) =>\n synapse.chat(message, context),\n [synapse],\n );\n}\n\n/**\n * Push the app's visible state to the agent via ext-apps `ui/update-model-context`.\n *\n * **Imperative** (no args) — returns a push function you call manually:\n * ```tsx\n * const push = useVisibleState();\n * push({ board: selectedBoard }, \"Viewing board X\");\n * ```\n *\n * **Declarative** (factory + deps) — auto-pushes when deps change:\n * ```tsx\n * useVisibleState(() => ({\n * state: { board: selectedBoard },\n * summary: `Viewing \"${selectedBoard?.name}\"`,\n * }), [selectedBoard]);\n * ```\n */\nexport function useVisibleState(): (state: Record<string, unknown>, summary?: string) => void;\nexport function useVisibleState(\n factory: () => { state: Record<string, unknown>; summary?: string },\n deps: unknown[],\n): void;\nexport function useVisibleState(\n factory?: () => { state: Record<string, unknown>; summary?: string },\n deps?: unknown[],\n): ((state: Record<string, unknown>, summary?: string) => void) | undefined {\n const synapse = useSynapseContext();\n const push = useCallback(\n (state: Record<string, unknown>, summary?: string) => synapse.setVisibleState(state, summary),\n [synapse],\n );\n\n // Declarative mode: auto-push when deps change.\n // The deps array is caller-provided (mirrors useMemo/useEffect pattern).\n const factoryRef = useRef(factory);\n factoryRef.current = factory;\n useEffect(() => {\n if (!factoryRef.current) return;\n const { state, summary } = factoryRef.current();\n push(state, summary);\n }, [...(deps ?? []), push]);\n\n if (!factory) return push;\n}\n\nexport function useFileUpload(): {\n pickFile: (options?: RequestFileOptions) => Promise<FileResult | null>;\n pickFiles: (options?: RequestFileOptions) => Promise<FileResult[]>;\n isPending: boolean;\n} {\n const synapse = useSynapseContext();\n const [isPending, setIsPending] = useState(false);\n\n const pickFile = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFile(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n const pickFiles = useCallback(\n async (options?: RequestFileOptions) => {\n setIsPending(true);\n try {\n return await synapse.pickFiles(options);\n } finally {\n setIsPending(false);\n }\n },\n [synapse],\n );\n\n return { pickFile, pickFiles, isPending };\n}\n\nexport function useStore<TState, TActions extends Record<string, ActionReducer<TState, any>>>(\n store: Store<TState, TActions>,\n): {\n state: TState;\n dispatch: StoreDispatch<TActions>;\n} {\n const state = useSyncExternalStore(\n (onStoreChange) => store.subscribe(onStoreChange),\n () => store.getState(),\n () => store.getState(),\n );\n\n return { state, dispatch: store.dispatch };\n}\n\n// -----------------------------------------------------------------------------\n// useCallToolAsTask — lifecycle wrapper around `synapse.callToolAsTask`\n// -----------------------------------------------------------------------------\n\n/**\n * Spec terminal status values for the MCP 2025-11-25 tasks utility.\n *\n * Typed via `satisfies TaskStatus` so a rename of any member of the\n * spec enum (`completed`/`failed`/`cancelled`) trips `tsc` — we never\n * hand-type these as bare string literals in comparisons.\n */\nconst COMPLETED_STATUS = \"completed\" satisfies TaskStatus;\nconst FAILED_STATUS = \"failed\" satisfies TaskStatus;\nconst CANCELLED_STATUS = \"cancelled\" satisfies TaskStatus;\n\nconst TERMINAL_STATUSES: ReadonlySet<TaskStatus> = new Set<TaskStatus>([\n COMPLETED_STATUS,\n FAILED_STATUS,\n CANCELLED_STATUS,\n]);\n\n/**\n * Fallback poll cadence used when the receiver's `CreateTaskResult.task`\n * carries no `pollInterval`. Chosen to roughly match the 5s cadence\n * described in Task 005 (\"a sensible default like 5s if pollInterval is\n * absent\") — the effective fire delay is this value × 1.5 ≈ 7.5s, well\n * below default TTLs but long enough to avoid hammering hosts that do\n * emit `notifications/tasks/status`.\n */\nconst DEFAULT_POLL_INTERVAL_MS = 5_000;\nconst POLL_FALLBACK_MULTIPLIER = 1.5;\n\n/**\n * Stop the poll fallback after this many consecutive `refresh()` failures.\n * Bridge teardown / TTL eviction / network outage all manifest as repeated\n * `tasks/get` rejections. Without a guard the timer re-arms forever; with\n * it we stop polling silently after `MAX_REFRESH_FAILURES` strikes — the\n * blocking `result()` path remains the authoritative source of truth, so\n * giving up on polling never loses the terminal value.\n */\nconst MAX_REFRESH_FAILURES = 5;\n\nexport interface UseCallToolAsTaskResult<TInput, TOutput> {\n /**\n * Start (or re-start) a task-augmented tool call. Returns the\n * resolved `TaskHandle` so callers can `await fire(...)` if they\n * want to know when the server has accepted the task, but reading\n * `task`/`result`/`error` from the hook is usually enough.\n *\n * Re-firing while a previous task is still in flight detaches this\n * hook from the prior handle (stops polling, unsubscribes) but does\n * NOT cancel the server-side task — the task keeps running and its\n * result may still be fetched elsewhere (e.g. on page revisit).\n */\n fire(args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>;\n /** Latest `Task` state, or `null` before `fire()` has been called. */\n task: Task | null;\n /** Populated once `handle.result()` resolves non-error. */\n result: ToolCallResult<TOutput> | null;\n /** Populated on rejection or when `result.isError === true`. */\n error: Error | null;\n /** `true` while the task is non-terminal (`working` / `input_required`). */\n isWorking: boolean;\n /** `true` when `task.status ∈ {completed, failed, cancelled}`. */\n isTerminal: boolean;\n /**\n * Cancel the active task via `tasks/cancel`. No-op when no task is\n * active. Swallowed errors surface via `error`.\n */\n cancel(): Promise<void>;\n}\n\n/**\n * React hook wrapper around `synapse.callToolAsTask`.\n *\n * Handles the full MCP 2025-11-25 task lifecycle:\n *\n * 1. `fire(args, options?)` sends the task-augmented `tools/call` and\n * stores the returned `TaskHandle` in a ref.\n * 2. Subscribes to `handle.onStatus` — updates `task` whenever the\n * host emits `notifications/tasks/status` (OPTIONAL per spec).\n * 3. Starts a polling fallback: if no status notification arrives\n * within `pollInterval × 1.5` (defaulting to ~7.5s), calls\n * `handle.refresh()` for canonical state. Stops on terminal.\n * 4. Awaits `handle.result()` in the background — resolves to either\n * `result` (success / `isError: false`) or `error` (network reject\n * OR `result.isError === true`).\n *\n * Cleanup (unmount or re-fire) unsubscribes from status events and\n * clears the poll timer, but does NOT cancel the server-side task —\n * the caller may remount and recover state by firing again, and tasks\n * outlive iframe teardown until TTL elapses.\n */\nexport function useCallToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(\n toolName: string,\n): UseCallToolAsTaskResult<TInput, TOutput> {\n const synapse = useSynapseContext();\n\n const [task, setTask] = useState<Task | null>(null);\n const [result, setResult] = useState<ToolCallResult<TOutput> | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Per-fire generation counter. Every `fire()` increments; any\n // asynchronous callback (status listener, poll timer, `result()`\n // resolution) captures the gen at schedule time and bails if the\n // current gen has moved past it. This is the single source of truth\n // for \"is this work still relevant?\" — more robust than comparing\n // TaskHandle identity because handles can be detached by re-fire.\n const genRef = useRef(0);\n\n // Active handle + its teardown handles. We keep both in refs so the\n // hook's stable `fire`/`cancel` callbacks can reach the current\n // lifecycle state without re-binding on every render.\n const handleRef = useRef<TaskHandle<TOutput> | null>(null);\n const unsubscribeRef = useRef<(() => void) | null>(null);\n const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // `pollInterval × 1.5`, captured per-fire. Falls back to the 5s\n // default when the host didn't provide a `pollInterval` in the\n // initial CreateTaskResult.task.\n const pollDelayRef = useRef<number>(DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER);\n\n // Track the latest known status out-of-band so the poll callback\n // can decide whether to keep polling without depending on the\n // `task` React state (which lags a render behind setState).\n const terminalRef = useRef<boolean>(false);\n\n // Consecutive `refresh()` failure count for the active fire. Reset\n // on every successful refresh, status notification, or new fire.\n const refreshFailureCountRef = useRef<number>(0);\n\n const clearPollTimer = useCallback(() => {\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n }, []);\n\n const detachCurrent = useCallback(() => {\n clearPollTimer();\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n }, [clearPollTimer]);\n\n const scheduleNextPoll = useCallback(\n (gen: number) => {\n clearPollTimer();\n if (terminalRef.current) return;\n pollTimerRef.current = setTimeout(() => {\n // Bail if this fire has been superseded or torn down.\n if (gen !== genRef.current) return;\n const h = handleRef.current;\n if (!h) return;\n if (terminalRef.current) return;\n // `refresh()` is the canonical source for `createdAt` /\n // `lastUpdatedAt` / `ttl` — notification-derived Tasks carry\n // placeholders per the router in `task-handle.ts`.\n h.refresh().then(\n (fresh) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(fresh);\n const isTerminal = TERMINAL_STATUSES.has(fresh.status);\n terminalRef.current = isTerminal;\n if (!isTerminal) scheduleNextPoll(gen);\n },\n () => {\n // Swallow refresh errors — the blocking `tasks/result` is\n // the authoritative path; polling is best-effort. But guard\n // against runaway re-arming: if the bridge is gone or the\n // task TTL has elapsed, every refresh rejects. Stop after\n // MAX_REFRESH_FAILURES consecutive strikes; result() will\n // still surface a terminal value or error when it settles.\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current += 1;\n if (refreshFailureCountRef.current >= MAX_REFRESH_FAILURES) return;\n if (!terminalRef.current) scheduleNextPoll(gen);\n },\n );\n }, pollDelayRef.current);\n },\n [clearPollTimer],\n );\n\n const fire = useCallback(\n async (args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>> => {\n // Detach any in-flight prior task BEFORE incrementing the gen so\n // its callbacks see the new gen and bail. (Incrementing then\n // detaching would also work, but detach-first makes the order\n // obvious: stop listening, bump generation, start fresh.)\n detachCurrent();\n const gen = ++genRef.current;\n\n // Reset per-fire state. Don't wipe `task` yet — `callToolAsTask`\n // is async; showing the previous terminal state briefly is less\n // jarring than flicker to null → working. We clear on resolution.\n setResult(null);\n setError(null);\n terminalRef.current = false;\n refreshFailureCountRef.current = 0;\n\n let handle: TaskHandle<TOutput>;\n try {\n handle = await synapse.callToolAsTask<TInput, TOutput>(toolName, args, options);\n } catch (err) {\n if (gen !== genRef.current) throw err;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n throw err;\n }\n\n // Caller superseded the fire between request and response —\n // don't attach listeners, but still return the handle so the\n // awaiter can observe it.\n if (gen !== genRef.current) return handle;\n\n handleRef.current = handle;\n\n // Derive the fallback poll delay from the receiver's advertised\n // `pollInterval`. Spec allows it to be absent; we then use the\n // 5s default described in Task 005.\n const hintedInterval = handle.task.pollInterval;\n pollDelayRef.current =\n typeof hintedInterval === \"number\" && hintedInterval > 0\n ? hintedInterval * POLL_FALLBACK_MULTIPLIER\n : DEFAULT_POLL_INTERVAL_MS * POLL_FALLBACK_MULTIPLIER;\n\n setTask(handle.task);\n terminalRef.current = TERMINAL_STATUSES.has(handle.task.status);\n\n // Subscribe to `notifications/tasks/status`. Each notification\n // resets the poll countdown (that's the whole point of the\n // \"notification OR polling\" contract — if notifications flow,\n // we don't poll; if they don't, the timer fires).\n unsubscribeRef.current = handle.onStatus((updated) => {\n if (gen !== genRef.current) return;\n refreshFailureCountRef.current = 0;\n setTask(updated);\n const isTerminal = TERMINAL_STATUSES.has(updated.status);\n terminalRef.current = isTerminal;\n if (isTerminal) {\n clearPollTimer();\n } else {\n scheduleNextPoll(gen);\n }\n });\n\n // Kick off the blocking result fetch — this is the authoritative\n // terminal value regardless of whether notifications or polls\n // landed in between. By spec, `tasks/result` blocks until the task\n // reaches a terminal status, so when this settles we KNOW the task\n // is terminal — stop polling and synthesize a terminal `task`\n // status so derived flags (`isTerminal`, `isWorking`) match the\n // populated `result` / `error` immediately.\n handle.result().then(\n (res) => {\n if (gen !== genRef.current) return;\n terminalRef.current = true;\n clearPollTimer();\n // Synthesize the terminal Task: failed if `isError`, otherwise\n // completed. The next status notification or refresh would\n // confirm this, but we want internal state consistent the\n // instant `result` is populated — a \"result populated while\n // isWorking=true\" render is incoherent for consumers.\n setTask((prev) => {\n const status: TaskStatus = res.isError ? \"failed\" : \"completed\";\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status, lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status,\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n if (res.isError) {\n // Spec: `CallToolResult.isError === true` is a tool-level\n // error, not a protocol error. Surface via `error` for\n // consumers who treat it as a failure, but also populate\n // `result` so callers inspecting the raw content block\n // still have access.\n setResult(res);\n const msg =\n typeof res.data === \"string\" && res.data.length > 0\n ? res.data\n : `Tool \"${toolName}\" returned isError: true`;\n setError(new Error(msg));\n } else {\n setResult(res);\n }\n },\n (err) => {\n if (gen !== genRef.current) return;\n // result() rejection means the `tasks/result` RPC failed\n // (transport error, taskId not found, bridge teardown). We\n // can't know the server-side task's actual final state, but\n // we know polling won't recover here either — same transport.\n // Mark terminal and synthesize `failed` status for UX\n // coherence; the populated `error` tells the consumer what\n // specifically went wrong.\n terminalRef.current = true;\n clearPollTimer();\n setTask((prev) => {\n const now = new Date().toISOString();\n return prev\n ? { ...prev, status: \"failed\", lastUpdatedAt: now }\n : {\n taskId: handle.task.taskId,\n status: \"failed\",\n ttl: handle.task.ttl,\n createdAt: handle.task.createdAt,\n lastUpdatedAt: now,\n };\n });\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n },\n );\n\n // Start the poll fallback only if we aren't already terminal.\n if (!terminalRef.current) scheduleNextPoll(gen);\n\n return handle;\n },\n [synapse, toolName, detachCurrent, clearPollTimer, scheduleNextPoll],\n );\n\n const cancel = useCallback(async (): Promise<void> => {\n const h = handleRef.current;\n if (!h) return;\n const gen = genRef.current;\n try {\n const cancelled = await h.cancel();\n if (gen !== genRef.current) return;\n setTask(cancelled);\n terminalRef.current = TERMINAL_STATUSES.has(cancelled.status);\n clearPollTimer();\n } catch (err) {\n if (gen !== genRef.current) return;\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n }\n }, [clearPollTimer]);\n\n // Cleanup on unmount: stop polling, drop the status subscription.\n // Deliberately do NOT call `handle.cancel()` — per Task 005, the\n // server-side task keeps running so a remount can recover state.\n useEffect(() => {\n return () => {\n genRef.current += 1;\n if (pollTimerRef.current !== null) {\n clearTimeout(pollTimerRef.current);\n pollTimerRef.current = null;\n }\n if (unsubscribeRef.current) {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n }\n handleRef.current = null;\n };\n }, []);\n\n const isTerminal = task !== null && TERMINAL_STATUSES.has(task.status);\n const isWorking = task !== null && !isTerminal;\n\n return { fire, task, result, error, isWorking, isTerminal, cancel };\n}\n"]} |
@@ -1,1 +0,1 @@ | ||
| (function(){'use strict';var de={"--color-background-primary":"#ffffff","--color-background-secondary":"#fafafa","--color-background-tertiary":"#f3f4f6","--color-text-primary":"#111827","--color-text-secondary":"#6b7280","--color-text-tertiary":"#9ca3af","--color-text-accent":"#2563eb","--nb-color-accent-foreground":"#ffffff","--color-border-primary":"#e5e7eb","--color-border-secondary":"#d1d5db","--color-ring-primary":"#2563eb","--nb-color-danger":"#dc2626","--nb-color-success":"#059669","--nb-color-warning":"#f59e0b","--nb-color-warm":"#d4620a","--nb-color-warm-light":"#fef5ee","--nb-color-processing":"#7c3aed","--nb-color-processing-light":"#f3eeff","--nb-color-info-light":"#eef4ff"},ue={"--color-background-primary":"#18181b","--color-background-secondary":"#27272a","--color-background-tertiary":"#2f2f34","--color-text-primary":"#fafafa","--color-text-secondary":"#a1a1aa","--color-text-tertiary":"#71717a","--color-text-accent":"#818cf8","--nb-color-accent-foreground":"#ffffff","--color-border-primary":"#3f3f46","--color-border-secondary":"#52525b","--color-ring-primary":"#818cf8","--nb-color-danger":"#f87171","--nb-color-success":"#34d399","--nb-color-warning":"#fbbf24","--nb-color-warm":"#fb923c","--nb-color-warm-light":"#3a2a1e","--nb-color-processing":"#a78bfa","--nb-color-processing-light":"#2a2440","--nb-color-info-light":"#1e2a44"},pe={light:de,dark:ue};function N(t,r){if(typeof document>"u")return;let n=document.documentElement.style;for(let[i,l]of Object.entries(pe[t]))n.setProperty(i,l);if(r&&typeof r=="object")for(let[i,l]of Object.entries(r))typeof i=="string"&&typeof l=="string"&&n.setProperty(i,l);}function R(t){typeof document<"u"&&document.documentElement.setAttribute("data-theme",t.mode),N(t.mode,t.tokens);}function C(t){try{if(t?.matchMedia?.("(prefers-color-scheme: dark)").matches)return "dark"}catch{}return "light"}function h(t,r){return t==="light"||t==="dark"?t:r}var b=class extends Error{constructor(r,n){super(`"${r}" is not supported by the "${n}" host`),this.name="HostUnsupportedError";}},A="synapse-ui-data",z="ui-lifecycle-iframe-ready",j="ui-lifecycle-iframe-render-data",G="ui-size-change",K="link",W="prompt",S="openai:set_globals",F="2026-01-26",Z="ui/initialize",q="ui/notifications/initialized",Y="ui/notifications/tool-result",V="ui/notifications/host-context-changed",B="ui/notifications/size-changed",Q="ui/open-link",$="ui/message",X="ui/resource-teardown",J="tools/call";function ee(t,r){let n=()=>t.openai,i=n()?.toolOutput??null,l={mode:h(n()?.theme,"light"),tokens:{}},a=new Set,p=new Set,f=false,m=s=>{if(f)return;let c=s.detail?.globals;if(c){if("toolOutput"in c&&c.toolOutput!=null){i=c.toolOutput;for(let u of a)u(i);}if("theme"in c&&c.theme!=null){let u=h(c.theme,l.mode);if(u!==l.mode){l={mode:u,tokens:{}};for(let P of p)P(l);}}}};return {host:"chatgpt",getData:()=>i,onData(s){return a.add(s),()=>a.delete(s)},getTheme:()=>l,onTheme(s){return p.add(s),()=>p.delete(s)},async callTool(s,c){let u=n()?.callTool;if(!u)throw new b("callTool","chatgpt");return await u(s,c??{})},sendPrompt(s){let c=n();(c?.sendFollowUpMessage??c?.sendFollowupMessage)?.({prompt:s});},openLink(s){let c=n()?.openExternal;c?c({href:s}):t.open(s,"_blank","noopener,noreferrer");},resize(){},capabilities(){let s=n();return {pull:typeof s?.callTool=="function",sendPrompt:typeof s?.sendFollowUpMessage=="function"||typeof s?.sendFollowupMessage=="function",openLink:true}},start(){t.addEventListener(S,m,{passive:true}),i=n()?.toolOutput??i;},destroy(){f||(f=true,t.removeEventListener(S,m),a.clear(),p.clear());}}}function O(t,r){if(!t)return null;let i=t.getElementById(r)?.textContent;if(!i)return null;try{return JSON.parse(i)??null}catch{return null}}function L(t){if(t==null||typeof t!="object")return null;let r=t,n=r.renderData,i=n!=null&&typeof n=="object"?n:r;return i.toolOutput!=null?i.toolOutput:i.structuredContent!=null?i.structuredContent:i}function te(t,r){let n=r.dataElementId??A,i=null,l={mode:C(t),tokens:{}},a=new Set,p=false,f=null,m=s=>{if(p)return;let c=h(s.matches?"dark":"light",l.mode);if(c!==l.mode){l={mode:c,tokens:{}};for(let u of a)u(l);}};return {host:"generic",getData:()=>i,onData(){return ()=>{}},getTheme:()=>l,onTheme(s){return a.add(s),()=>a.delete(s)},async callTool(s){throw new b("callTool","generic")},sendPrompt(){},openLink(s){t.open(s,"_blank","noopener,noreferrer");},resize(){},capabilities(){return {pull:false,sendPrompt:false,openLink:true}},start(){i=O(t.document,n);try{f=t.matchMedia?.("(prefers-color-scheme: dark)")??null,f?.addEventListener?.("change",m);}catch{f=null;}},destroy(){p||(p=true,f?.removeEventListener?.("change",m),f=null,a.clear());}}}var fe=3e4;function ne(t,r){let n=r.dataElementId??A,i=r.autoResize!==false,l=null,a={mode:C(t),tokens:{}},p=new Set,f=new Set,m=false,s=false,c=1,u=new Map,P=-1,v=null,k=null,U=()=>t.parent??t;function I(e){U().postMessage(e,"*");}function _(e){s||I(e);}function x(e,o){I({jsonrpc:"2.0",method:e,params:o??{}});}function w(e,o){let d=c++;return new Promise((g,y)=>{let T=setTimeout(()=>{u.delete(d),y(new Error(`"${e}" timed out`));},fe);u.set(d,{resolve:g,reject:y,timer:T}),I({jsonrpc:"2.0",id:d,method:e,params:o??{}});})}function D(e){if(e!=null){l=e;for(let o of p)o(e);}}function M(e){if(!e||typeof e!="object")return;let{mode:o,tokens:d}=a,g=false;if(e.theme!=null){let T=h(e.theme,o);T!==o&&(o=T,g=true);}let y=e.styles;if(y?.variables&&typeof y.variables=="object"&&(d={...d,...y.variables},g=true),g){a={mode:o,tokens:d};for(let T of f)T(a);}}function E(e){if(m)return;let o=typeof e=="number"?e:Math.ceil(t.document.body.scrollHeight);o!==P&&(P=o,x(B,{height:o}),_({type:G,payload:{height:o}}));}function se(e){let o=Number(e.id),d=u.get(o);if(d)if(u.delete(o),clearTimeout(d.timer),e.error!=null){let g=e.error;d.reject(new Error(g.message??"request failed"));}else d.resolve(e.result);}function ie(e,o){if(e===Y){let d=o.structuredContent;D(d??L(o));}else e===V&&M(o);}function ae(e){e.method===X&&I({jsonrpc:"2.0",id:e.id,result:{}});}function le(e){if(e.type===j||e.type==="renderData"){let{theme:o,...d}=e.payload??{};o!=null&&M({theme:o}),Object.keys(d).length>0&&D(L(d));}}let H=e=>{if(m||e.source&&e.source!==U())return;let o=e.data;if(!(!o||typeof o!="object")){if(o.jsonrpc!=="2.0"){le(o);return}o.id!=null&&("result"in o||"error"in o)?se(o):typeof o.method=="string"&&(o.id!=null?ae(o):ie(o.method,o.params??{}));}};function ce(){k=()=>E(),t.addEventListener("resize",k),typeof t.ResizeObserver<"u"&&(v=new t.ResizeObserver(()=>E()),v.observe(t.document.body));}return {host:"claude",getData:()=>l,onData(e){return p.add(e),()=>p.delete(e)},getTheme:()=>a,onTheme(e){return f.add(e),()=>f.delete(e)},async callTool(e,o){return await w(J,{name:e,arguments:o??{}})},sendPrompt(e){w($,{role:"user",content:[{type:"text",text:e}]}).catch(()=>{}),_({type:W,payload:{prompt:e}});},openLink(e){w(Q,{url:e}).catch(()=>{}),_({type:K,payload:{url:e}});},resize(e){E(e);},capabilities(){return {pull:true,sendPrompt:true,openLink:true}},start(){t.addEventListener("message",H),l=O(t.document,n),i&&ce(),I({type:z}),w(Z,{appInfo:{name:r.name??"synapse-ui",version:r.version??"0.0.0"},appCapabilities:{availableDisplayModes:["inline"]},protocolVersion:F}).then(e=>{m||(s=true,M(e?.hostContext),x(q,{}),P=-1,E());}).catch(()=>{}),E();},destroy(){if(!m){m=true,t.removeEventListener("message",H),k&&t.removeEventListener("resize",k),k=null,v?.disconnect(),v=null;for(let e of u.values())clearTimeout(e.timer),e.reject(new Error("adapter destroyed"));u.clear(),p.clear(),f.clear();}}}}function me(t){if(t.openai!=null)return "chatgpt";try{if(t.parent!=null&&t.parent!==t)return "claude"}catch{return "claude"}return "generic"}function ge(t,r,n){switch(t){case "chatgpt":return ee(r);case "claude":case "nimblebrain":return ne(r,n);default:return te(r,n)}}function oe(t,r){let n=r.host??me(t);return ge(n,t,r)}function re(t={}){let r=t.window??globalThis,n=oe(r,t);R(n.getTheme());let i=n.onTheme(R);n.start();let l=false;return {data:()=>n.getData(),onData:a=>n.onData(a),theme:()=>n.getTheme(),onTheme:a=>n.onTheme(a),callTool:(a,p)=>n.callTool(a,p),sendPrompt:a=>n.sendPrompt(a),openLink:a=>n.openLink(a),resize:a=>n.resize(a),capabilities:()=>n.capabilities(),host:()=>n.host,destroy(){l||(l=true,i(),n.destroy());}}}globalThis.SynapseUI={connect:re};})(); | ||
| (function(){'use strict';var he=new Set(["auto","block","swap","fallback","optional"]),Te={"--color-background-primary":"#ffffff","--color-background-secondary":"#fafafa","--color-background-tertiary":"#f3f4f6","--color-text-primary":"#111827","--color-text-secondary":"#6b7280","--color-text-tertiary":"#9ca3af","--color-text-accent":"#2563eb","--nb-color-accent-foreground":"#ffffff","--color-border-primary":"#e5e7eb","--color-border-secondary":"#d1d5db","--color-ring-primary":"#2563eb","--nb-color-danger":"#dc2626","--nb-color-success":"#059669","--nb-color-warning":"#f59e0b","--nb-color-warm":"#d4620a","--nb-color-warm-light":"#fef5ee","--nb-color-processing":"#7c3aed","--nb-color-processing-light":"#f3eeff","--nb-color-info-light":"#eef4ff"},be={"--color-background-primary":"#18181b","--color-background-secondary":"#27272a","--color-background-tertiary":"#2f2f34","--color-text-primary":"#fafafa","--color-text-secondary":"#a1a1aa","--color-text-tertiary":"#71717a","--color-text-accent":"#818cf8","--nb-color-accent-foreground":"#ffffff","--color-border-primary":"#3f3f46","--color-border-secondary":"#52525b","--color-ring-primary":"#818cf8","--nb-color-danger":"#f87171","--nb-color-success":"#34d399","--nb-color-warning":"#fbbf24","--nb-color-warm":"#fb923c","--nb-color-warm-light":"#3a2a1e","--nb-color-processing":"#a78bfa","--nb-color-processing-light":"#2a2440","--nb-color-info-light":"#1e2a44"},ke={light:Te,dark:be};function Pe(e,n){if(typeof document>"u")return;let o=document.documentElement.style;for(let[s,a]of Object.entries(ke[e]))o.setProperty(s,a);if(n&&typeof n=="object")for(let[s,a]of Object.entries(n))typeof s=="string"&&typeof a=="string"&&o.setProperty(s,a);}var S=new Set,G="";function A(e){return Array.isArray(e)?JSON.stringify(e.map(n=>[n.family,n.src,n.weight,n.style,n.display])):""}function x(e){if(!Array.isArray(e))return;let n=e.filter(we);if(!(e.length>0&&n.length===0))return n}function Ie(e){let n=x(e);if(n===void 0||typeof document>"u"||!document.fonts||typeof FontFace>"u")return;let o=A(n);if(o!==G){G=o;for(let s of S)try{document.fonts.delete(s);}catch{}S.clear();for(let s of n)try{let a=new FontFace(s.family,s.src,{weight:s.weight,style:s.style,display:s.display&&he.has(s.display)?s.display:"swap"});document.fonts.add(a),S.add(a);}catch{}}}function we(e){if(!e||typeof e!="object")return false;let n=e;return typeof n.family=="string"&&typeof n.src=="string"}function W(e,n,o){Pe(e,n),Ie(o);}function R(e){typeof document<"u"&&document.documentElement.setAttribute("data-theme",e.mode),W(e.mode,e.tokens,e.fontFaces);}function O(e){try{if(e?.matchMedia?.("(prefers-color-scheme: dark)").matches)return "dark"}catch{}return "light"}function h(e,n){return e==="light"||e==="dark"?e:n}var T=class extends Error{constructor(n,o){super(`"${n}" is not supported by the "${o}" host`),this.name="HostUnsupportedError";}},_="synapse-ui-data",V="ui-lifecycle-iframe-ready",Z="ui-lifecycle-iframe-render-data",Y="ui-size-change",q="link",B="prompt",U="openai:set_globals",Q="2026-01-26",X="ui/initialize",$="ui/notifications/initialized",J="ui/notifications/tool-result",ee="ui/notifications/host-context-changed",te="ui/notifications/size-changed",ne="ui/open-link",oe="ui/message",re="ui/resource-teardown",se="tools/call";function ie(e,n){let o=()=>e.openai,s=o()?.toolOutput??null,a={mode:h(o()?.theme,"light"),tokens:{}},c=new Set,u=new Set,f=false,m=i=>{if(f)return;let l=i.detail?.globals;if(l){if("toolOutput"in l&&l.toolOutput!=null){s=l.toolOutput;for(let p of c)p(s);}if("theme"in l&&l.theme!=null){let p=h(l.theme,a.mode);if(p!==a.mode){a={mode:p,tokens:{}};for(let b of u)b(a);}}}};return {host:"chatgpt",getData:()=>s,onData(i){return c.add(i),()=>c.delete(i)},getTheme:()=>a,onTheme(i){return u.add(i),()=>u.delete(i)},async callTool(i,l){let p=o()?.callTool;if(!p)throw new T("callTool","chatgpt");return await p(i,l??{})},sendPrompt(i){let l=o();(l?.sendFollowUpMessage??l?.sendFollowupMessage)?.({prompt:i});},openLink(i){let l=o()?.openExternal;l?l({href:i}):e.open(i,"_blank","noopener,noreferrer");},resize(){},capabilities(){let i=o();return {pull:typeof i?.callTool=="function",sendPrompt:typeof i?.sendFollowUpMessage=="function"||typeof i?.sendFollowupMessage=="function",openLink:true}},start(){e.addEventListener(U,m,{passive:true}),s=o()?.toolOutput??s;},destroy(){f||(f=true,e.removeEventListener(U,m),c.clear(),u.clear());}}}function M(e,n){if(!e)return null;let s=e.getElementById(n)?.textContent;if(!s)return null;try{return JSON.parse(s)??null}catch{return null}}function L(e){if(e==null||typeof e!="object")return null;let n=e,o=n.renderData,s=o!=null&&typeof o=="object"?o:n;return s.toolOutput!=null?s.toolOutput:s.structuredContent!=null?s.structuredContent:s}function ae(e,n){let o=n.dataElementId??_,s=null,a={mode:O(e),tokens:{}},c=new Set,u=false,f=null,m=i=>{if(u)return;let l=h(i.matches?"dark":"light",a.mode);if(l!==a.mode){a={mode:l,tokens:{}};for(let p of c)p(a);}};return {host:"generic",getData:()=>s,onData(){return ()=>{}},getTheme:()=>a,onTheme(i){return c.add(i),()=>c.delete(i)},async callTool(i){throw new T("callTool","generic")},sendPrompt(){},openLink(i){e.open(i,"_blank","noopener,noreferrer");},resize(){},capabilities(){return {pull:false,sendPrompt:false,openLink:true}},start(){s=M(e.document,o);try{f=e.matchMedia?.("(prefers-color-scheme: dark)")??null,f?.addEventListener?.("change",m);}catch{f=null;}},destroy(){u||(u=true,f?.removeEventListener?.("change",m),f=null,c.clear());}}}var Ee="synapse/fontFaces";function Ce(e){return x(e?.[Ee])}function ce(e,n){return Ce(n)??e}var ve=3e4;function le(e,n){let o=n.dataElementId??_,s=n.autoResize!==false,a=null,c={mode:O(e),tokens:{}},u=new Set,f=new Set,m=false,i=false,l=1,p=new Map,b=-1,E=null,k=null,H=()=>e.parent??e;function P(t){H().postMessage(t,"*");}function F(t){i||P(t);}function N(t,r){P({jsonrpc:"2.0",method:t,params:r??{}});}function C(t,r){let d=l++;return new Promise((g,y)=>{let w=setTimeout(()=>{p.delete(d),y(new Error(`"${t}" timed out`));},ve);p.set(d,{resolve:g,reject:y,timer:w}),P({jsonrpc:"2.0",id:d,method:t,params:r??{}});})}function z(t){if(t!=null){a=t;for(let r of u)r(t);}}function D(t){if(!t||typeof t!="object")return;let{mode:r,tokens:d,fontFaces:g}=c,y=false;if(t.theme!=null){let v=h(t.theme,r);v!==r&&(r=v,y=true);}let w=t.styles;w?.variables&&typeof w.variables=="object"&&(d={...d,...w.variables},y=true);let K=ce(g,t);if(A(K)!==A(g)&&(g=K,y=true),y){c={mode:r,tokens:d,...g?{fontFaces:g}:{}};for(let v of f)v(c);}}function I(t){if(m)return;let r=typeof t=="number"?t:Math.ceil(e.document.body.scrollHeight);r!==b&&(b=r,N(te,{height:r}),F({type:Y,payload:{height:r}}));}function ue(t){let r=Number(t.id),d=p.get(r);if(d)if(p.delete(r),clearTimeout(d.timer),t.error!=null){let g=t.error;d.reject(new Error(g.message??"request failed"));}else d.resolve(t.result);}function fe(t,r){if(t===J){let d=r.structuredContent;z(d??L(r));}else t===ee&&D(r);}function me(t){t.method===re&&P({jsonrpc:"2.0",id:t.id,result:{}});}function ge(t){if(t.type===Z||t.type==="renderData"){let{theme:r,...d}=t.payload??{};r!=null&&D({theme:r}),Object.keys(d).length>0&&z(L(d));}}let j=t=>{if(m||t.source&&t.source!==H())return;let r=t.data;if(!(!r||typeof r!="object")){if(r.jsonrpc!=="2.0"){ge(r);return}r.id!=null&&("result"in r||"error"in r)?ue(r):typeof r.method=="string"&&(r.id!=null?me(r):fe(r.method,r.params??{}));}};function ye(){k=()=>I(),e.addEventListener("resize",k),typeof e.ResizeObserver<"u"&&(E=new e.ResizeObserver(()=>I()),E.observe(e.document.body));}return {host:"claude",getData:()=>a,onData(t){return u.add(t),()=>u.delete(t)},getTheme:()=>c,onTheme(t){return f.add(t),()=>f.delete(t)},async callTool(t,r){return await C(se,{name:t,arguments:r??{}})},sendPrompt(t){C(oe,{role:"user",content:[{type:"text",text:t}]}).catch(()=>{}),F({type:B,payload:{prompt:t}});},openLink(t){C(ne,{url:t}).catch(()=>{}),F({type:q,payload:{url:t}});},resize(t){I(t);},capabilities(){return {pull:true,sendPrompt:true,openLink:true}},start(){e.addEventListener("message",j),a=M(e.document,o),s&&ye(),P({type:V}),C(X,{appInfo:{name:n.name??"synapse-ui",version:n.version??"0.0.0"},appCapabilities:{availableDisplayModes:["inline"]},protocolVersion:Q}).then(t=>{m||(i=true,D(t?.hostContext),N($,{}),b=-1,I());}).catch(()=>{}),I();},destroy(){if(!m){m=true,e.removeEventListener("message",j),k&&e.removeEventListener("resize",k),k=null,E?.disconnect(),E=null;for(let t of p.values())clearTimeout(t.timer),t.reject(new Error("adapter destroyed"));p.clear(),u.clear(),f.clear();}}}}function Ae(e){if(e.openai!=null)return "chatgpt";try{if(e.parent!=null&&e.parent!==e)return "claude"}catch{return "claude"}return "generic"}function Oe(e,n,o){switch(e){case "chatgpt":return ie(n);case "claude":case "nimblebrain":return le(n,o);default:return ae(n,o)}}function de(e,n){let o=n.host??Ae(e);return Oe(o,e,n)}function pe(e={}){let n=e.window??globalThis,o=de(n,e);R(o.getTheme());let s=o.onTheme(R);o.start();let a=false;return {data:()=>o.getData(),onData:c=>o.onData(c),theme:()=>o.getTheme(),onTheme:c=>o.onTheme(c),callTool:(c,u)=>o.callTool(c,u),sendPrompt:c=>o.sendPrompt(c),openLink:c=>o.openLink(c),resize:c=>o.resize(c),capabilities:()=>o.capabilities(),host:()=>o.host,destroy(){a||(a=true,s(),o.destroy());}}}globalThis.SynapseUI={connect:pe};})(); |
+1
-6
| { | ||
| "name": "@nimblebrain/synapse", | ||
| "version": "0.12.2", | ||
| "version": "0.13.0", | ||
| "description": "Agent-aware app SDK for the MCP ext-apps protocol", | ||
@@ -27,7 +27,2 @@ "type": "module", | ||
| }, | ||
| "./ui/fonts": { | ||
| "types": "./dist/ui/fonts.d.ts", | ||
| "import": "./dist/ui/fonts.js", | ||
| "require": "./dist/ui/fonts.cjs" | ||
| }, | ||
| "./ui/base": { | ||
@@ -34,0 +29,0 @@ "types": "./dist/ui/base.d.ts", |
+54
-2
@@ -51,3 +51,2 @@ # @nimblebrain/synapse | ||
| | `@nimblebrain/synapse/ui` | Component library — tokens, primitives, components, layouts | | ||
| | `@nimblebrain/synapse/ui/fonts` | Side-effect import that loads the brand fonts into the iframe | | ||
| | `@nimblebrain/synapse/ui/base` | Side-effect import that establishes the root-height chain (`html, body, #root`) the app shell fills. `AppFrame` does this automatically on render; import it in your entry to apply it before first paint | | ||
@@ -71,5 +70,10 @@ | `@nimblebrain/synapse/vite` | Vite plugin for dev mode | | ||
| - **The library holds no brand.** Tokens are `var(--token, neutral-fallback)` | ||
| references, not hex values. The host injects the real palette/fonts at runtime | ||
| references, not hex values. The host injects the real palette at runtime | ||
| (the MCP ext-apps `hostContext.styles.variables`), so the same app adopts | ||
| whatever host it runs in. Standalone, it renders in neutral fallbacks. | ||
| - **That includes typography — the SDK ships no fonts.** Font fallbacks are | ||
| web-safe system stacks, so an app renders correctly with no host, no network, | ||
| and no font files. A host that wants its own typeface sends `@font-face` | ||
| descriptors on the theme; the library fetches nothing on its own. See | ||
| [Host fonts](#host-fonts). | ||
| - **Theme via CSS, not React.** Components style with token-driven inline-style | ||
@@ -92,2 +96,50 @@ objects whose values are those `var()` refs, so theming — including light/dark — | ||
| ### Host fonts | ||
| A CSS custom property can *name* a font family but cannot *load* one, and an app | ||
| iframe is its own document — it inherits no `@font-face` from the host page. So a | ||
| host that sends only tokens is naming a typeface the app has no way to render. | ||
| `SynapseTheme.fontFaces` closes that gap: the host sends the faces alongside the | ||
| tokens, and the SDK loads them into the app document. | ||
| ```ts | ||
| // Host side — sent as the `synapse/fontFaces` host-context extension. | ||
| { | ||
| mode: "dark", | ||
| tokens: { "--font-sans": "'Your Sans', system-ui, sans-serif" }, | ||
| fontFaces: [ | ||
| { family: "Your Sans", src: "url('/fonts/your-sans.woff2') format('woff2')", weight: "400 700" }, | ||
| ], | ||
| } | ||
| ``` | ||
| | Field | Notes | | ||
| |---|---| | ||
| | `family` | Must match the family named in the host's `--font-*` token value. | | ||
| | `src` | Any CSS `src` descriptor — relative (`url('/fonts/x.woff2')`), absolute (`url('https://cdn.example/x.woff2')`), or `data:`. | | ||
| | `weight` | Single weight (`400`) or a variable range (`400 700`). Optional. | | ||
| | `style` | `normal`, `italic`, … Optional. | | ||
| | `display` | Defaults to `swap`, so text paints in the fallback rather than blocking. Optional. | | ||
| Three things worth knowing: | ||
| - **Sending no fonts is a supported configuration, not a degraded one.** Omit | ||
| `fontFaces` and the app renders in the web-safe fallbacks (`system-ui`, | ||
| `ui-monospace`, `Georgia`). | ||
| - **Give every `--font-*` token value a web-safe tail.** A bare family name with | ||
| no matching face falls through to the browser default, not to your intended | ||
| stack. `"'Your Sans', system-ui, sans-serif"`, never `"'Your Sans'"`. | ||
| - **The URL must satisfy the app iframe's CSP — and `'self'` may not mean what | ||
| you expect.** A host that mounts apps in a `srcdoc` iframe *without* | ||
| `allow-same-origin` (the usual choice, since granting it to third-party app | ||
| HTML is a sandbox escape) gives the frame an **opaque origin**, and CSP | ||
| `'self'` then matches nothing. Under a typical `font-src 'self' data:`, a | ||
| `data:` URI is the only form that works unconditionally; any http(s) URL — | ||
| self-hosted or CDN — needs the host to add that origin to the frame's CSP. | ||
| Check the host's policy before choosing between them. | ||
| Faces are applied through the same funnel as the token variables | ||
| (`applyTheme`), so a theme change can never leave an app with the host's palette | ||
| under the wrong typeface. | ||
| ## Quick Start | ||
@@ -94,0 +146,0 @@ |
| 'use strict'; | ||
| var chunkKTXASIFJ_cjs = require('./chunk-KTXASIFJ.cjs'); | ||
| var extApps = require('@modelcontextprotocol/ext-apps'); | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": extApps.TOOL_RESULT_METHOD, | ||
| "tool-input": extApps.TOOL_INPUT_METHOD, | ||
| "tool-input-partial": extApps.TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": extApps.TOOL_CANCELLED_METHOD, | ||
| "theme-changed": extApps.HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: extApps.RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| extApps.INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(currentTheme.mode, currentTheme.tokens); | ||
| } | ||
| } | ||
| transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(mode, tokens); | ||
| const set = handlers.get(extApps.HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([extApps.HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === extApps.TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(extApps.INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| { | ||
| const theme = extractTheme(currentHostContext); | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens); | ||
| } | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| const theme = extractTheme(currentHostContext); | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| exports.connect = connect; | ||
| exports.createSynapse = createSynapse; | ||
| //# sourceMappingURL=chunk-A4ENX6XW.cjs.map | ||
| //# sourceMappingURL=chunk-A4ENX6XW.cjs.map |
Sorry, the diff of this file is too big to display
| 'use strict'; | ||
| var chunkKTXASIFJ_cjs = require('./chunk-KTXASIFJ.cjs'); | ||
| // src/host/theme.ts | ||
| function applyHostTheme(theme) { | ||
| if (typeof document !== "undefined") { | ||
| document.documentElement.setAttribute("data-theme", theme.mode); | ||
| } | ||
| chunkKTXASIFJ_cjs.applyThemeVariables(theme.mode, theme.tokens); | ||
| } | ||
| function preferredMode(win) { | ||
| try { | ||
| if (win?.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark"; | ||
| } catch { | ||
| } | ||
| return "light"; | ||
| } | ||
| function coerceMode(value, fallback) { | ||
| return value === "light" || value === "dark" ? value : fallback; | ||
| } | ||
| // src/host/types.ts | ||
| var HostUnsupportedError = class extends Error { | ||
| constructor(feature, host) { | ||
| super(`"${feature}" is not supported by the "${host}" host`); | ||
| this.name = "HostUnsupportedError"; | ||
| } | ||
| }; | ||
| var SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| var MCPUI_READY = "ui-lifecycle-iframe-ready"; | ||
| var MCPUI_RENDER_DATA = "ui-lifecycle-iframe-render-data"; | ||
| var MCPUI_SIZE_CHANGE = "ui-size-change"; | ||
| var MCPUI_LINK = "link"; | ||
| var MCPUI_PROMPT = "prompt"; | ||
| var OPENAI_SET_GLOBALS = "openai:set_globals"; | ||
| var MCPAPP_PROTOCOL_VERSION = "2026-01-26"; | ||
| var MCPAPP_INITIALIZE = "ui/initialize"; | ||
| var MCPAPP_INITIALIZED = "ui/notifications/initialized"; | ||
| var MCPAPP_TOOL_RESULT = "ui/notifications/tool-result"; | ||
| var MCPAPP_HOST_CONTEXT_CHANGED = "ui/notifications/host-context-changed"; | ||
| var MCPAPP_SIZE_CHANGED = "ui/notifications/size-changed"; | ||
| var MCPAPP_OPEN_LINK = "ui/open-link"; | ||
| var MCPAPP_MESSAGE = "ui/message"; | ||
| var MCPAPP_TEARDOWN = "ui/resource-teardown"; | ||
| var MCP_TOOLS_CALL = "tools/call"; | ||
| // src/host/adapters/chatgpt.ts | ||
| function createChatGPTAdapter(win, _options) { | ||
| const openai = () => win.openai; | ||
| let currentData = openai()?.toolOutput ?? null; | ||
| let currentTheme = { | ||
| mode: coerceMode(openai()?.theme, "light"), | ||
| tokens: {} | ||
| }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| const onSetGlobals = (event) => { | ||
| if (destroyed) return; | ||
| const globals = event.detail?.globals; | ||
| if (!globals) return; | ||
| if ("toolOutput" in globals && globals.toolOutput != null) { | ||
| currentData = globals.toolOutput; | ||
| for (const cb of dataCbs) cb(currentData); | ||
| } | ||
| if ("theme" in globals && globals.theme != null) { | ||
| const mode = coerceMode(globals.theme, currentTheme.mode); | ||
| if (mode !== currentTheme.mode) { | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| host: "chatgpt", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| const call = openai()?.callTool; | ||
| if (!call) throw new HostUnsupportedError("callTool", "chatgpt"); | ||
| return await call(name, args ?? {}); | ||
| }, | ||
| sendPrompt(text) { | ||
| const o = openai(); | ||
| const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage; | ||
| send?.({ prompt: text }); | ||
| }, | ||
| openLink(url) { | ||
| const open = openai()?.openExternal; | ||
| if (open) open({ href: url }); | ||
| else win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| const o = openai(); | ||
| return { | ||
| pull: typeof o?.callTool === "function", | ||
| sendPrompt: typeof o?.sendFollowUpMessage === "function" || typeof o?.sendFollowupMessage === "function", | ||
| openLink: true | ||
| }; | ||
| }, | ||
| start() { | ||
| win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals, { passive: true }); | ||
| currentData = openai()?.toolOutput ?? currentData; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/data.ts | ||
| function readInlineData(doc, elementId) { | ||
| if (!doc) return null; | ||
| const el = doc.getElementById(elementId); | ||
| const text = el?.textContent; | ||
| if (!text) return null; | ||
| try { | ||
| const parsed = JSON.parse(text); | ||
| return parsed ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function unwrapRenderData(payload) { | ||
| if (payload == null || typeof payload !== "object") return null; | ||
| const rec = payload; | ||
| const nested = rec.renderData; | ||
| const source = nested != null && typeof nested === "object" ? nested : rec; | ||
| if (source.toolOutput != null) return source.toolOutput; | ||
| if (source.structuredContent != null) return source.structuredContent; | ||
| return source; | ||
| } | ||
| // src/host/adapters/inline.ts | ||
| function createInlineAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let media = null; | ||
| const onSchemeChange = (event) => { | ||
| if (destroyed) return; | ||
| const mode = coerceMode(event.matches ? "dark" : "light", currentTheme.mode); | ||
| if (mode === currentTheme.mode) return; | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| }; | ||
| return { | ||
| host: "generic", | ||
| getData: () => currentData, | ||
| onData() { | ||
| return () => { | ||
| }; | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(_name) { | ||
| throw new HostUnsupportedError("callTool", "generic"); | ||
| }, | ||
| sendPrompt() { | ||
| }, | ||
| openLink(url) { | ||
| win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| return { pull: false, sendPrompt: false, openLink: true }; | ||
| }, | ||
| start() { | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| try { | ||
| media = win.matchMedia?.("(prefers-color-scheme: dark)") ?? null; | ||
| media?.addEventListener?.("change", onSchemeChange); | ||
| } catch { | ||
| media = null; | ||
| } | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| media?.removeEventListener?.("change", onSchemeChange); | ||
| media = null; | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/adapters/mcpapps.ts | ||
| var REQUEST_TIMEOUT_MS = 3e4; | ||
| function createMcpAppsAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| const autoResize = options.autoResize !== false; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let standardConfirmed = false; | ||
| let nextId = 1; | ||
| const pending = /* @__PURE__ */ new Map(); | ||
| let lastReportedHeight = -1; | ||
| let resizeObserver = null; | ||
| let onWindowResize = null; | ||
| const parent = () => win.parent ?? win; | ||
| function post(message) { | ||
| parent().postMessage(message, "*"); | ||
| } | ||
| function postLegacy(message) { | ||
| if (standardConfirmed) return; | ||
| post(message); | ||
| } | ||
| function notify(method, params) { | ||
| post({ jsonrpc: "2.0", method, params: params ?? {} }); | ||
| } | ||
| function request(method, params) { | ||
| const id = nextId++; | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| pending.delete(id); | ||
| reject(new Error(`"${method}" timed out`)); | ||
| }, REQUEST_TIMEOUT_MS); | ||
| pending.set(id, { resolve, reject, timer }); | ||
| post({ jsonrpc: "2.0", id, method, params: params ?? {} }); | ||
| }); | ||
| } | ||
| function emitData(next) { | ||
| if (next == null) return; | ||
| currentData = next; | ||
| for (const cb of dataCbs) cb(next); | ||
| } | ||
| function applyHostContext(ctx) { | ||
| if (!ctx || typeof ctx !== "object") return; | ||
| let { mode, tokens } = currentTheme; | ||
| let changed = false; | ||
| if (ctx.theme != null) { | ||
| const next = coerceMode(ctx.theme, mode); | ||
| if (next !== mode) { | ||
| mode = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| const styles = ctx.styles; | ||
| if (styles?.variables && typeof styles.variables === "object") { | ||
| tokens = { ...tokens, ...styles.variables }; | ||
| changed = true; | ||
| } | ||
| if (changed) { | ||
| currentTheme = { mode, tokens }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| function reportSize(height) { | ||
| if (destroyed) return; | ||
| const h = typeof height === "number" ? height : Math.ceil(win.document.body.scrollHeight); | ||
| if (h === lastReportedHeight) return; | ||
| lastReportedHeight = h; | ||
| notify(MCPAPP_SIZE_CHANGED, { height: h }); | ||
| postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } }); | ||
| } | ||
| function handleResponse(d) { | ||
| const id = Number(d.id); | ||
| const p = pending.get(id); | ||
| if (!p) return; | ||
| pending.delete(id); | ||
| clearTimeout(p.timer); | ||
| if (d.error != null) { | ||
| const err = d.error; | ||
| p.reject(new Error(err.message ?? "request failed")); | ||
| } else { | ||
| p.resolve(d.result); | ||
| } | ||
| } | ||
| function handleNotification(method, params) { | ||
| if (method === MCPAPP_TOOL_RESULT) { | ||
| const structured = params.structuredContent; | ||
| emitData(structured != null ? structured : unwrapRenderData(params)); | ||
| } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) { | ||
| applyHostContext(params); | ||
| } | ||
| } | ||
| function handleRequest(d) { | ||
| if (d.method === MCPAPP_TEARDOWN) { | ||
| post({ jsonrpc: "2.0", id: d.id, result: {} }); | ||
| } | ||
| } | ||
| function handleLegacy(d) { | ||
| if (d.type === MCPUI_RENDER_DATA || d.type === "renderData") { | ||
| const { theme, ...rest } = d.payload ?? {}; | ||
| if (theme != null) applyHostContext({ theme }); | ||
| if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest)); | ||
| } | ||
| } | ||
| const onMessage = (event) => { | ||
| if (destroyed) return; | ||
| if (event.source && event.source !== parent()) return; | ||
| const d = event.data; | ||
| if (!d || typeof d !== "object") return; | ||
| if (d.jsonrpc !== "2.0") { | ||
| handleLegacy(d); | ||
| return; | ||
| } | ||
| if (d.id != null && ("result" in d || "error" in d)) { | ||
| handleResponse(d); | ||
| } else if (typeof d.method === "string") { | ||
| if (d.id != null) handleRequest(d); | ||
| else handleNotification(d.method, d.params ?? {}); | ||
| } | ||
| }; | ||
| function setupResize() { | ||
| onWindowResize = () => reportSize(); | ||
| win.addEventListener("resize", onWindowResize); | ||
| if (typeof win.ResizeObserver !== "undefined") { | ||
| resizeObserver = new win.ResizeObserver(() => reportSize()); | ||
| resizeObserver.observe(win.document.body); | ||
| } | ||
| } | ||
| return { | ||
| host: "claude", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| return await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} }); | ||
| }, | ||
| sendPrompt(text) { | ||
| void request(MCPAPP_MESSAGE, { role: "user", content: [{ type: "text", text }] }).catch( | ||
| () => { | ||
| } | ||
| ); | ||
| postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } }); | ||
| }, | ||
| openLink(url) { | ||
| void request(MCPAPP_OPEN_LINK, { url }).catch(() => { | ||
| }); | ||
| postLegacy({ type: MCPUI_LINK, payload: { url } }); | ||
| }, | ||
| resize(height) { | ||
| reportSize(height); | ||
| }, | ||
| capabilities() { | ||
| return { pull: true, sendPrompt: true, openLink: true }; | ||
| }, | ||
| start() { | ||
| win.addEventListener("message", onMessage); | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| if (autoResize) setupResize(); | ||
| post({ type: MCPUI_READY }); | ||
| request(MCPAPP_INITIALIZE, { | ||
| appInfo: { name: options.name ?? "synapse-ui", version: options.version ?? "0.0.0" }, | ||
| appCapabilities: { availableDisplayModes: ["inline"] }, | ||
| protocolVersion: MCPAPP_PROTOCOL_VERSION | ||
| }).then((result) => { | ||
| if (destroyed) return; | ||
| standardConfirmed = true; | ||
| applyHostContext(result?.hostContext); | ||
| notify(MCPAPP_INITIALIZED, {}); | ||
| lastReportedHeight = -1; | ||
| reportSize(); | ||
| }).catch(() => { | ||
| }); | ||
| reportSize(); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener("message", onMessage); | ||
| if (onWindowResize) win.removeEventListener("resize", onWindowResize); | ||
| onWindowResize = null; | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| for (const p of pending.values()) { | ||
| clearTimeout(p.timer); | ||
| p.reject(new Error("adapter destroyed")); | ||
| } | ||
| pending.clear(); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/detect.ts | ||
| function detectHostKind(win) { | ||
| if (win.openai != null) return "chatgpt"; | ||
| try { | ||
| if (win.parent != null && win.parent !== win) return "claude"; | ||
| } catch { | ||
| return "claude"; | ||
| } | ||
| return "generic"; | ||
| } | ||
| function adapterForKind(kind, win, options) { | ||
| switch (kind) { | ||
| case "chatgpt": | ||
| return createChatGPTAdapter(win); | ||
| case "claude": | ||
| case "nimblebrain": | ||
| return createMcpAppsAdapter(win, options); | ||
| default: | ||
| return createInlineAdapter(win, options); | ||
| } | ||
| } | ||
| function selectAdapter(win, options) { | ||
| const kind = options.host ?? detectHostKind(win); | ||
| return adapterForKind(kind, win, options); | ||
| } | ||
| // src/host/connect.ts | ||
| function connectUI(options = {}) { | ||
| const win = options.window ?? globalThis; | ||
| const adapter = selectAdapter(win, options); | ||
| applyHostTheme(adapter.getTheme()); | ||
| const unsubTheme = adapter.onTheme(applyHostTheme); | ||
| adapter.start(); | ||
| let destroyed = false; | ||
| return { | ||
| data: () => adapter.getData(), | ||
| onData: (cb) => adapter.onData(cb), | ||
| theme: () => adapter.getTheme(), | ||
| onTheme: (cb) => adapter.onTheme(cb), | ||
| callTool: (name, args) => adapter.callTool(name, args), | ||
| sendPrompt: (text) => adapter.sendPrompt(text), | ||
| openLink: (url) => adapter.openLink(url), | ||
| resize: (height) => adapter.resize(height), | ||
| capabilities: () => adapter.capabilities(), | ||
| host: () => adapter.host, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| unsubTheme(); | ||
| adapter.destroy(); | ||
| } | ||
| }; | ||
| } | ||
| exports.HostUnsupportedError = HostUnsupportedError; | ||
| exports.SYNAPSE_DATA_ELEMENT_ID = SYNAPSE_DATA_ELEMENT_ID; | ||
| exports.adapterForKind = adapterForKind; | ||
| exports.applyHostTheme = applyHostTheme; | ||
| exports.coerceMode = coerceMode; | ||
| exports.connectUI = connectUI; | ||
| exports.createChatGPTAdapter = createChatGPTAdapter; | ||
| exports.createInlineAdapter = createInlineAdapter; | ||
| exports.createMcpAppsAdapter = createMcpAppsAdapter; | ||
| exports.detectHostKind = detectHostKind; | ||
| exports.preferredMode = preferredMode; | ||
| exports.selectAdapter = selectAdapter; | ||
| //# sourceMappingURL=chunk-ANUZ7WFQ.cjs.map | ||
| //# sourceMappingURL=chunk-ANUZ7WFQ.cjs.map |
| {"version":3,"sources":["../src/host/theme.ts","../src/host/types.ts","../src/host/adapters/chatgpt.ts","../src/host/data.ts","../src/host/adapters/inline.ts","../src/host/adapters/mcpapps.ts","../src/host/detect.ts","../src/host/connect.ts"],"names":["applyThemeVariables"],"mappings":"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","file":"chunk-ANUZ7WFQ.cjs","sourcesContent":["import { applyThemeVariables } from \"../theme-defaults.js\";\nimport type { SynapseUITheme } from \"./types.js\";\n\n/**\n * Apply a resolved theme to the DOM for the cross-host client.\n *\n * Two conventions coexist across Synapse components, so the client drives both:\n *\n * - `document.documentElement[data-theme=\"light\"|\"dark\"]` — how self-contained\n * HTML components (Bassethound's report) gate their `--var` palettes, and the\n * lever a host's light/dark signal actually flips.\n * - CSS custom properties via {@link applyThemeVariables} — how the\n * `@nimblebrain/synapse/ui` token components consume theme, backed by the\n * SDK's neutral defaults so every referenced var resolves in both modes.\n *\n * Setting both means an app can use either convention (or a host that supplies\n * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe.\n */\nexport function applyHostTheme(theme: SynapseUITheme): void {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"data-theme\", theme.mode);\n }\n applyThemeVariables(theme.mode, theme.tokens);\n}\n\n/** Read the OS-level color scheme as a sane default for hosts that don't push a\n * theme until later (mcp-ui) or ever (standalone). */\nexport function preferredMode(win: Window | undefined): \"light\" | \"dark\" {\n try {\n if (win?.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) return \"dark\";\n } catch {\n // matchMedia unavailable (older test envs) — fall through to light.\n }\n return \"light\";\n}\n\n/** Coerce an arbitrary host-supplied theme signal to a mode. Accepts the string\n * form (`\"dark\"`) both hosts use; anything else falls back. */\nexport function coerceMode(value: unknown, fallback: \"light\" | \"dark\"): \"light\" | \"dark\" {\n return value === \"light\" || value === \"dark\" ? value : fallback;\n}\n","/**\n * Cross-host UI client — types and wire constants.\n *\n * The cross-host client (`connectUI`) renders one Synapse-authored component in\n * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude\n * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch\n * a host protocol. This is a **push-first** surface: the tool output that spawned\n * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the\n * pull escape hatch, advertised per host via `capabilities()`.\n *\n * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the\n * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so\n * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas).\n */\n\n/**\n * The host the client resolved to, as reported by `synapse.host()`. An escape\n * hatch — apps should rarely branch on it; `capabilities()` is the supported way\n * to feature-detect. `\"nimblebrain\"` is reserved for the runtime adapter (P3).\n */\nexport type HostKind = \"chatgpt\" | \"claude\" | \"nimblebrain\" | \"generic\";\n\n/** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS\n * custom properties the host publishes — the MCP Apps adapter reads them from\n * `hostContext.styles.variables`; where a host sends none they stay empty and the\n * SDK's neutral defaults back them. */\nexport interface SynapseUITheme {\n mode: \"light\" | \"dark\";\n tokens: Record<string, string>;\n}\n\n/** What the active host actually supports. `data()`/`onData()`/`theme()`/\n * `resize()` work everywhere; these three vary. */\nexport interface HostCapabilities {\n /** `callTool()` can reach the server (widget→server fetch). */\n pull: boolean;\n /** `sendPrompt()` reaches the agent conversation. */\n sendPrompt: boolean;\n /** `openLink()` opens an external URL through the host. */\n openLink: boolean;\n}\n\n/** Thrown by `callTool()` when the active host offers no widget→server call. */\nexport class HostUnsupportedError extends Error {\n constructor(feature: string, host: HostKind) {\n super(`\"${feature}\" is not supported by the \"${host}\" host`);\n this.name = \"HostUnsupportedError\";\n }\n}\n\nexport interface ConnectUIOptions {\n /** App name — informational; forwarded to hosts that accept an appInfo. */\n name?: string;\n /** App semver — informational. */\n version?: string;\n /**\n * Force a host adapter instead of auto-detecting. Used by preview harnesses,\n * SSR, and tests; production apps omit it and let the SDK feature-detect.\n * `\"claude\"` → MCP Apps standard adapter, `\"chatgpt\"` → OpenAI Apps adapter,\n * `\"generic\"` → inline adapter.\n */\n host?: HostKind;\n /**\n * `id` of the `<script type=\"application/json\">` element carrying pushed data\n * baked into the HTML (the mcp-ui / SSR path). Defaults to\n * {@link SYNAPSE_DATA_ELEMENT_ID}.\n */\n dataElementId?: string;\n /**\n * Auto-report content height to the host on layout changes (mcp-ui only).\n * Defaults to `true`. Set `false` to size manually via `resize()`.\n */\n autoResize?: boolean;\n /** Window to bind to. Defaults to the global `window`. Injectable for tests. */\n window?: Window & typeof globalThis;\n}\n\n/**\n * The public cross-host client. Bound to `synapse` by convention.\n *\n * ```ts\n * const synapse = connectUI();\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (may be null → empty state)\n * ```\n */\nexport interface SynapseUIClient {\n /** The current pushed data, or `null` before anything has been delivered. */\n data<T = unknown>(): T | null;\n /** Subscribe to data updates (NOT replayed — read `data()` for the current\n * value). Returns an unsubscribe. */\n onData<T = unknown>(cb: (data: T) => void): () => void;\n\n /** The current resolved theme. */\n theme(): SynapseUITheme;\n /** Subscribe to theme changes. The client already applies the theme to the DOM\n * (`data-theme` attribute + CSS variables) before this fires. */\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n\n /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the\n * host advertises no pull (`capabilities().pull === false`). */\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n /** Send a follow-up message to the agent conversation. No-op where unsupported. */\n sendPrompt(text: string): void;\n /** Open an external URL through the host (falls back to `window.open`). */\n openLink(url: string): void;\n /** Report content height to the host. Omit `height` to measure `document.body`. */\n resize(height?: number): void;\n\n /** What the active host supports. */\n capabilities(): HostCapabilities;\n /** The resolved host — an escape hatch; prefer `capabilities()`. */\n host(): HostKind;\n /** Tear down listeners/observers. */\n destroy(): void;\n}\n\n/**\n * Internal adapter contract. One per host bridge; the client is a thin façade\n * over the selected adapter.\n */\nexport interface HostAdapter {\n readonly host: HostKind;\n getData<T = unknown>(): T | null;\n onData<T = unknown>(cb: (data: T) => void): () => void;\n getTheme(): SynapseUITheme;\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n sendPrompt(text: string): void;\n openLink(url: string): void;\n resize(height?: number): void;\n capabilities(): HostCapabilities;\n /** Begin listening / send the ready handshake / read baked-in data. Called once\n * by `connectUI` synchronously so `getData()` is populated on return. */\n start(): void;\n destroy(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Wire constants — the bridge message shapes each host speaks. Centralized so a\n// host protocol tweak is a one-line change, not a grep-and-pray.\n// ---------------------------------------------------------------------------\n\n/** Default `id` of the baked-in data `<script type=\"application/json\">`. */\nexport const SYNAPSE_DATA_ELEMENT_ID = \"synapse-ui-data\";\n\n/** mcp-ui iframe-lifecycle messages (child → host, host → child). */\nexport const MCPUI_READY = \"ui-lifecycle-iframe-ready\";\nexport const MCPUI_RENDER_DATA = \"ui-lifecycle-iframe-render-data\";\nexport const MCPUI_SIZE_CHANGE = \"ui-size-change\";\n/** mcp-ui action messages (child → host). */\nexport const MCPUI_LINK = \"link\";\nexport const MCPUI_PROMPT = \"prompt\";\n\n/** OpenAI Apps SDK globals-broadcast event (host → child). */\nexport const OPENAI_SET_GLOBALS = \"openai:set_globals\";\n\n// ---------------------------------------------------------------------------\n// MCP Apps standard (SEP-1865) — the convergence bridge, primary for Claude\n// Desktop and the NimbleBrain runtime. The View iframe is an MCP client and the\n// host an MCP server; they exchange raw JSON-RPC 2.0 objects over `postMessage`\n// (no wrapper envelope). Method names are canonical to the ext-apps spec.\n// ---------------------------------------------------------------------------\n\n/** Protocol version exchanged in the `ui/initialize` handshake. */\nexport const MCPAPP_PROTOCOL_VERSION = \"2026-01-26\";\n/** View → host handshake request; the result carries the host context. */\nexport const MCPAPP_INITIALIZE = \"ui/initialize\";\n/** View → host notification sent once after the init result — the host holds all\n * pushes until it arrives. */\nexport const MCPAPP_INITIALIZED = \"ui/notifications/initialized\";\n/** Host → view notification carrying the tool output. `params` IS the\n * `CallToolResult` (data at `params.structuredContent`) — no `result` wrapper. */\nexport const MCPAPP_TOOL_RESULT = \"ui/notifications/tool-result\";\n/** Host → view notification carrying a partial host context (theme, styles, …). */\nexport const MCPAPP_HOST_CONTEXT_CHANGED = \"ui/notifications/host-context-changed\";\n/** View → host notification reporting intrinsic size (`{ width?, height? }`). A\n * host may keep the frame hidden until it receives one, so it is sent promptly. */\nexport const MCPAPP_SIZE_CHANGED = \"ui/notifications/size-changed\";\n/** View → host request: open an external URL (`{ url }`). */\nexport const MCPAPP_OPEN_LINK = \"ui/open-link\";\n/** View → host request: send a follow-up to the conversation\n * (`{ role: \"user\", content: ContentBlock[] }`). */\nexport const MCPAPP_MESSAGE = \"ui/message\";\n/** Host → view request: graceful teardown; the view replies `{}`. */\nexport const MCPAPP_TEARDOWN = \"ui/resource-teardown\";\n/** Standard MCP method the View may call over the same channel (pull). */\nexport const MCP_TOOLS_CALL = \"tools/call\";\n","import { coerceMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n OPENAI_SET_GLOBALS,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * The subset of the OpenAI Apps SDK `window.openai` surface this adapter uses.\n * All members are optional — the SDK version a host ships may not carry every\n * one, so each call is guarded and degraded.\n */\ninterface OpenAiHost {\n toolOutput?: unknown;\n theme?: unknown;\n callTool?: (name: string, args?: Record<string, unknown>) => Promise<unknown>;\n sendFollowUpMessage?: (arg: { prompt: string }) => void;\n /** Older casing seen in the wild — tried as a fallback. */\n sendFollowupMessage?: (arg: { prompt: string }) => void;\n openExternal?: (arg: { href: string }) => void;\n}\n\ninterface SetGlobalsDetail {\n globals?: { toolOutput?: unknown; theme?: unknown };\n}\n\n/**\n * ChatGPT (OpenAI Apps SDK) adapter.\n *\n * Data arrives on `window.openai.toolOutput` (available synchronously before the\n * widget script runs) and updates via the `openai:set_globals` event. Theme is a\n * mode string on the same surface. The host auto-sizes the iframe, so `resize()`\n * is a no-op. `callTool` is available when the host exposes `window.openai.callTool`.\n */\nexport function createChatGPTAdapter(\n win: Window & typeof globalThis,\n _options: ConnectUIOptions,\n): HostAdapter {\n const openai = () => (win as unknown as { openai?: OpenAiHost }).openai;\n\n let currentData: unknown = openai()?.toolOutput ?? null;\n let currentTheme: SynapseUITheme = {\n mode: coerceMode(openai()?.theme, \"light\"),\n tokens: {},\n };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n\n const onSetGlobals = (event: Event) => {\n if (destroyed) return;\n const globals = (event as CustomEvent<SetGlobalsDetail>).detail?.globals;\n if (!globals) return;\n if (\"toolOutput\" in globals && globals.toolOutput != null) {\n currentData = globals.toolOutput;\n for (const cb of dataCbs) cb(currentData);\n }\n if (\"theme\" in globals && globals.theme != null) {\n const mode = coerceMode(globals.theme, currentTheme.mode);\n if (mode !== currentTheme.mode) {\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n };\n\n return {\n host: \"chatgpt\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n const call = openai()?.callTool;\n if (!call) throw new HostUnsupportedError(\"callTool\", \"chatgpt\");\n return (await call(name, args ?? {})) as O;\n },\n sendPrompt(text: string) {\n const o = openai();\n const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage;\n send?.({ prompt: text });\n },\n openLink(url: string) {\n const open = openai()?.openExternal;\n if (open) open({ href: url });\n else win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // ChatGPT auto-sizes the widget iframe — nothing to report.\n },\n capabilities(): HostCapabilities {\n const o = openai();\n return {\n pull: typeof o?.callTool === \"function\",\n sendPrompt:\n typeof o?.sendFollowUpMessage === \"function\" ||\n typeof o?.sendFollowupMessage === \"function\",\n openLink: true,\n };\n },\n start() {\n win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener, { passive: true });\n // Re-read in case the host mutated globals between construction and start.\n currentData = openai()?.toolOutput ?? currentData;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener);\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","/**\n * Read data baked into the HTML as `<script type=\"application/json\" id=…>`.\n *\n * This is how the mcp-ui embedded-resource path and SSR/standalone deliver the\n * pushed tool output with no round-trip: the server helper escapes the payload\n * and substitutes it into the element (see the Python `SynapseUI.render_html`).\n * A data-free template leaves the marker comment in place, which fails\n * `JSON.parse` and reads back as `null` — exactly the \"no data yet\" state.\n */\nexport function readInlineData<T = unknown>(\n doc: Document | undefined,\n elementId: string,\n): T | null {\n if (!doc) return null;\n const el = doc.getElementById(elementId);\n const text = el?.textContent;\n if (!text) return null;\n try {\n const parsed = JSON.parse(text);\n return (parsed ?? null) as T | null;\n } catch {\n // Unreplaced marker or malformed blob — treat as no data.\n return null;\n }\n}\n\n/**\n * Pull the app payload out of an arbitrary render-data envelope. Hosts wrap the\n * tool output differently (`renderData`, `toolOutput`, `structuredContent`, or\n * the bare object), so unwrap the known envelope keys, else pass the object\n * through. Kept framework-generic — no app-specific keys.\n */\nexport function unwrapRenderData<T = unknown>(payload: unknown): T | null {\n if (payload == null || typeof payload !== \"object\") return null;\n const rec = payload as Record<string, unknown>;\n const nested = rec.renderData;\n const source =\n nested != null && typeof nested === \"object\" ? (nested as Record<string, unknown>) : rec;\n if (source.toolOutput != null) return source.toolOutput as T;\n if (source.structuredContent != null) return source.structuredContent as T;\n // No bare `data` envelope key — too ambiguous with an app's own `data` field.\n return source as T;\n}\n","import { readInlineData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * Inline / standalone fallback adapter.\n *\n * No live host bridge — used for SSR, previews, and a plain browser render. Data\n * comes only from the baked-in `<script type=\"application/json\">` blob; there is\n * nothing to push, so `onData` never fires after `start()`. Theme follows the OS\n * color scheme and tracks `prefers-color-scheme` changes. `openLink` opens a new\n * tab; `sendPrompt`/`callTool` have no destination.\n */\nexport function createInlineAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n let media: MediaQueryList | null = null;\n\n const onSchemeChange = (event: MediaQueryListEvent) => {\n if (destroyed) return;\n const mode = coerceMode(event.matches ? \"dark\" : \"light\", currentTheme.mode);\n if (mode === currentTheme.mode) return;\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n };\n\n return {\n host: \"generic\",\n getData: <T>() => currentData as T | null,\n onData() {\n // Static render — no updates after the initial baked-in read.\n return () => {};\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(_name: string): Promise<O> {\n throw new HostUnsupportedError(\"callTool\", \"generic\");\n },\n sendPrompt() {\n // No agent to reach in a standalone render.\n },\n openLink(url: string) {\n win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // No host to size for.\n },\n capabilities(): HostCapabilities {\n return { pull: false, sendPrompt: false, openLink: true };\n },\n start() {\n currentData = readInlineData(win.document, dataElementId);\n try {\n media = win.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n media?.addEventListener?.(\"change\", onSchemeChange);\n } catch {\n media = null;\n }\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n media?.removeEventListener?.(\"change\", onSchemeChange);\n media = null;\n themeCbs.clear();\n },\n };\n}\n","import { readInlineData, unwrapRenderData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n MCP_TOOLS_CALL,\n MCPAPP_HOST_CONTEXT_CHANGED,\n MCPAPP_INITIALIZE,\n MCPAPP_INITIALIZED,\n MCPAPP_MESSAGE,\n MCPAPP_OPEN_LINK,\n MCPAPP_PROTOCOL_VERSION,\n MCPAPP_SIZE_CHANGED,\n MCPAPP_TEARDOWN,\n MCPAPP_TOOL_RESULT,\n MCPUI_LINK,\n MCPUI_PROMPT,\n MCPUI_READY,\n MCPUI_RENDER_DATA,\n MCPUI_SIZE_CHANGE,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/** How long a widget→host request (`callTool`, `openLink`, `sendPrompt`) waits\n * for its response before rejecting, so a silent host never hangs a promise. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * MCP Apps standard (SEP-1865) adapter — the convergence bridge for Claude\n * Desktop and other MCP Apps hosts.\n *\n * The View iframe is an MCP client speaking JSON-RPC 2.0 to `window.parent`:\n *\n * 1. posts `ui/initialize` and awaits the host context (theme, style variables);\n * 2. posts `ui/notifications/initialized`, then a `size-changed` — a host keeps\n * the frame hidden until it has both the handshake and a size;\n * 3. receives data via `ui/notifications/tool-result` (`params` IS the\n * `CallToolResult`, so data is at `params.structuredContent`) and theme via\n * `ui/notifications/host-context-changed`.\n *\n * Actions go up as requests: `ui/open-link`, `ui/message` (follow-up), and\n * `tools/call` (pull). The legacy mcp-ui `ui-lifecycle-*` messages are sent and\n * accepted alongside so a pre-standard host still renders — a standard host drops\n * the non-JSON-RPC frames, and a legacy host ignores the JSON-RPC ones.\n */\nexport function createMcpAppsAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n const autoResize = options.autoResize !== false;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n // Set once `ui/initialize` resolves — proof we're on an MCP Apps standard host,\n // which lets us stop mirroring actions to the legacy shim.\n let standardConfirmed = false;\n let nextId = 1;\n const pending = new Map<number, PendingRequest>();\n let lastReportedHeight = -1;\n let resizeObserver: ResizeObserver | null = null;\n let onWindowResize: (() => void) | null = null;\n\n const parent = () => win.parent ?? win;\n\n function post(message: Record<string, unknown>): void {\n parent().postMessage(message, \"*\");\n }\n\n // The legacy mcp-ui frames (render-data in, size/link/prompt out) are a\n // transitional shim for the one host that still speaks it — the NimbleBrain\n // runtime, which shares this adapter via the `nimblebrain` kind until the P3\n // `nimblebrain` adapter lands and this shim is removed. Once the handshake\n // confirms a standard host we stop mirroring, so a host that understood both\n // dialects never acts on an action twice.\n function postLegacy(message: Record<string, unknown>): void {\n if (standardConfirmed) return;\n post(message);\n }\n\n function notify(method: string, params?: Record<string, unknown>): void {\n post({ jsonrpc: \"2.0\", method, params: params ?? {} });\n }\n\n function request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`\"${method}\" timed out`));\n }, REQUEST_TIMEOUT_MS);\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });\n post({ jsonrpc: \"2.0\", id, method, params: params ?? {} });\n });\n }\n\n function emitData(next: unknown): void {\n if (next == null) return;\n currentData = next;\n for (const cb of dataCbs) cb(next);\n }\n\n /** Merge a full or partial host context into the resolved theme (mode + tokens). */\n function applyHostContext(ctx: Record<string, unknown> | null | undefined): void {\n if (!ctx || typeof ctx !== \"object\") return;\n let { mode, tokens } = currentTheme;\n let changed = false;\n if (ctx.theme != null) {\n const next = coerceMode(ctx.theme, mode);\n if (next !== mode) {\n mode = next;\n changed = true;\n }\n }\n const styles = ctx.styles as { variables?: Record<string, string> } | undefined;\n if (styles?.variables && typeof styles.variables === \"object\") {\n tokens = { ...tokens, ...styles.variables };\n changed = true;\n }\n if (changed) {\n currentTheme = { mode, tokens };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n\n function reportSize(height?: number): void {\n if (destroyed) return;\n const h = typeof height === \"number\" ? height : Math.ceil(win.document.body.scrollHeight);\n if (h === lastReportedHeight) return;\n lastReportedHeight = h;\n notify(MCPAPP_SIZE_CHANGED, { height: h });\n postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } });\n }\n\n function handleResponse(d: Record<string, unknown>): void {\n // Normalize the echoed id: JSON-RPC requires a same-type echo, but a lax host\n // that returned \"1\" for 1 would otherwise miss the numeric-keyed pending map.\n const id = Number(d.id);\n const p = pending.get(id);\n if (!p) return;\n pending.delete(id);\n clearTimeout(p.timer);\n if (d.error != null) {\n const err = d.error as { message?: string };\n p.reject(new Error(err.message ?? \"request failed\"));\n } else {\n p.resolve(d.result);\n }\n }\n\n function handleNotification(method: string, params: Record<string, unknown>): void {\n if (method === MCPAPP_TOOL_RESULT) {\n // `params` is the CallToolResult: the render data lives at structuredContent.\n const structured = params.structuredContent;\n emitData(structured != null ? structured : unwrapRenderData(params));\n } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) {\n applyHostContext(params);\n }\n }\n\n function handleRequest(d: Record<string, unknown>): void {\n // The one host→view request we honor: acknowledge teardown so the host can\n // dispose the frame cleanly.\n if (d.method === MCPAPP_TEARDOWN) {\n post({ jsonrpc: \"2.0\", id: d.id, result: {} });\n }\n }\n\n /** Legacy mcp-ui render-data (non-JSON-RPC) — a pre-standard host's data path. */\n function handleLegacy(d: Record<string, unknown>): void {\n if (d.type === MCPUI_RENDER_DATA || d.type === \"renderData\") {\n const { theme, ...rest } = (d.payload ?? {}) as Record<string, unknown>;\n if (theme != null) applyHostContext({ theme });\n if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest));\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n if (destroyed) return;\n // Accept only frames from the host window when the browser sets a source.\n if (event.source && event.source !== parent()) return;\n const d = event.data as Record<string, unknown> | null | undefined;\n if (!d || typeof d !== \"object\") return;\n if (d.jsonrpc !== \"2.0\") {\n handleLegacy(d);\n return;\n }\n if (d.id != null && (\"result\" in d || \"error\" in d)) {\n handleResponse(d);\n } else if (typeof d.method === \"string\") {\n if (d.id != null) handleRequest(d);\n else handleNotification(d.method, (d.params ?? {}) as Record<string, unknown>);\n }\n };\n\n function setupResize(): void {\n onWindowResize = () => reportSize();\n win.addEventListener(\"resize\", onWindowResize);\n if (typeof win.ResizeObserver !== \"undefined\") {\n resizeObserver = new win.ResizeObserver(() => reportSize());\n resizeObserver.observe(win.document.body);\n }\n }\n\n return {\n host: \"claude\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n return (await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} })) as O;\n },\n sendPrompt(text: string) {\n // Standard follow-up (ack ignored); legacy mirror for a pre-standard host.\n void request(MCPAPP_MESSAGE, { role: \"user\", content: [{ type: \"text\", text }] }).catch(\n () => {},\n );\n postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } });\n },\n openLink(url: string) {\n void request(MCPAPP_OPEN_LINK, { url }).catch(() => {});\n postLegacy({ type: MCPUI_LINK, payload: { url } });\n },\n resize(height?: number) {\n reportSize(height);\n },\n capabilities(): HostCapabilities {\n // The MCP Apps standard host answers `tools/call` over this bridge, so pull\n // is advertised. A legacy-only host that shares this adapter (nimblebrain,\n // pre-P3) does not, so there callTool rejects only after REQUEST_TIMEOUT_MS\n // rather than failing fast.\n return { pull: true, sendPrompt: true, openLink: true };\n },\n start() {\n win.addEventListener(\"message\", onMessage as EventListener);\n\n // Baked-in data → available on first paint, before the handshake resolves.\n currentData = readInlineData(win.document, dataElementId);\n\n if (autoResize) setupResize();\n\n // Legacy mcp-ui ready (a standard host drops this non-JSON-RPC frame).\n post({ type: MCPUI_READY });\n\n // MCP Apps standard handshake. A legacy-only host never answers, so the\n // legacy render-data path (handleLegacy) still feeds the widget.\n request<{ hostContext?: Record<string, unknown> }>(MCPAPP_INITIALIZE, {\n appInfo: { name: options.name ?? \"synapse-ui\", version: options.version ?? \"0.0.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\"] },\n protocolVersion: MCPAPP_PROTOCOL_VERSION,\n })\n .then((result) => {\n if (destroyed) return;\n standardConfirmed = true;\n applyHostContext(result?.hostContext);\n notify(MCPAPP_INITIALIZED, {});\n // A host keeps the frame hidden until it gets a size after init — force one.\n lastReportedHeight = -1;\n reportSize();\n })\n .catch(() => {\n // Not an MCP Apps host (or it was slow) — the legacy path covers data.\n });\n\n reportSize();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(\"message\", onMessage as EventListener);\n if (onWindowResize) win.removeEventListener(\"resize\", onWindowResize);\n onWindowResize = null;\n resizeObserver?.disconnect();\n resizeObserver = null;\n for (const p of pending.values()) {\n clearTimeout(p.timer);\n p.reject(new Error(\"adapter destroyed\"));\n }\n pending.clear();\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","import { createChatGPTAdapter } from \"./adapters/chatgpt.js\";\nimport { createInlineAdapter } from \"./adapters/inline.js\";\nimport { createMcpAppsAdapter } from \"./adapters/mcpapps.js\";\nimport type { ConnectUIOptions, HostAdapter, HostKind } from \"./types.js\";\n\n/**\n * Minimal window shape the detector inspects. Kept structural so detection is\n * unit-testable with a plain object — no full `Window` fake required.\n */\ninterface DetectableWindow {\n openai?: unknown;\n parent?: unknown;\n self?: unknown;\n}\n\n/**\n * Feature-detect the host from the browsing context.\n *\n * - `window.openai` present → **chatgpt** (OpenAI Apps SDK).\n * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin\n * access that throws) → **claude** (MCP Apps standard postMessage host).\n * - top-level document → **generic** (inline / standalone).\n *\n * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and\n * honors an explicit `options.host` override for previews/SSR/tests.\n */\nexport function detectHostKind(win: DetectableWindow): HostKind {\n if (win.openai != null) return \"chatgpt\";\n try {\n if (win.parent != null && win.parent !== win) return \"claude\";\n } catch {\n // Cross-origin parent access throws → we are framed by another origin.\n return \"claude\";\n }\n return \"generic\";\n}\n\n/** Build the adapter for an explicit host kind (`options.host` or a detected one). */\nexport function adapterForKind(\n kind: HostKind,\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n switch (kind) {\n case \"chatgpt\":\n return createChatGPTAdapter(win, options);\n case \"claude\":\n case \"nimblebrain\":\n // Both speak the MCP Apps standard (SEP-1865). `nimblebrain` shares it for\n // now; a dedicated adapter over the `synapse/*` extension lands in P3.\n return createMcpAppsAdapter(win, options);\n default:\n return createInlineAdapter(win, options);\n }\n}\n\n/** Select and construct the host adapter, honoring `options.host` when set. */\nexport function selectAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const kind = options.host ?? detectHostKind(win as unknown as DetectableWindow);\n return adapterForKind(kind, win, options);\n}\n","import { selectAdapter } from \"./detect.js\";\nimport { applyHostTheme } from \"./theme.js\";\nimport type { ConnectUIOptions, SynapseUIClient } from \"./types.js\";\n\n/**\n * Connect a Synapse-authored component to whatever host it renders in — ChatGPT\n * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one\n * push-first API. Feature-detects the host, selects an adapter, and applies the\n * host theme to the DOM before returning.\n *\n * Synchronous: `data()` is populated from baked-in data (where present) on\n * return, and pushed updates arrive via `onData`. Bind the result to `synapse`:\n *\n * ```ts\n * const synapse = connectUI({ name: \"my-widget\", version: \"1.0.0\" });\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (null → empty state)\n * ```\n */\nexport function connectUI(options: ConnectUIOptions = {}): SynapseUIClient {\n const win = options.window ?? (globalThis as unknown as Window & typeof globalThis);\n const adapter = selectAdapter(win, options);\n\n // The client owns theme application: adapters resolve mode/tokens; this puts\n // them on the DOM (data-theme + CSS vars) so apps never wire theme by hand.\n applyHostTheme(adapter.getTheme());\n const unsubTheme = adapter.onTheme(applyHostTheme);\n\n adapter.start();\n\n let destroyed = false;\n\n return {\n data: <T>() => adapter.getData<T>(),\n onData: <T>(cb: (data: T) => void) => adapter.onData<T>(cb),\n theme: () => adapter.getTheme(),\n onTheme: (cb) => adapter.onTheme(cb),\n callTool: <O>(name: string, args?: Record<string, unknown>) => adapter.callTool<O>(name, args),\n sendPrompt: (text: string) => adapter.sendPrompt(text),\n openLink: (url: string) => adapter.openLink(url),\n resize: (height?: number) => adapter.resize(height),\n capabilities: () => adapter.capabilities(),\n host: () => adapter.host,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubTheme();\n adapter.destroy();\n },\n };\n}\n"]} |
| import { applyThemeVariables } from './chunk-VXTMI266.js'; | ||
| // src/host/theme.ts | ||
| function applyHostTheme(theme) { | ||
| if (typeof document !== "undefined") { | ||
| document.documentElement.setAttribute("data-theme", theme.mode); | ||
| } | ||
| applyThemeVariables(theme.mode, theme.tokens); | ||
| } | ||
| function preferredMode(win) { | ||
| try { | ||
| if (win?.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark"; | ||
| } catch { | ||
| } | ||
| return "light"; | ||
| } | ||
| function coerceMode(value, fallback) { | ||
| return value === "light" || value === "dark" ? value : fallback; | ||
| } | ||
| // src/host/types.ts | ||
| var HostUnsupportedError = class extends Error { | ||
| constructor(feature, host) { | ||
| super(`"${feature}" is not supported by the "${host}" host`); | ||
| this.name = "HostUnsupportedError"; | ||
| } | ||
| }; | ||
| var SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| var MCPUI_READY = "ui-lifecycle-iframe-ready"; | ||
| var MCPUI_RENDER_DATA = "ui-lifecycle-iframe-render-data"; | ||
| var MCPUI_SIZE_CHANGE = "ui-size-change"; | ||
| var MCPUI_LINK = "link"; | ||
| var MCPUI_PROMPT = "prompt"; | ||
| var OPENAI_SET_GLOBALS = "openai:set_globals"; | ||
| var MCPAPP_PROTOCOL_VERSION = "2026-01-26"; | ||
| var MCPAPP_INITIALIZE = "ui/initialize"; | ||
| var MCPAPP_INITIALIZED = "ui/notifications/initialized"; | ||
| var MCPAPP_TOOL_RESULT = "ui/notifications/tool-result"; | ||
| var MCPAPP_HOST_CONTEXT_CHANGED = "ui/notifications/host-context-changed"; | ||
| var MCPAPP_SIZE_CHANGED = "ui/notifications/size-changed"; | ||
| var MCPAPP_OPEN_LINK = "ui/open-link"; | ||
| var MCPAPP_MESSAGE = "ui/message"; | ||
| var MCPAPP_TEARDOWN = "ui/resource-teardown"; | ||
| var MCP_TOOLS_CALL = "tools/call"; | ||
| // src/host/adapters/chatgpt.ts | ||
| function createChatGPTAdapter(win, _options) { | ||
| const openai = () => win.openai; | ||
| let currentData = openai()?.toolOutput ?? null; | ||
| let currentTheme = { | ||
| mode: coerceMode(openai()?.theme, "light"), | ||
| tokens: {} | ||
| }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| const onSetGlobals = (event) => { | ||
| if (destroyed) return; | ||
| const globals = event.detail?.globals; | ||
| if (!globals) return; | ||
| if ("toolOutput" in globals && globals.toolOutput != null) { | ||
| currentData = globals.toolOutput; | ||
| for (const cb of dataCbs) cb(currentData); | ||
| } | ||
| if ("theme" in globals && globals.theme != null) { | ||
| const mode = coerceMode(globals.theme, currentTheme.mode); | ||
| if (mode !== currentTheme.mode) { | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| host: "chatgpt", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| const call = openai()?.callTool; | ||
| if (!call) throw new HostUnsupportedError("callTool", "chatgpt"); | ||
| return await call(name, args ?? {}); | ||
| }, | ||
| sendPrompt(text) { | ||
| const o = openai(); | ||
| const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage; | ||
| send?.({ prompt: text }); | ||
| }, | ||
| openLink(url) { | ||
| const open = openai()?.openExternal; | ||
| if (open) open({ href: url }); | ||
| else win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| const o = openai(); | ||
| return { | ||
| pull: typeof o?.callTool === "function", | ||
| sendPrompt: typeof o?.sendFollowUpMessage === "function" || typeof o?.sendFollowupMessage === "function", | ||
| openLink: true | ||
| }; | ||
| }, | ||
| start() { | ||
| win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals, { passive: true }); | ||
| currentData = openai()?.toolOutput ?? currentData; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/data.ts | ||
| function readInlineData(doc, elementId) { | ||
| if (!doc) return null; | ||
| const el = doc.getElementById(elementId); | ||
| const text = el?.textContent; | ||
| if (!text) return null; | ||
| try { | ||
| const parsed = JSON.parse(text); | ||
| return parsed ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function unwrapRenderData(payload) { | ||
| if (payload == null || typeof payload !== "object") return null; | ||
| const rec = payload; | ||
| const nested = rec.renderData; | ||
| const source = nested != null && typeof nested === "object" ? nested : rec; | ||
| if (source.toolOutput != null) return source.toolOutput; | ||
| if (source.structuredContent != null) return source.structuredContent; | ||
| return source; | ||
| } | ||
| // src/host/adapters/inline.ts | ||
| function createInlineAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let media = null; | ||
| const onSchemeChange = (event) => { | ||
| if (destroyed) return; | ||
| const mode = coerceMode(event.matches ? "dark" : "light", currentTheme.mode); | ||
| if (mode === currentTheme.mode) return; | ||
| currentTheme = { mode, tokens: {} }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| }; | ||
| return { | ||
| host: "generic", | ||
| getData: () => currentData, | ||
| onData() { | ||
| return () => { | ||
| }; | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(_name) { | ||
| throw new HostUnsupportedError("callTool", "generic"); | ||
| }, | ||
| sendPrompt() { | ||
| }, | ||
| openLink(url) { | ||
| win.open(url, "_blank", "noopener,noreferrer"); | ||
| }, | ||
| resize() { | ||
| }, | ||
| capabilities() { | ||
| return { pull: false, sendPrompt: false, openLink: true }; | ||
| }, | ||
| start() { | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| try { | ||
| media = win.matchMedia?.("(prefers-color-scheme: dark)") ?? null; | ||
| media?.addEventListener?.("change", onSchemeChange); | ||
| } catch { | ||
| media = null; | ||
| } | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| media?.removeEventListener?.("change", onSchemeChange); | ||
| media = null; | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/adapters/mcpapps.ts | ||
| var REQUEST_TIMEOUT_MS = 3e4; | ||
| function createMcpAppsAdapter(win, options) { | ||
| const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID; | ||
| const autoResize = options.autoResize !== false; | ||
| let currentData = null; | ||
| let currentTheme = { mode: preferredMode(win), tokens: {} }; | ||
| const dataCbs = /* @__PURE__ */ new Set(); | ||
| const themeCbs = /* @__PURE__ */ new Set(); | ||
| let destroyed = false; | ||
| let standardConfirmed = false; | ||
| let nextId = 1; | ||
| const pending = /* @__PURE__ */ new Map(); | ||
| let lastReportedHeight = -1; | ||
| let resizeObserver = null; | ||
| let onWindowResize = null; | ||
| const parent = () => win.parent ?? win; | ||
| function post(message) { | ||
| parent().postMessage(message, "*"); | ||
| } | ||
| function postLegacy(message) { | ||
| if (standardConfirmed) return; | ||
| post(message); | ||
| } | ||
| function notify(method, params) { | ||
| post({ jsonrpc: "2.0", method, params: params ?? {} }); | ||
| } | ||
| function request(method, params) { | ||
| const id = nextId++; | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => { | ||
| pending.delete(id); | ||
| reject(new Error(`"${method}" timed out`)); | ||
| }, REQUEST_TIMEOUT_MS); | ||
| pending.set(id, { resolve, reject, timer }); | ||
| post({ jsonrpc: "2.0", id, method, params: params ?? {} }); | ||
| }); | ||
| } | ||
| function emitData(next) { | ||
| if (next == null) return; | ||
| currentData = next; | ||
| for (const cb of dataCbs) cb(next); | ||
| } | ||
| function applyHostContext(ctx) { | ||
| if (!ctx || typeof ctx !== "object") return; | ||
| let { mode, tokens } = currentTheme; | ||
| let changed = false; | ||
| if (ctx.theme != null) { | ||
| const next = coerceMode(ctx.theme, mode); | ||
| if (next !== mode) { | ||
| mode = next; | ||
| changed = true; | ||
| } | ||
| } | ||
| const styles = ctx.styles; | ||
| if (styles?.variables && typeof styles.variables === "object") { | ||
| tokens = { ...tokens, ...styles.variables }; | ||
| changed = true; | ||
| } | ||
| if (changed) { | ||
| currentTheme = { mode, tokens }; | ||
| for (const cb of themeCbs) cb(currentTheme); | ||
| } | ||
| } | ||
| function reportSize(height) { | ||
| if (destroyed) return; | ||
| const h = typeof height === "number" ? height : Math.ceil(win.document.body.scrollHeight); | ||
| if (h === lastReportedHeight) return; | ||
| lastReportedHeight = h; | ||
| notify(MCPAPP_SIZE_CHANGED, { height: h }); | ||
| postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } }); | ||
| } | ||
| function handleResponse(d) { | ||
| const id = Number(d.id); | ||
| const p = pending.get(id); | ||
| if (!p) return; | ||
| pending.delete(id); | ||
| clearTimeout(p.timer); | ||
| if (d.error != null) { | ||
| const err = d.error; | ||
| p.reject(new Error(err.message ?? "request failed")); | ||
| } else { | ||
| p.resolve(d.result); | ||
| } | ||
| } | ||
| function handleNotification(method, params) { | ||
| if (method === MCPAPP_TOOL_RESULT) { | ||
| const structured = params.structuredContent; | ||
| emitData(structured != null ? structured : unwrapRenderData(params)); | ||
| } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) { | ||
| applyHostContext(params); | ||
| } | ||
| } | ||
| function handleRequest(d) { | ||
| if (d.method === MCPAPP_TEARDOWN) { | ||
| post({ jsonrpc: "2.0", id: d.id, result: {} }); | ||
| } | ||
| } | ||
| function handleLegacy(d) { | ||
| if (d.type === MCPUI_RENDER_DATA || d.type === "renderData") { | ||
| const { theme, ...rest } = d.payload ?? {}; | ||
| if (theme != null) applyHostContext({ theme }); | ||
| if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest)); | ||
| } | ||
| } | ||
| const onMessage = (event) => { | ||
| if (destroyed) return; | ||
| if (event.source && event.source !== parent()) return; | ||
| const d = event.data; | ||
| if (!d || typeof d !== "object") return; | ||
| if (d.jsonrpc !== "2.0") { | ||
| handleLegacy(d); | ||
| return; | ||
| } | ||
| if (d.id != null && ("result" in d || "error" in d)) { | ||
| handleResponse(d); | ||
| } else if (typeof d.method === "string") { | ||
| if (d.id != null) handleRequest(d); | ||
| else handleNotification(d.method, d.params ?? {}); | ||
| } | ||
| }; | ||
| function setupResize() { | ||
| onWindowResize = () => reportSize(); | ||
| win.addEventListener("resize", onWindowResize); | ||
| if (typeof win.ResizeObserver !== "undefined") { | ||
| resizeObserver = new win.ResizeObserver(() => reportSize()); | ||
| resizeObserver.observe(win.document.body); | ||
| } | ||
| } | ||
| return { | ||
| host: "claude", | ||
| getData: () => currentData, | ||
| onData(cb) { | ||
| dataCbs.add(cb); | ||
| return () => dataCbs.delete(cb); | ||
| }, | ||
| getTheme: () => currentTheme, | ||
| onTheme(cb) { | ||
| themeCbs.add(cb); | ||
| return () => themeCbs.delete(cb); | ||
| }, | ||
| async callTool(name, args) { | ||
| return await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} }); | ||
| }, | ||
| sendPrompt(text) { | ||
| void request(MCPAPP_MESSAGE, { role: "user", content: [{ type: "text", text }] }).catch( | ||
| () => { | ||
| } | ||
| ); | ||
| postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } }); | ||
| }, | ||
| openLink(url) { | ||
| void request(MCPAPP_OPEN_LINK, { url }).catch(() => { | ||
| }); | ||
| postLegacy({ type: MCPUI_LINK, payload: { url } }); | ||
| }, | ||
| resize(height) { | ||
| reportSize(height); | ||
| }, | ||
| capabilities() { | ||
| return { pull: true, sendPrompt: true, openLink: true }; | ||
| }, | ||
| start() { | ||
| win.addEventListener("message", onMessage); | ||
| currentData = readInlineData(win.document, dataElementId); | ||
| if (autoResize) setupResize(); | ||
| post({ type: MCPUI_READY }); | ||
| request(MCPAPP_INITIALIZE, { | ||
| appInfo: { name: options.name ?? "synapse-ui", version: options.version ?? "0.0.0" }, | ||
| appCapabilities: { availableDisplayModes: ["inline"] }, | ||
| protocolVersion: MCPAPP_PROTOCOL_VERSION | ||
| }).then((result) => { | ||
| if (destroyed) return; | ||
| standardConfirmed = true; | ||
| applyHostContext(result?.hostContext); | ||
| notify(MCPAPP_INITIALIZED, {}); | ||
| lastReportedHeight = -1; | ||
| reportSize(); | ||
| }).catch(() => { | ||
| }); | ||
| reportSize(); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| win.removeEventListener("message", onMessage); | ||
| if (onWindowResize) win.removeEventListener("resize", onWindowResize); | ||
| onWindowResize = null; | ||
| resizeObserver?.disconnect(); | ||
| resizeObserver = null; | ||
| for (const p of pending.values()) { | ||
| clearTimeout(p.timer); | ||
| p.reject(new Error("adapter destroyed")); | ||
| } | ||
| pending.clear(); | ||
| dataCbs.clear(); | ||
| themeCbs.clear(); | ||
| } | ||
| }; | ||
| } | ||
| // src/host/detect.ts | ||
| function detectHostKind(win) { | ||
| if (win.openai != null) return "chatgpt"; | ||
| try { | ||
| if (win.parent != null && win.parent !== win) return "claude"; | ||
| } catch { | ||
| return "claude"; | ||
| } | ||
| return "generic"; | ||
| } | ||
| function adapterForKind(kind, win, options) { | ||
| switch (kind) { | ||
| case "chatgpt": | ||
| return createChatGPTAdapter(win); | ||
| case "claude": | ||
| case "nimblebrain": | ||
| return createMcpAppsAdapter(win, options); | ||
| default: | ||
| return createInlineAdapter(win, options); | ||
| } | ||
| } | ||
| function selectAdapter(win, options) { | ||
| const kind = options.host ?? detectHostKind(win); | ||
| return adapterForKind(kind, win, options); | ||
| } | ||
| // src/host/connect.ts | ||
| function connectUI(options = {}) { | ||
| const win = options.window ?? globalThis; | ||
| const adapter = selectAdapter(win, options); | ||
| applyHostTheme(adapter.getTheme()); | ||
| const unsubTheme = adapter.onTheme(applyHostTheme); | ||
| adapter.start(); | ||
| let destroyed = false; | ||
| return { | ||
| data: () => adapter.getData(), | ||
| onData: (cb) => adapter.onData(cb), | ||
| theme: () => adapter.getTheme(), | ||
| onTheme: (cb) => adapter.onTheme(cb), | ||
| callTool: (name, args) => adapter.callTool(name, args), | ||
| sendPrompt: (text) => adapter.sendPrompt(text), | ||
| openLink: (url) => adapter.openLink(url), | ||
| resize: (height) => adapter.resize(height), | ||
| capabilities: () => adapter.capabilities(), | ||
| host: () => adapter.host, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| unsubTheme(); | ||
| adapter.destroy(); | ||
| } | ||
| }; | ||
| } | ||
| export { HostUnsupportedError, SYNAPSE_DATA_ELEMENT_ID, adapterForKind, applyHostTheme, coerceMode, connectUI, createChatGPTAdapter, createInlineAdapter, createMcpAppsAdapter, detectHostKind, preferredMode, selectAdapter }; | ||
| //# sourceMappingURL=chunk-JVKQNMAP.js.map | ||
| //# sourceMappingURL=chunk-JVKQNMAP.js.map |
| {"version":3,"sources":["../src/host/theme.ts","../src/host/types.ts","../src/host/adapters/chatgpt.ts","../src/host/data.ts","../src/host/adapters/inline.ts","../src/host/adapters/mcpapps.ts","../src/host/detect.ts","../src/host/connect.ts"],"names":[],"mappings":"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","file":"chunk-JVKQNMAP.js","sourcesContent":["import { applyThemeVariables } from \"../theme-defaults.js\";\nimport type { SynapseUITheme } from \"./types.js\";\n\n/**\n * Apply a resolved theme to the DOM for the cross-host client.\n *\n * Two conventions coexist across Synapse components, so the client drives both:\n *\n * - `document.documentElement[data-theme=\"light\"|\"dark\"]` — how self-contained\n * HTML components (Bassethound's report) gate their `--var` palettes, and the\n * lever a host's light/dark signal actually flips.\n * - CSS custom properties via {@link applyThemeVariables} — how the\n * `@nimblebrain/synapse/ui` token components consume theme, backed by the\n * SDK's neutral defaults so every referenced var resolves in both modes.\n *\n * Setting both means an app can use either convention (or a host that supplies\n * only a mode string, like the OpenAI Apps SDK, still themes correctly). SSR-safe.\n */\nexport function applyHostTheme(theme: SynapseUITheme): void {\n if (typeof document !== \"undefined\") {\n document.documentElement.setAttribute(\"data-theme\", theme.mode);\n }\n applyThemeVariables(theme.mode, theme.tokens);\n}\n\n/** Read the OS-level color scheme as a sane default for hosts that don't push a\n * theme until later (mcp-ui) or ever (standalone). */\nexport function preferredMode(win: Window | undefined): \"light\" | \"dark\" {\n try {\n if (win?.matchMedia?.(\"(prefers-color-scheme: dark)\").matches) return \"dark\";\n } catch {\n // matchMedia unavailable (older test envs) — fall through to light.\n }\n return \"light\";\n}\n\n/** Coerce an arbitrary host-supplied theme signal to a mode. Accepts the string\n * form (`\"dark\"`) both hosts use; anything else falls back. */\nexport function coerceMode(value: unknown, fallback: \"light\" | \"dark\"): \"light\" | \"dark\" {\n return value === \"light\" || value === \"dark\" ? value : fallback;\n}\n","/**\n * Cross-host UI client — types and wire constants.\n *\n * The cross-host client (`connectUI`) renders one Synapse-authored component in\n * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude\n * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch\n * a host protocol. This is a **push-first** surface: the tool output that spawned\n * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the\n * pull escape hatch, advertised per host via `capabilities()`.\n *\n * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the\n * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so\n * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas).\n */\n\n/**\n * The host the client resolved to, as reported by `synapse.host()`. An escape\n * hatch — apps should rarely branch on it; `capabilities()` is the supported way\n * to feature-detect. `\"nimblebrain\"` is reserved for the runtime adapter (P3).\n */\nexport type HostKind = \"chatgpt\" | \"claude\" | \"nimblebrain\" | \"generic\";\n\n/** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS\n * custom properties the host publishes — the MCP Apps adapter reads them from\n * `hostContext.styles.variables`; where a host sends none they stay empty and the\n * SDK's neutral defaults back them. */\nexport interface SynapseUITheme {\n mode: \"light\" | \"dark\";\n tokens: Record<string, string>;\n}\n\n/** What the active host actually supports. `data()`/`onData()`/`theme()`/\n * `resize()` work everywhere; these three vary. */\nexport interface HostCapabilities {\n /** `callTool()` can reach the server (widget→server fetch). */\n pull: boolean;\n /** `sendPrompt()` reaches the agent conversation. */\n sendPrompt: boolean;\n /** `openLink()` opens an external URL through the host. */\n openLink: boolean;\n}\n\n/** Thrown by `callTool()` when the active host offers no widget→server call. */\nexport class HostUnsupportedError extends Error {\n constructor(feature: string, host: HostKind) {\n super(`\"${feature}\" is not supported by the \"${host}\" host`);\n this.name = \"HostUnsupportedError\";\n }\n}\n\nexport interface ConnectUIOptions {\n /** App name — informational; forwarded to hosts that accept an appInfo. */\n name?: string;\n /** App semver — informational. */\n version?: string;\n /**\n * Force a host adapter instead of auto-detecting. Used by preview harnesses,\n * SSR, and tests; production apps omit it and let the SDK feature-detect.\n * `\"claude\"` → MCP Apps standard adapter, `\"chatgpt\"` → OpenAI Apps adapter,\n * `\"generic\"` → inline adapter.\n */\n host?: HostKind;\n /**\n * `id` of the `<script type=\"application/json\">` element carrying pushed data\n * baked into the HTML (the mcp-ui / SSR path). Defaults to\n * {@link SYNAPSE_DATA_ELEMENT_ID}.\n */\n dataElementId?: string;\n /**\n * Auto-report content height to the host on layout changes (mcp-ui only).\n * Defaults to `true`. Set `false` to size manually via `resize()`.\n */\n autoResize?: boolean;\n /** Window to bind to. Defaults to the global `window`. Injectable for tests. */\n window?: Window & typeof globalThis;\n}\n\n/**\n * The public cross-host client. Bound to `synapse` by convention.\n *\n * ```ts\n * const synapse = connectUI();\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (may be null → empty state)\n * ```\n */\nexport interface SynapseUIClient {\n /** The current pushed data, or `null` before anything has been delivered. */\n data<T = unknown>(): T | null;\n /** Subscribe to data updates (NOT replayed — read `data()` for the current\n * value). Returns an unsubscribe. */\n onData<T = unknown>(cb: (data: T) => void): () => void;\n\n /** The current resolved theme. */\n theme(): SynapseUITheme;\n /** Subscribe to theme changes. The client already applies the theme to the DOM\n * (`data-theme` attribute + CSS variables) before this fires. */\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n\n /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the\n * host advertises no pull (`capabilities().pull === false`). */\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n /** Send a follow-up message to the agent conversation. No-op where unsupported. */\n sendPrompt(text: string): void;\n /** Open an external URL through the host (falls back to `window.open`). */\n openLink(url: string): void;\n /** Report content height to the host. Omit `height` to measure `document.body`. */\n resize(height?: number): void;\n\n /** What the active host supports. */\n capabilities(): HostCapabilities;\n /** The resolved host — an escape hatch; prefer `capabilities()`. */\n host(): HostKind;\n /** Tear down listeners/observers. */\n destroy(): void;\n}\n\n/**\n * Internal adapter contract. One per host bridge; the client is a thin façade\n * over the selected adapter.\n */\nexport interface HostAdapter {\n readonly host: HostKind;\n getData<T = unknown>(): T | null;\n onData<T = unknown>(cb: (data: T) => void): () => void;\n getTheme(): SynapseUITheme;\n onTheme(cb: (theme: SynapseUITheme) => void): () => void;\n callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>;\n sendPrompt(text: string): void;\n openLink(url: string): void;\n resize(height?: number): void;\n capabilities(): HostCapabilities;\n /** Begin listening / send the ready handshake / read baked-in data. Called once\n * by `connectUI` synchronously so `getData()` is populated on return. */\n start(): void;\n destroy(): void;\n}\n\n// ---------------------------------------------------------------------------\n// Wire constants — the bridge message shapes each host speaks. Centralized so a\n// host protocol tweak is a one-line change, not a grep-and-pray.\n// ---------------------------------------------------------------------------\n\n/** Default `id` of the baked-in data `<script type=\"application/json\">`. */\nexport const SYNAPSE_DATA_ELEMENT_ID = \"synapse-ui-data\";\n\n/** mcp-ui iframe-lifecycle messages (child → host, host → child). */\nexport const MCPUI_READY = \"ui-lifecycle-iframe-ready\";\nexport const MCPUI_RENDER_DATA = \"ui-lifecycle-iframe-render-data\";\nexport const MCPUI_SIZE_CHANGE = \"ui-size-change\";\n/** mcp-ui action messages (child → host). */\nexport const MCPUI_LINK = \"link\";\nexport const MCPUI_PROMPT = \"prompt\";\n\n/** OpenAI Apps SDK globals-broadcast event (host → child). */\nexport const OPENAI_SET_GLOBALS = \"openai:set_globals\";\n\n// ---------------------------------------------------------------------------\n// MCP Apps standard (SEP-1865) — the convergence bridge, primary for Claude\n// Desktop and the NimbleBrain runtime. The View iframe is an MCP client and the\n// host an MCP server; they exchange raw JSON-RPC 2.0 objects over `postMessage`\n// (no wrapper envelope). Method names are canonical to the ext-apps spec.\n// ---------------------------------------------------------------------------\n\n/** Protocol version exchanged in the `ui/initialize` handshake. */\nexport const MCPAPP_PROTOCOL_VERSION = \"2026-01-26\";\n/** View → host handshake request; the result carries the host context. */\nexport const MCPAPP_INITIALIZE = \"ui/initialize\";\n/** View → host notification sent once after the init result — the host holds all\n * pushes until it arrives. */\nexport const MCPAPP_INITIALIZED = \"ui/notifications/initialized\";\n/** Host → view notification carrying the tool output. `params` IS the\n * `CallToolResult` (data at `params.structuredContent`) — no `result` wrapper. */\nexport const MCPAPP_TOOL_RESULT = \"ui/notifications/tool-result\";\n/** Host → view notification carrying a partial host context (theme, styles, …). */\nexport const MCPAPP_HOST_CONTEXT_CHANGED = \"ui/notifications/host-context-changed\";\n/** View → host notification reporting intrinsic size (`{ width?, height? }`). A\n * host may keep the frame hidden until it receives one, so it is sent promptly. */\nexport const MCPAPP_SIZE_CHANGED = \"ui/notifications/size-changed\";\n/** View → host request: open an external URL (`{ url }`). */\nexport const MCPAPP_OPEN_LINK = \"ui/open-link\";\n/** View → host request: send a follow-up to the conversation\n * (`{ role: \"user\", content: ContentBlock[] }`). */\nexport const MCPAPP_MESSAGE = \"ui/message\";\n/** Host → view request: graceful teardown; the view replies `{}`. */\nexport const MCPAPP_TEARDOWN = \"ui/resource-teardown\";\n/** Standard MCP method the View may call over the same channel (pull). */\nexport const MCP_TOOLS_CALL = \"tools/call\";\n","import { coerceMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n OPENAI_SET_GLOBALS,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * The subset of the OpenAI Apps SDK `window.openai` surface this adapter uses.\n * All members are optional — the SDK version a host ships may not carry every\n * one, so each call is guarded and degraded.\n */\ninterface OpenAiHost {\n toolOutput?: unknown;\n theme?: unknown;\n callTool?: (name: string, args?: Record<string, unknown>) => Promise<unknown>;\n sendFollowUpMessage?: (arg: { prompt: string }) => void;\n /** Older casing seen in the wild — tried as a fallback. */\n sendFollowupMessage?: (arg: { prompt: string }) => void;\n openExternal?: (arg: { href: string }) => void;\n}\n\ninterface SetGlobalsDetail {\n globals?: { toolOutput?: unknown; theme?: unknown };\n}\n\n/**\n * ChatGPT (OpenAI Apps SDK) adapter.\n *\n * Data arrives on `window.openai.toolOutput` (available synchronously before the\n * widget script runs) and updates via the `openai:set_globals` event. Theme is a\n * mode string on the same surface. The host auto-sizes the iframe, so `resize()`\n * is a no-op. `callTool` is available when the host exposes `window.openai.callTool`.\n */\nexport function createChatGPTAdapter(\n win: Window & typeof globalThis,\n _options: ConnectUIOptions,\n): HostAdapter {\n const openai = () => (win as unknown as { openai?: OpenAiHost }).openai;\n\n let currentData: unknown = openai()?.toolOutput ?? null;\n let currentTheme: SynapseUITheme = {\n mode: coerceMode(openai()?.theme, \"light\"),\n tokens: {},\n };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n\n const onSetGlobals = (event: Event) => {\n if (destroyed) return;\n const globals = (event as CustomEvent<SetGlobalsDetail>).detail?.globals;\n if (!globals) return;\n if (\"toolOutput\" in globals && globals.toolOutput != null) {\n currentData = globals.toolOutput;\n for (const cb of dataCbs) cb(currentData);\n }\n if (\"theme\" in globals && globals.theme != null) {\n const mode = coerceMode(globals.theme, currentTheme.mode);\n if (mode !== currentTheme.mode) {\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n };\n\n return {\n host: \"chatgpt\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n const call = openai()?.callTool;\n if (!call) throw new HostUnsupportedError(\"callTool\", \"chatgpt\");\n return (await call(name, args ?? {})) as O;\n },\n sendPrompt(text: string) {\n const o = openai();\n const send = o?.sendFollowUpMessage ?? o?.sendFollowupMessage;\n send?.({ prompt: text });\n },\n openLink(url: string) {\n const open = openai()?.openExternal;\n if (open) open({ href: url });\n else win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // ChatGPT auto-sizes the widget iframe — nothing to report.\n },\n capabilities(): HostCapabilities {\n const o = openai();\n return {\n pull: typeof o?.callTool === \"function\",\n sendPrompt:\n typeof o?.sendFollowUpMessage === \"function\" ||\n typeof o?.sendFollowupMessage === \"function\",\n openLink: true,\n };\n },\n start() {\n win.addEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener, { passive: true });\n // Re-read in case the host mutated globals between construction and start.\n currentData = openai()?.toolOutput ?? currentData;\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(OPENAI_SET_GLOBALS, onSetGlobals as EventListener);\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","/**\n * Read data baked into the HTML as `<script type=\"application/json\" id=…>`.\n *\n * This is how the mcp-ui embedded-resource path and SSR/standalone deliver the\n * pushed tool output with no round-trip: the server helper escapes the payload\n * and substitutes it into the element (see the Python `SynapseUI.render_html`).\n * A data-free template leaves the marker comment in place, which fails\n * `JSON.parse` and reads back as `null` — exactly the \"no data yet\" state.\n */\nexport function readInlineData<T = unknown>(\n doc: Document | undefined,\n elementId: string,\n): T | null {\n if (!doc) return null;\n const el = doc.getElementById(elementId);\n const text = el?.textContent;\n if (!text) return null;\n try {\n const parsed = JSON.parse(text);\n return (parsed ?? null) as T | null;\n } catch {\n // Unreplaced marker or malformed blob — treat as no data.\n return null;\n }\n}\n\n/**\n * Pull the app payload out of an arbitrary render-data envelope. Hosts wrap the\n * tool output differently (`renderData`, `toolOutput`, `structuredContent`, or\n * the bare object), so unwrap the known envelope keys, else pass the object\n * through. Kept framework-generic — no app-specific keys.\n */\nexport function unwrapRenderData<T = unknown>(payload: unknown): T | null {\n if (payload == null || typeof payload !== \"object\") return null;\n const rec = payload as Record<string, unknown>;\n const nested = rec.renderData;\n const source =\n nested != null && typeof nested === \"object\" ? (nested as Record<string, unknown>) : rec;\n if (source.toolOutput != null) return source.toolOutput as T;\n if (source.structuredContent != null) return source.structuredContent as T;\n // No bare `data` envelope key — too ambiguous with an app's own `data` field.\n return source as T;\n}\n","import { readInlineData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n HostUnsupportedError,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/**\n * Inline / standalone fallback adapter.\n *\n * No live host bridge — used for SSR, previews, and a plain browser render. Data\n * comes only from the baked-in `<script type=\"application/json\">` blob; there is\n * nothing to push, so `onData` never fires after `start()`. Theme follows the OS\n * color scheme and tracks `prefers-color-scheme` changes. `openLink` opens a new\n * tab; `sendPrompt`/`callTool` have no destination.\n */\nexport function createInlineAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n let media: MediaQueryList | null = null;\n\n const onSchemeChange = (event: MediaQueryListEvent) => {\n if (destroyed) return;\n const mode = coerceMode(event.matches ? \"dark\" : \"light\", currentTheme.mode);\n if (mode === currentTheme.mode) return;\n currentTheme = { mode, tokens: {} };\n for (const cb of themeCbs) cb(currentTheme);\n };\n\n return {\n host: \"generic\",\n getData: <T>() => currentData as T | null,\n onData() {\n // Static render — no updates after the initial baked-in read.\n return () => {};\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(_name: string): Promise<O> {\n throw new HostUnsupportedError(\"callTool\", \"generic\");\n },\n sendPrompt() {\n // No agent to reach in a standalone render.\n },\n openLink(url: string) {\n win.open(url, \"_blank\", \"noopener,noreferrer\");\n },\n resize() {\n // No host to size for.\n },\n capabilities(): HostCapabilities {\n return { pull: false, sendPrompt: false, openLink: true };\n },\n start() {\n currentData = readInlineData(win.document, dataElementId);\n try {\n media = win.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n media?.addEventListener?.(\"change\", onSchemeChange);\n } catch {\n media = null;\n }\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n media?.removeEventListener?.(\"change\", onSchemeChange);\n media = null;\n themeCbs.clear();\n },\n };\n}\n","import { readInlineData, unwrapRenderData } from \"../data.js\";\nimport { coerceMode, preferredMode } from \"../theme.js\";\nimport {\n type ConnectUIOptions,\n type HostAdapter,\n type HostCapabilities,\n MCP_TOOLS_CALL,\n MCPAPP_HOST_CONTEXT_CHANGED,\n MCPAPP_INITIALIZE,\n MCPAPP_INITIALIZED,\n MCPAPP_MESSAGE,\n MCPAPP_OPEN_LINK,\n MCPAPP_PROTOCOL_VERSION,\n MCPAPP_SIZE_CHANGED,\n MCPAPP_TEARDOWN,\n MCPAPP_TOOL_RESULT,\n MCPUI_LINK,\n MCPUI_PROMPT,\n MCPUI_READY,\n MCPUI_RENDER_DATA,\n MCPUI_SIZE_CHANGE,\n SYNAPSE_DATA_ELEMENT_ID,\n type SynapseUITheme,\n} from \"../types.js\";\n\n/** How long a widget→host request (`callTool`, `openLink`, `sendPrompt`) waits\n * for its response before rejecting, so a silent host never hangs a promise. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\ninterface PendingRequest {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * MCP Apps standard (SEP-1865) adapter — the convergence bridge for Claude\n * Desktop and other MCP Apps hosts.\n *\n * The View iframe is an MCP client speaking JSON-RPC 2.0 to `window.parent`:\n *\n * 1. posts `ui/initialize` and awaits the host context (theme, style variables);\n * 2. posts `ui/notifications/initialized`, then a `size-changed` — a host keeps\n * the frame hidden until it has both the handshake and a size;\n * 3. receives data via `ui/notifications/tool-result` (`params` IS the\n * `CallToolResult`, so data is at `params.structuredContent`) and theme via\n * `ui/notifications/host-context-changed`.\n *\n * Actions go up as requests: `ui/open-link`, `ui/message` (follow-up), and\n * `tools/call` (pull). The legacy mcp-ui `ui-lifecycle-*` messages are sent and\n * accepted alongside so a pre-standard host still renders — a standard host drops\n * the non-JSON-RPC frames, and a legacy host ignores the JSON-RPC ones.\n */\nexport function createMcpAppsAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const dataElementId = options.dataElementId ?? SYNAPSE_DATA_ELEMENT_ID;\n const autoResize = options.autoResize !== false;\n\n let currentData: unknown = null;\n let currentTheme: SynapseUITheme = { mode: preferredMode(win), tokens: {} };\n\n const dataCbs = new Set<(d: unknown) => void>();\n const themeCbs = new Set<(t: SynapseUITheme) => void>();\n let destroyed = false;\n // Set once `ui/initialize` resolves — proof we're on an MCP Apps standard host,\n // which lets us stop mirroring actions to the legacy shim.\n let standardConfirmed = false;\n let nextId = 1;\n const pending = new Map<number, PendingRequest>();\n let lastReportedHeight = -1;\n let resizeObserver: ResizeObserver | null = null;\n let onWindowResize: (() => void) | null = null;\n\n const parent = () => win.parent ?? win;\n\n function post(message: Record<string, unknown>): void {\n parent().postMessage(message, \"*\");\n }\n\n // The legacy mcp-ui frames (render-data in, size/link/prompt out) are a\n // transitional shim for the one host that still speaks it — the NimbleBrain\n // runtime, which shares this adapter via the `nimblebrain` kind until the P3\n // `nimblebrain` adapter lands and this shim is removed. Once the handshake\n // confirms a standard host we stop mirroring, so a host that understood both\n // dialects never acts on an action twice.\n function postLegacy(message: Record<string, unknown>): void {\n if (standardConfirmed) return;\n post(message);\n }\n\n function notify(method: string, params?: Record<string, unknown>): void {\n post({ jsonrpc: \"2.0\", method, params: params ?? {} });\n }\n\n function request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {\n const id = nextId++;\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n pending.delete(id);\n reject(new Error(`\"${method}\" timed out`));\n }, REQUEST_TIMEOUT_MS);\n pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer });\n post({ jsonrpc: \"2.0\", id, method, params: params ?? {} });\n });\n }\n\n function emitData(next: unknown): void {\n if (next == null) return;\n currentData = next;\n for (const cb of dataCbs) cb(next);\n }\n\n /** Merge a full or partial host context into the resolved theme (mode + tokens). */\n function applyHostContext(ctx: Record<string, unknown> | null | undefined): void {\n if (!ctx || typeof ctx !== \"object\") return;\n let { mode, tokens } = currentTheme;\n let changed = false;\n if (ctx.theme != null) {\n const next = coerceMode(ctx.theme, mode);\n if (next !== mode) {\n mode = next;\n changed = true;\n }\n }\n const styles = ctx.styles as { variables?: Record<string, string> } | undefined;\n if (styles?.variables && typeof styles.variables === \"object\") {\n tokens = { ...tokens, ...styles.variables };\n changed = true;\n }\n if (changed) {\n currentTheme = { mode, tokens };\n for (const cb of themeCbs) cb(currentTheme);\n }\n }\n\n function reportSize(height?: number): void {\n if (destroyed) return;\n const h = typeof height === \"number\" ? height : Math.ceil(win.document.body.scrollHeight);\n if (h === lastReportedHeight) return;\n lastReportedHeight = h;\n notify(MCPAPP_SIZE_CHANGED, { height: h });\n postLegacy({ type: MCPUI_SIZE_CHANGE, payload: { height: h } });\n }\n\n function handleResponse(d: Record<string, unknown>): void {\n // Normalize the echoed id: JSON-RPC requires a same-type echo, but a lax host\n // that returned \"1\" for 1 would otherwise miss the numeric-keyed pending map.\n const id = Number(d.id);\n const p = pending.get(id);\n if (!p) return;\n pending.delete(id);\n clearTimeout(p.timer);\n if (d.error != null) {\n const err = d.error as { message?: string };\n p.reject(new Error(err.message ?? \"request failed\"));\n } else {\n p.resolve(d.result);\n }\n }\n\n function handleNotification(method: string, params: Record<string, unknown>): void {\n if (method === MCPAPP_TOOL_RESULT) {\n // `params` is the CallToolResult: the render data lives at structuredContent.\n const structured = params.structuredContent;\n emitData(structured != null ? structured : unwrapRenderData(params));\n } else if (method === MCPAPP_HOST_CONTEXT_CHANGED) {\n applyHostContext(params);\n }\n }\n\n function handleRequest(d: Record<string, unknown>): void {\n // The one host→view request we honor: acknowledge teardown so the host can\n // dispose the frame cleanly.\n if (d.method === MCPAPP_TEARDOWN) {\n post({ jsonrpc: \"2.0\", id: d.id, result: {} });\n }\n }\n\n /** Legacy mcp-ui render-data (non-JSON-RPC) — a pre-standard host's data path. */\n function handleLegacy(d: Record<string, unknown>): void {\n if (d.type === MCPUI_RENDER_DATA || d.type === \"renderData\") {\n const { theme, ...rest } = (d.payload ?? {}) as Record<string, unknown>;\n if (theme != null) applyHostContext({ theme });\n if (Object.keys(rest).length > 0) emitData(unwrapRenderData(rest));\n }\n }\n\n const onMessage = (event: MessageEvent) => {\n if (destroyed) return;\n // Accept only frames from the host window when the browser sets a source.\n if (event.source && event.source !== parent()) return;\n const d = event.data as Record<string, unknown> | null | undefined;\n if (!d || typeof d !== \"object\") return;\n if (d.jsonrpc !== \"2.0\") {\n handleLegacy(d);\n return;\n }\n if (d.id != null && (\"result\" in d || \"error\" in d)) {\n handleResponse(d);\n } else if (typeof d.method === \"string\") {\n if (d.id != null) handleRequest(d);\n else handleNotification(d.method, (d.params ?? {}) as Record<string, unknown>);\n }\n };\n\n function setupResize(): void {\n onWindowResize = () => reportSize();\n win.addEventListener(\"resize\", onWindowResize);\n if (typeof win.ResizeObserver !== \"undefined\") {\n resizeObserver = new win.ResizeObserver(() => reportSize());\n resizeObserver.observe(win.document.body);\n }\n }\n\n return {\n host: \"claude\",\n getData: <T>() => currentData as T | null,\n onData(cb) {\n dataCbs.add(cb as (d: unknown) => void);\n return () => dataCbs.delete(cb as (d: unknown) => void);\n },\n getTheme: () => currentTheme,\n onTheme(cb) {\n themeCbs.add(cb);\n return () => themeCbs.delete(cb);\n },\n async callTool<O>(name: string, args?: Record<string, unknown>): Promise<O> {\n return (await request(MCP_TOOLS_CALL, { name, arguments: args ?? {} })) as O;\n },\n sendPrompt(text: string) {\n // Standard follow-up (ack ignored); legacy mirror for a pre-standard host.\n void request(MCPAPP_MESSAGE, { role: \"user\", content: [{ type: \"text\", text }] }).catch(\n () => {},\n );\n postLegacy({ type: MCPUI_PROMPT, payload: { prompt: text } });\n },\n openLink(url: string) {\n void request(MCPAPP_OPEN_LINK, { url }).catch(() => {});\n postLegacy({ type: MCPUI_LINK, payload: { url } });\n },\n resize(height?: number) {\n reportSize(height);\n },\n capabilities(): HostCapabilities {\n // The MCP Apps standard host answers `tools/call` over this bridge, so pull\n // is advertised. A legacy-only host that shares this adapter (nimblebrain,\n // pre-P3) does not, so there callTool rejects only after REQUEST_TIMEOUT_MS\n // rather than failing fast.\n return { pull: true, sendPrompt: true, openLink: true };\n },\n start() {\n win.addEventListener(\"message\", onMessage as EventListener);\n\n // Baked-in data → available on first paint, before the handshake resolves.\n currentData = readInlineData(win.document, dataElementId);\n\n if (autoResize) setupResize();\n\n // Legacy mcp-ui ready (a standard host drops this non-JSON-RPC frame).\n post({ type: MCPUI_READY });\n\n // MCP Apps standard handshake. A legacy-only host never answers, so the\n // legacy render-data path (handleLegacy) still feeds the widget.\n request<{ hostContext?: Record<string, unknown> }>(MCPAPP_INITIALIZE, {\n appInfo: { name: options.name ?? \"synapse-ui\", version: options.version ?? \"0.0.0\" },\n appCapabilities: { availableDisplayModes: [\"inline\"] },\n protocolVersion: MCPAPP_PROTOCOL_VERSION,\n })\n .then((result) => {\n if (destroyed) return;\n standardConfirmed = true;\n applyHostContext(result?.hostContext);\n notify(MCPAPP_INITIALIZED, {});\n // A host keeps the frame hidden until it gets a size after init — force one.\n lastReportedHeight = -1;\n reportSize();\n })\n .catch(() => {\n // Not an MCP Apps host (or it was slow) — the legacy path covers data.\n });\n\n reportSize();\n },\n destroy() {\n if (destroyed) return;\n destroyed = true;\n win.removeEventListener(\"message\", onMessage as EventListener);\n if (onWindowResize) win.removeEventListener(\"resize\", onWindowResize);\n onWindowResize = null;\n resizeObserver?.disconnect();\n resizeObserver = null;\n for (const p of pending.values()) {\n clearTimeout(p.timer);\n p.reject(new Error(\"adapter destroyed\"));\n }\n pending.clear();\n dataCbs.clear();\n themeCbs.clear();\n },\n };\n}\n","import { createChatGPTAdapter } from \"./adapters/chatgpt.js\";\nimport { createInlineAdapter } from \"./adapters/inline.js\";\nimport { createMcpAppsAdapter } from \"./adapters/mcpapps.js\";\nimport type { ConnectUIOptions, HostAdapter, HostKind } from \"./types.js\";\n\n/**\n * Minimal window shape the detector inspects. Kept structural so detection is\n * unit-testable with a plain object — no full `Window` fake required.\n */\ninterface DetectableWindow {\n openai?: unknown;\n parent?: unknown;\n self?: unknown;\n}\n\n/**\n * Feature-detect the host from the browsing context.\n *\n * - `window.openai` present → **chatgpt** (OpenAI Apps SDK).\n * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin\n * access that throws) → **claude** (MCP Apps standard postMessage host).\n * - top-level document → **generic** (inline / standalone).\n *\n * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and\n * honors an explicit `options.host` override for previews/SSR/tests.\n */\nexport function detectHostKind(win: DetectableWindow): HostKind {\n if (win.openai != null) return \"chatgpt\";\n try {\n if (win.parent != null && win.parent !== win) return \"claude\";\n } catch {\n // Cross-origin parent access throws → we are framed by another origin.\n return \"claude\";\n }\n return \"generic\";\n}\n\n/** Build the adapter for an explicit host kind (`options.host` or a detected one). */\nexport function adapterForKind(\n kind: HostKind,\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n switch (kind) {\n case \"chatgpt\":\n return createChatGPTAdapter(win, options);\n case \"claude\":\n case \"nimblebrain\":\n // Both speak the MCP Apps standard (SEP-1865). `nimblebrain` shares it for\n // now; a dedicated adapter over the `synapse/*` extension lands in P3.\n return createMcpAppsAdapter(win, options);\n default:\n return createInlineAdapter(win, options);\n }\n}\n\n/** Select and construct the host adapter, honoring `options.host` when set. */\nexport function selectAdapter(\n win: Window & typeof globalThis,\n options: ConnectUIOptions,\n): HostAdapter {\n const kind = options.host ?? detectHostKind(win as unknown as DetectableWindow);\n return adapterForKind(kind, win, options);\n}\n","import { selectAdapter } from \"./detect.js\";\nimport { applyHostTheme } from \"./theme.js\";\nimport type { ConnectUIOptions, SynapseUIClient } from \"./types.js\";\n\n/**\n * Connect a Synapse-authored component to whatever host it renders in — ChatGPT\n * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one\n * push-first API. Feature-detects the host, selects an adapter, and applies the\n * host theme to the DOM before returning.\n *\n * Synchronous: `data()` is populated from baked-in data (where present) on\n * return, and pushed updates arrive via `onData`. Bind the result to `synapse`:\n *\n * ```ts\n * const synapse = connectUI({ name: \"my-widget\", version: \"1.0.0\" });\n * synapse.onData(render); // future pushes/updates\n * render(synapse.data()); // current value (null → empty state)\n * ```\n */\nexport function connectUI(options: ConnectUIOptions = {}): SynapseUIClient {\n const win = options.window ?? (globalThis as unknown as Window & typeof globalThis);\n const adapter = selectAdapter(win, options);\n\n // The client owns theme application: adapters resolve mode/tokens; this puts\n // them on the DOM (data-theme + CSS vars) so apps never wire theme by hand.\n applyHostTheme(adapter.getTheme());\n const unsubTheme = adapter.onTheme(applyHostTheme);\n\n adapter.start();\n\n let destroyed = false;\n\n return {\n data: <T>() => adapter.getData<T>(),\n onData: <T>(cb: (data: T) => void) => adapter.onData<T>(cb),\n theme: () => adapter.getTheme(),\n onTheme: (cb) => adapter.onTheme(cb),\n callTool: <O>(name: string, args?: Record<string, unknown>) => adapter.callTool<O>(name, args),\n sendPrompt: (text: string) => adapter.sendPrompt(text),\n openLink: (url: string) => adapter.openLink(url),\n resize: (height?: number) => adapter.resize(height),\n capabilities: () => adapter.capabilities(),\n host: () => adapter.host,\n destroy() {\n if (destroyed) return;\n destroyed = true;\n unsubTheme();\n adapter.destroy();\n },\n };\n}\n"]} |
| 'use strict'; | ||
| // src/theme-defaults.ts | ||
| var LIGHT = { | ||
| // ── Surfaces ── | ||
| "--color-background-primary": "#ffffff", | ||
| "--color-background-secondary": "#fafafa", | ||
| "--color-background-tertiary": "#f3f4f6", | ||
| // ── Text ── | ||
| "--color-text-primary": "#111827", | ||
| "--color-text-secondary": "#6b7280", | ||
| "--color-text-tertiary": "#9ca3af", | ||
| "--color-text-accent": "#2563eb", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring ── | ||
| "--color-border-primary": "#e5e7eb", | ||
| "--color-border-secondary": "#d1d5db", | ||
| "--color-ring-primary": "#2563eb", | ||
| // ── Status / brand semantics ── | ||
| "--nb-color-danger": "#dc2626", | ||
| "--nb-color-success": "#059669", | ||
| "--nb-color-warning": "#f59e0b", | ||
| "--nb-color-warm": "#d4620a", | ||
| "--nb-color-warm-light": "#fef5ee", | ||
| "--nb-color-processing": "#7c3aed", | ||
| "--nb-color-processing-light": "#f3eeff", | ||
| "--nb-color-info-light": "#eef4ff" | ||
| }; | ||
| var DARK = { | ||
| // ── Surfaces (base → lifted) ── | ||
| "--color-background-primary": "#18181b", | ||
| "--color-background-secondary": "#27272a", | ||
| "--color-background-tertiary": "#2f2f34", | ||
| // ── Text ── | ||
| "--color-text-primary": "#fafafa", | ||
| "--color-text-secondary": "#a1a1aa", | ||
| "--color-text-tertiary": "#71717a", | ||
| "--color-text-accent": "#818cf8", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring (lighter than surfaces so they remain visible) ── | ||
| "--color-border-primary": "#3f3f46", | ||
| "--color-border-secondary": "#52525b", | ||
| "--color-ring-primary": "#818cf8", | ||
| // ── Status / brand semantics (brightened for contrast on dark) ── | ||
| "--nb-color-danger": "#f87171", | ||
| "--nb-color-success": "#34d399", | ||
| "--nb-color-warning": "#fbbf24", | ||
| "--nb-color-warm": "#fb923c", | ||
| "--nb-color-warm-light": "#3a2a1e", | ||
| "--nb-color-processing": "#a78bfa", | ||
| "--nb-color-processing-light": "#2a2440", | ||
| "--nb-color-info-light": "#1e2a44" | ||
| }; | ||
| var DEFAULT_THEME_VARS = { | ||
| light: LIGHT, | ||
| dark: DARK | ||
| }; | ||
| function applyThemeVariables(mode, hostVars) { | ||
| if (typeof document === "undefined") return; | ||
| const root = document.documentElement.style; | ||
| for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) { | ||
| root.setProperty(k, v); | ||
| } | ||
| if (hostVars && typeof hostVars === "object") { | ||
| for (const [k, v] of Object.entries(hostVars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| root.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| exports.applyThemeVariables = applyThemeVariables; | ||
| //# sourceMappingURL=chunk-KTXASIFJ.cjs.map | ||
| //# sourceMappingURL=chunk-KTXASIFJ.cjs.map |
| {"version":3,"sources":["../src/theme-defaults.ts"],"names":[],"mappings":";;;AAsCA,IAAM,KAAA,GAAgC;AAAA;AAAA,EAEpC,4BAAA,EAA8B,SAAA;AAAA,EAC9B,8BAAA,EAAgC,SAAA;AAAA,EAChC,6BAAA,EAA+B,SAAA;AAAA;AAAA,EAE/B,sBAAA,EAAwB,SAAA;AAAA,EACxB,wBAAA,EAA0B,SAAA;AAAA,EAC1B,uBAAA,EAAyB,SAAA;AAAA,EACzB,qBAAA,EAAuB,SAAA;AAAA,EACvB,8BAAA,EAAgC,SAAA;AAAA;AAAA,EAEhC,wBAAA,EAA0B,SAAA;AAAA,EAC1B,0BAAA,EAA4B,SAAA;AAAA,EAC5B,sBAAA,EAAwB,SAAA;AAAA;AAAA,EAExB,mBAAA,EAAqB,SAAA;AAAA,EACrB,oBAAA,EAAsB,SAAA;AAAA,EACtB,oBAAA,EAAsB,SAAA;AAAA,EACtB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,uBAAA,EAAyB,SAAA;AAAA,EACzB,6BAAA,EAA+B,SAAA;AAAA,EAC/B,uBAAA,EAAyB;AAC3B,CAAA;AASA,IAAM,IAAA,GAA+B;AAAA;AAAA,EAEnC,4BAAA,EAA8B,SAAA;AAAA,EAC9B,8BAAA,EAAgC,SAAA;AAAA,EAChC,6BAAA,EAA+B,SAAA;AAAA;AAAA,EAE/B,sBAAA,EAAwB,SAAA;AAAA,EACxB,wBAAA,EAA0B,SAAA;AAAA,EAC1B,uBAAA,EAAyB,SAAA;AAAA,EACzB,qBAAA,EAAuB,SAAA;AAAA,EACvB,8BAAA,EAAgC,SAAA;AAAA;AAAA,EAEhC,wBAAA,EAA0B,SAAA;AAAA,EAC1B,0BAAA,EAA4B,SAAA;AAAA,EAC5B,sBAAA,EAAwB,SAAA;AAAA;AAAA,EAExB,mBAAA,EAAqB,SAAA;AAAA,EACrB,oBAAA,EAAsB,SAAA;AAAA,EACtB,oBAAA,EAAsB,SAAA;AAAA,EACtB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,uBAAA,EAAyB,SAAA;AAAA,EACzB,6BAAA,EAA+B,SAAA;AAAA,EAC/B,uBAAA,EAAyB;AAC3B,CAAA;AAMO,IAAM,kBAAA,GAAuE;AAAA,EAClF,KAAA,EAAO,KAAA;AAAA,EACP,IAAA,EAAM;AACR,CAAA;AAcO,SAAS,mBAAA,CACd,MACA,QAAA,EACM;AACN,EAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,EAAA,MAAM,IAAA,GAAO,SAAS,eAAA,CAAgB,KAAA;AACtC,EAAA,KAAA,MAAW,CAAC,GAAG,CAAC,CAAA,IAAK,OAAO,OAAA,CAAQ,kBAAA,CAAmB,IAAI,CAAC,CAAA,EAAG;AAC7D,IAAA,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,EACvB;AACA,EAAA,IAAI,QAAA,IAAY,OAAO,QAAA,KAAa,QAAA,EAAU;AAC5C,IAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC7C,MAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,OAAO,MAAM,QAAA,EAAU;AAClD,QAAA,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF","file":"chunk-KTXASIFJ.cjs","sourcesContent":["/**\n * Theme-aware default backing for the `@nimblebrain/synapse/ui` token contract.\n *\n * `tokens` (see `ui/tokens.ts`) are `var(--token, fallback)` references. A CSS\n * `var()` fallback is a *static literal* — it cannot branch on light vs. dark.\n * So any var the host does NOT inject resolves to its single hardcoded (light)\n * fallback in BOTH themes. Pair such a token with a theme-aware one and you get\n * white-on-white in dark mode: it looks correct in light, passes `tsc`/build,\n * and only breaks when the theme is toggled.\n *\n * This module closes that gap with a default layer the SDK controls and that\n * CAN branch on theme. {@link applyThemeVariables} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits still resolves to a theme-correct neutral value.\n *\n * It's the `var()` fallback, but able to branch on theme. The values stay\n * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. This keeps\n * the library host-agnostic while guaranteeing every token resolves correctly\n * in both themes even against an incomplete host, a standalone `connect()`\n * widget, or a third-party host.\n *\n * Only theme-sensitive (color) vars are listed here. Theme-invariant vars\n * (radii, type scale, shadows, font stacks, weights, border widths) look the\n * same in both themes, so their static `var()` fallback is already correct —\n * they are intentionally absent. The `tokens` regression test enforces this\n * with a TOTAL partition: every referenced var must be either declared\n * theme-invariant (an explicit allowlist) or defined in both maps below, so a\n * newly added theme-sensitive token can't slip through unbacked.\n */\n\n/**\n * Light-theme neutral defaults. Values match the (light) fallbacks baked into\n * `tokens` — applying them changes nothing observable in light mode; they exist\n * so the light path flows through the same code as dark and so a standalone\n * render with an explicit `light` theme is backed identically to its fallbacks.\n */\nconst LIGHT: Record<string, string> = {\n // ── Surfaces ──\n \"--color-background-primary\": \"#ffffff\",\n \"--color-background-secondary\": \"#fafafa\",\n \"--color-background-tertiary\": \"#f3f4f6\",\n // ── Text ──\n \"--color-text-primary\": \"#111827\",\n \"--color-text-secondary\": \"#6b7280\",\n \"--color-text-tertiary\": \"#9ca3af\",\n \"--color-text-accent\": \"#2563eb\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring ──\n \"--color-border-primary\": \"#e5e7eb\",\n \"--color-border-secondary\": \"#d1d5db\",\n \"--color-ring-primary\": \"#2563eb\",\n // ── Status / brand semantics ──\n \"--nb-color-danger\": \"#dc2626\",\n \"--nb-color-success\": \"#059669\",\n \"--nb-color-warning\": \"#f59e0b\",\n \"--nb-color-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\n \"--nb-color-processing\": \"#7c3aed\",\n \"--nb-color-processing-light\": \"#f3eeff\",\n \"--nb-color-info-light\": \"#eef4ff\",\n};\n\n/**\n * Dark-theme neutral defaults. A monotonic neutral ladder — the three surface\n * tiers are the darkest, borders sit a step lighter so they stay visible\n * against every surface, and text inverts to light. \"Subtle\"/\"strong\" keep\n * their light-mode semantics: `bgSubtle` reads as a lifted hover/inset tint and\n * `borderStrong` is more prominent than `border`.\n */\nconst DARK: Record<string, string> = {\n // ── Surfaces (base → lifted) ──\n \"--color-background-primary\": \"#18181b\",\n \"--color-background-secondary\": \"#27272a\",\n \"--color-background-tertiary\": \"#2f2f34\",\n // ── Text ──\n \"--color-text-primary\": \"#fafafa\",\n \"--color-text-secondary\": \"#a1a1aa\",\n \"--color-text-tertiary\": \"#71717a\",\n \"--color-text-accent\": \"#818cf8\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring (lighter than surfaces so they remain visible) ──\n \"--color-border-primary\": \"#3f3f46\",\n \"--color-border-secondary\": \"#52525b\",\n \"--color-ring-primary\": \"#818cf8\",\n // ── Status / brand semantics (brightened for contrast on dark) ──\n \"--nb-color-danger\": \"#f87171\",\n \"--nb-color-success\": \"#34d399\",\n \"--nb-color-warning\": \"#fbbf24\",\n \"--nb-color-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\n \"--nb-color-processing\": \"#a78bfa\",\n \"--nb-color-processing-light\": \"#2a2440\",\n \"--nb-color-info-light\": \"#1e2a44\",\n};\n\n/**\n * The neutral default theme, keyed by mode. Exported for the regression test\n * that asserts every color var referenced by `tokens` is backed in both modes.\n */\nexport const DEFAULT_THEME_VARS: Record<\"light\" | \"dark\", Record<string, string>> = {\n light: LIGHT,\n dark: DARK,\n};\n\n/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default. This is the single path by\n * which theming reaches the DOM (the handshake, `host-context-changed`, and the\n * React `<SynapseProvider>` all funnel through here).\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on every theme change is correct and cheap.\n */\nexport function applyThemeVariables(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\n}\n"]} |
| // src/theme-defaults.ts | ||
| var LIGHT = { | ||
| // ── Surfaces ── | ||
| "--color-background-primary": "#ffffff", | ||
| "--color-background-secondary": "#fafafa", | ||
| "--color-background-tertiary": "#f3f4f6", | ||
| // ── Text ── | ||
| "--color-text-primary": "#111827", | ||
| "--color-text-secondary": "#6b7280", | ||
| "--color-text-tertiary": "#9ca3af", | ||
| "--color-text-accent": "#2563eb", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring ── | ||
| "--color-border-primary": "#e5e7eb", | ||
| "--color-border-secondary": "#d1d5db", | ||
| "--color-ring-primary": "#2563eb", | ||
| // ── Status / brand semantics ── | ||
| "--nb-color-danger": "#dc2626", | ||
| "--nb-color-success": "#059669", | ||
| "--nb-color-warning": "#f59e0b", | ||
| "--nb-color-warm": "#d4620a", | ||
| "--nb-color-warm-light": "#fef5ee", | ||
| "--nb-color-processing": "#7c3aed", | ||
| "--nb-color-processing-light": "#f3eeff", | ||
| "--nb-color-info-light": "#eef4ff" | ||
| }; | ||
| var DARK = { | ||
| // ── Surfaces (base → lifted) ── | ||
| "--color-background-primary": "#18181b", | ||
| "--color-background-secondary": "#27272a", | ||
| "--color-background-tertiary": "#2f2f34", | ||
| // ── Text ── | ||
| "--color-text-primary": "#fafafa", | ||
| "--color-text-secondary": "#a1a1aa", | ||
| "--color-text-tertiary": "#71717a", | ||
| "--color-text-accent": "#818cf8", | ||
| "--nb-color-accent-foreground": "#ffffff", | ||
| // ── Border / ring (lighter than surfaces so they remain visible) ── | ||
| "--color-border-primary": "#3f3f46", | ||
| "--color-border-secondary": "#52525b", | ||
| "--color-ring-primary": "#818cf8", | ||
| // ── Status / brand semantics (brightened for contrast on dark) ── | ||
| "--nb-color-danger": "#f87171", | ||
| "--nb-color-success": "#34d399", | ||
| "--nb-color-warning": "#fbbf24", | ||
| "--nb-color-warm": "#fb923c", | ||
| "--nb-color-warm-light": "#3a2a1e", | ||
| "--nb-color-processing": "#a78bfa", | ||
| "--nb-color-processing-light": "#2a2440", | ||
| "--nb-color-info-light": "#1e2a44" | ||
| }; | ||
| var DEFAULT_THEME_VARS = { | ||
| light: LIGHT, | ||
| dark: DARK | ||
| }; | ||
| function applyThemeVariables(mode, hostVars) { | ||
| if (typeof document === "undefined") return; | ||
| const root = document.documentElement.style; | ||
| for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) { | ||
| root.setProperty(k, v); | ||
| } | ||
| if (hostVars && typeof hostVars === "object") { | ||
| for (const [k, v] of Object.entries(hostVars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| root.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| export { applyThemeVariables }; | ||
| //# sourceMappingURL=chunk-VXTMI266.js.map | ||
| //# sourceMappingURL=chunk-VXTMI266.js.map |
| {"version":3,"sources":["../src/theme-defaults.ts"],"names":[],"mappings":";AAsCA,IAAM,KAAA,GAAgC;AAAA;AAAA,EAEpC,4BAAA,EAA8B,SAAA;AAAA,EAC9B,8BAAA,EAAgC,SAAA;AAAA,EAChC,6BAAA,EAA+B,SAAA;AAAA;AAAA,EAE/B,sBAAA,EAAwB,SAAA;AAAA,EACxB,wBAAA,EAA0B,SAAA;AAAA,EAC1B,uBAAA,EAAyB,SAAA;AAAA,EACzB,qBAAA,EAAuB,SAAA;AAAA,EACvB,8BAAA,EAAgC,SAAA;AAAA;AAAA,EAEhC,wBAAA,EAA0B,SAAA;AAAA,EAC1B,0BAAA,EAA4B,SAAA;AAAA,EAC5B,sBAAA,EAAwB,SAAA;AAAA;AAAA,EAExB,mBAAA,EAAqB,SAAA;AAAA,EACrB,oBAAA,EAAsB,SAAA;AAAA,EACtB,oBAAA,EAAsB,SAAA;AAAA,EACtB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,uBAAA,EAAyB,SAAA;AAAA,EACzB,6BAAA,EAA+B,SAAA;AAAA,EAC/B,uBAAA,EAAyB;AAC3B,CAAA;AASA,IAAM,IAAA,GAA+B;AAAA;AAAA,EAEnC,4BAAA,EAA8B,SAAA;AAAA,EAC9B,8BAAA,EAAgC,SAAA;AAAA,EAChC,6BAAA,EAA+B,SAAA;AAAA;AAAA,EAE/B,sBAAA,EAAwB,SAAA;AAAA,EACxB,wBAAA,EAA0B,SAAA;AAAA,EAC1B,uBAAA,EAAyB,SAAA;AAAA,EACzB,qBAAA,EAAuB,SAAA;AAAA,EACvB,8BAAA,EAAgC,SAAA;AAAA;AAAA,EAEhC,wBAAA,EAA0B,SAAA;AAAA,EAC1B,0BAAA,EAA4B,SAAA;AAAA,EAC5B,sBAAA,EAAwB,SAAA;AAAA;AAAA,EAExB,mBAAA,EAAqB,SAAA;AAAA,EACrB,oBAAA,EAAsB,SAAA;AAAA,EACtB,oBAAA,EAAsB,SAAA;AAAA,EACtB,iBAAA,EAAmB,SAAA;AAAA,EACnB,uBAAA,EAAyB,SAAA;AAAA,EACzB,uBAAA,EAAyB,SAAA;AAAA,EACzB,6BAAA,EAA+B,SAAA;AAAA,EAC/B,uBAAA,EAAyB;AAC3B,CAAA;AAMO,IAAM,kBAAA,GAAuE;AAAA,EAClF,KAAA,EAAO,KAAA;AAAA,EACP,IAAA,EAAM;AACR,CAAA;AAcO,SAAS,mBAAA,CACd,MACA,QAAA,EACM;AACN,EAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,EAAA,MAAM,IAAA,GAAO,SAAS,eAAA,CAAgB,KAAA;AACtC,EAAA,KAAA,MAAW,CAAC,GAAG,CAAC,CAAA,IAAK,OAAO,OAAA,CAAQ,kBAAA,CAAmB,IAAI,CAAC,CAAA,EAAG;AAC7D,IAAA,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,EACvB;AACA,EAAA,IAAI,QAAA,IAAY,OAAO,QAAA,KAAa,QAAA,EAAU;AAC5C,IAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC7C,MAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,OAAO,MAAM,QAAA,EAAU;AAClD,QAAA,IAAA,CAAK,WAAA,CAAY,GAAG,CAAC,CAAA;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF","file":"chunk-VXTMI266.js","sourcesContent":["/**\n * Theme-aware default backing for the `@nimblebrain/synapse/ui` token contract.\n *\n * `tokens` (see `ui/tokens.ts`) are `var(--token, fallback)` references. A CSS\n * `var()` fallback is a *static literal* — it cannot branch on light vs. dark.\n * So any var the host does NOT inject resolves to its single hardcoded (light)\n * fallback in BOTH themes. Pair such a token with a theme-aware one and you get\n * white-on-white in dark mode: it looks correct in light, passes `tsc`/build,\n * and only breaks when the theme is toggled.\n *\n * This module closes that gap with a default layer the SDK controls and that\n * CAN branch on theme. {@link applyThemeVariables} writes the neutral defaults\n * for the active `mode` first, then the host's variables on top — so:\n * - the host's (brand) values always win for the keys it provides, and\n * - any var the host omits still resolves to a theme-correct neutral value.\n *\n * It's the `var()` fallback, but able to branch on theme. The values stay\n * deliberately NEUTRAL (grays + a generic blue), never NimbleBrain brand —\n * brand arrives only by host injection, mirroring `ui/tokens.ts`. This keeps\n * the library host-agnostic while guaranteeing every token resolves correctly\n * in both themes even against an incomplete host, a standalone `connect()`\n * widget, or a third-party host.\n *\n * Only theme-sensitive (color) vars are listed here. Theme-invariant vars\n * (radii, type scale, shadows, font stacks, weights, border widths) look the\n * same in both themes, so their static `var()` fallback is already correct —\n * they are intentionally absent. The `tokens` regression test enforces this\n * with a TOTAL partition: every referenced var must be either declared\n * theme-invariant (an explicit allowlist) or defined in both maps below, so a\n * newly added theme-sensitive token can't slip through unbacked.\n */\n\n/**\n * Light-theme neutral defaults. Values match the (light) fallbacks baked into\n * `tokens` — applying them changes nothing observable in light mode; they exist\n * so the light path flows through the same code as dark and so a standalone\n * render with an explicit `light` theme is backed identically to its fallbacks.\n */\nconst LIGHT: Record<string, string> = {\n // ── Surfaces ──\n \"--color-background-primary\": \"#ffffff\",\n \"--color-background-secondary\": \"#fafafa\",\n \"--color-background-tertiary\": \"#f3f4f6\",\n // ── Text ──\n \"--color-text-primary\": \"#111827\",\n \"--color-text-secondary\": \"#6b7280\",\n \"--color-text-tertiary\": \"#9ca3af\",\n \"--color-text-accent\": \"#2563eb\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring ──\n \"--color-border-primary\": \"#e5e7eb\",\n \"--color-border-secondary\": \"#d1d5db\",\n \"--color-ring-primary\": \"#2563eb\",\n // ── Status / brand semantics ──\n \"--nb-color-danger\": \"#dc2626\",\n \"--nb-color-success\": \"#059669\",\n \"--nb-color-warning\": \"#f59e0b\",\n \"--nb-color-warm\": \"#d4620a\",\n \"--nb-color-warm-light\": \"#fef5ee\",\n \"--nb-color-processing\": \"#7c3aed\",\n \"--nb-color-processing-light\": \"#f3eeff\",\n \"--nb-color-info-light\": \"#eef4ff\",\n};\n\n/**\n * Dark-theme neutral defaults. A monotonic neutral ladder — the three surface\n * tiers are the darkest, borders sit a step lighter so they stay visible\n * against every surface, and text inverts to light. \"Subtle\"/\"strong\" keep\n * their light-mode semantics: `bgSubtle` reads as a lifted hover/inset tint and\n * `borderStrong` is more prominent than `border`.\n */\nconst DARK: Record<string, string> = {\n // ── Surfaces (base → lifted) ──\n \"--color-background-primary\": \"#18181b\",\n \"--color-background-secondary\": \"#27272a\",\n \"--color-background-tertiary\": \"#2f2f34\",\n // ── Text ──\n \"--color-text-primary\": \"#fafafa\",\n \"--color-text-secondary\": \"#a1a1aa\",\n \"--color-text-tertiary\": \"#71717a\",\n \"--color-text-accent\": \"#818cf8\",\n \"--nb-color-accent-foreground\": \"#ffffff\",\n // ── Border / ring (lighter than surfaces so they remain visible) ──\n \"--color-border-primary\": \"#3f3f46\",\n \"--color-border-secondary\": \"#52525b\",\n \"--color-ring-primary\": \"#818cf8\",\n // ── Status / brand semantics (brightened for contrast on dark) ──\n \"--nb-color-danger\": \"#f87171\",\n \"--nb-color-success\": \"#34d399\",\n \"--nb-color-warning\": \"#fbbf24\",\n \"--nb-color-warm\": \"#fb923c\",\n \"--nb-color-warm-light\": \"#3a2a1e\",\n \"--nb-color-processing\": \"#a78bfa\",\n \"--nb-color-processing-light\": \"#2a2440\",\n \"--nb-color-info-light\": \"#1e2a44\",\n};\n\n/**\n * The neutral default theme, keyed by mode. Exported for the regression test\n * that asserts every color var referenced by `tokens` is backed in both modes.\n */\nexport const DEFAULT_THEME_VARS: Record<\"light\" | \"dark\", Record<string, string>> = {\n light: LIGHT,\n dark: DARK,\n};\n\n/**\n * Apply theme CSS custom properties onto `document.documentElement`.\n *\n * Writes the neutral defaults for `mode` FIRST, then the host's variables on\n * top — so the host's values win for the keys it provides, and any var it omits\n * still resolves to a theme-correct neutral default. This is the single path by\n * which theming reaches the DOM (the handshake, `host-context-changed`, and the\n * React `<SynapseProvider>` all funnel through here).\n *\n * SSR-safe (no-ops when `document` is unavailable). Idempotent — `setProperty`\n * overwrites, so re-applying on every theme change is correct and cheap.\n */\nexport function applyThemeVariables(\n mode: \"light\" | \"dark\",\n hostVars: Record<string, string> | undefined | null,\n): void {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement.style;\n for (const [k, v] of Object.entries(DEFAULT_THEME_VARS[mode])) {\n root.setProperty(k, v);\n }\n if (hostVars && typeof hostVars === \"object\") {\n for (const [k, v] of Object.entries(hostVars)) {\n if (typeof k === \"string\" && typeof v === \"string\") {\n root.setProperty(k, v);\n }\n }\n }\n}\n"]} |
| import { applyThemeVariables } from './chunk-VXTMI266.js'; | ||
| import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, HOST_CONTEXT_CHANGED_METHOD, INITIALIZED_METHOD, MESSAGE_METHOD, OPEN_LINK_METHOD, TOOL_RESULT_METHOD, RESOURCE_TEARDOWN_METHOD, TOOL_CANCELLED_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_INPUT_METHOD } from '@modelcontextprotocol/ext-apps'; | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": TOOL_RESULT_METHOD, | ||
| "tool-input": TOOL_INPUT_METHOD, | ||
| "tool-input-partial": TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": TOOL_CANCELLED_METHOD, | ||
| "theme-changed": HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| applyThemeVariables(currentTheme.mode, currentTheme.tokens); | ||
| } | ||
| } | ||
| transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| applyThemeVariables(mode, tokens); | ||
| const set = handlers.get(HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| { | ||
| const theme = extractTheme(currentHostContext); | ||
| applyThemeVariables(theme.mode, theme.tokens); | ||
| } | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| const theme = extractTheme(currentHostContext); | ||
| applyThemeVariables(theme.mode, theme.tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| export { connect, createSynapse }; | ||
| //# sourceMappingURL=chunk-XGJAADVV.js.map | ||
| //# sourceMappingURL=chunk-XGJAADVV.js.map |
Sorry, the diff of this file is too big to display
| /** | ||
| * Cross-host UI client — types and wire constants. | ||
| * | ||
| * The cross-host client (`connectUI`) renders one Synapse-authored component in | ||
| * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude | ||
| * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch | ||
| * a host protocol. This is a **push-first** surface: the tool output that spawned | ||
| * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the | ||
| * pull escape hatch, advertised per host via `capabilities()`. | ||
| * | ||
| * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the | ||
| * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so | ||
| * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas). | ||
| */ | ||
| /** | ||
| * The host the client resolved to, as reported by `synapse.host()`. An escape | ||
| * hatch — apps should rarely branch on it; `capabilities()` is the supported way | ||
| * to feature-detect. `"nimblebrain"` is reserved for the runtime adapter (P3). | ||
| */ | ||
| type HostKind = "chatgpt" | "claude" | "nimblebrain" | "generic"; | ||
| /** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS | ||
| * custom properties the host publishes — the MCP Apps adapter reads them from | ||
| * `hostContext.styles.variables`; where a host sends none they stay empty and the | ||
| * SDK's neutral defaults back them. */ | ||
| interface SynapseUITheme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| /** What the active host actually supports. `data()`/`onData()`/`theme()`/ | ||
| * `resize()` work everywhere; these three vary. */ | ||
| interface HostCapabilities { | ||
| /** `callTool()` can reach the server (widget→server fetch). */ | ||
| pull: boolean; | ||
| /** `sendPrompt()` reaches the agent conversation. */ | ||
| sendPrompt: boolean; | ||
| /** `openLink()` opens an external URL through the host. */ | ||
| openLink: boolean; | ||
| } | ||
| /** Thrown by `callTool()` when the active host offers no widget→server call. */ | ||
| declare class HostUnsupportedError extends Error { | ||
| constructor(feature: string, host: HostKind); | ||
| } | ||
| interface ConnectUIOptions { | ||
| /** App name — informational; forwarded to hosts that accept an appInfo. */ | ||
| name?: string; | ||
| /** App semver — informational. */ | ||
| version?: string; | ||
| /** | ||
| * Force a host adapter instead of auto-detecting. Used by preview harnesses, | ||
| * SSR, and tests; production apps omit it and let the SDK feature-detect. | ||
| * `"claude"` → MCP Apps standard adapter, `"chatgpt"` → OpenAI Apps adapter, | ||
| * `"generic"` → inline adapter. | ||
| */ | ||
| host?: HostKind; | ||
| /** | ||
| * `id` of the `<script type="application/json">` element carrying pushed data | ||
| * baked into the HTML (the mcp-ui / SSR path). Defaults to | ||
| * {@link SYNAPSE_DATA_ELEMENT_ID}. | ||
| */ | ||
| dataElementId?: string; | ||
| /** | ||
| * Auto-report content height to the host on layout changes (mcp-ui only). | ||
| * Defaults to `true`. Set `false` to size manually via `resize()`. | ||
| */ | ||
| autoResize?: boolean; | ||
| /** Window to bind to. Defaults to the global `window`. Injectable for tests. */ | ||
| window?: Window & typeof globalThis; | ||
| } | ||
| /** | ||
| * The public cross-host client. Bound to `synapse` by convention. | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI(); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (may be null → empty state) | ||
| * ``` | ||
| */ | ||
| interface SynapseUIClient { | ||
| /** The current pushed data, or `null` before anything has been delivered. */ | ||
| data<T = unknown>(): T | null; | ||
| /** Subscribe to data updates (NOT replayed — read `data()` for the current | ||
| * value). Returns an unsubscribe. */ | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| /** The current resolved theme. */ | ||
| theme(): SynapseUITheme; | ||
| /** Subscribe to theme changes. The client already applies the theme to the DOM | ||
| * (`data-theme` attribute + CSS variables) before this fires. */ | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the | ||
| * host advertises no pull (`capabilities().pull === false`). */ | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| /** Send a follow-up message to the agent conversation. No-op where unsupported. */ | ||
| sendPrompt(text: string): void; | ||
| /** Open an external URL through the host (falls back to `window.open`). */ | ||
| openLink(url: string): void; | ||
| /** Report content height to the host. Omit `height` to measure `document.body`. */ | ||
| resize(height?: number): void; | ||
| /** What the active host supports. */ | ||
| capabilities(): HostCapabilities; | ||
| /** The resolved host — an escape hatch; prefer `capabilities()`. */ | ||
| host(): HostKind; | ||
| /** Tear down listeners/observers. */ | ||
| destroy(): void; | ||
| } | ||
| /** | ||
| * Internal adapter contract. One per host bridge; the client is a thin façade | ||
| * over the selected adapter. | ||
| */ | ||
| interface HostAdapter { | ||
| readonly host: HostKind; | ||
| getData<T = unknown>(): T | null; | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| getTheme(): SynapseUITheme; | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| sendPrompt(text: string): void; | ||
| openLink(url: string): void; | ||
| resize(height?: number): void; | ||
| capabilities(): HostCapabilities; | ||
| /** Begin listening / send the ready handshake / read baked-in data. Called once | ||
| * by `connectUI` synchronously so `getData()` is populated on return. */ | ||
| start(): void; | ||
| destroy(): void; | ||
| } | ||
| /** Default `id` of the baked-in data `<script type="application/json">`. */ | ||
| declare const SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| /** | ||
| * Connect a Synapse-authored component to whatever host it renders in — ChatGPT | ||
| * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one | ||
| * push-first API. Feature-detects the host, selects an adapter, and applies the | ||
| * host theme to the DOM before returning. | ||
| * | ||
| * Synchronous: `data()` is populated from baked-in data (where present) on | ||
| * return, and pushed updates arrive via `onData`. Bind the result to `synapse`: | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI({ name: "my-widget", version: "1.0.0" }); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (null → empty state) | ||
| * ``` | ||
| */ | ||
| declare function connectUI(options?: ConnectUIOptions): SynapseUIClient; | ||
| /** | ||
| * Minimal window shape the detector inspects. Kept structural so detection is | ||
| * unit-testable with a plain object — no full `Window` fake required. | ||
| */ | ||
| interface DetectableWindow { | ||
| openai?: unknown; | ||
| parent?: unknown; | ||
| self?: unknown; | ||
| } | ||
| /** | ||
| * Feature-detect the host from the browsing context. | ||
| * | ||
| * - `window.openai` present → **chatgpt** (OpenAI Apps SDK). | ||
| * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin | ||
| * access that throws) → **claude** (MCP Apps standard postMessage host). | ||
| * - top-level document → **generic** (inline / standalone). | ||
| * | ||
| * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and | ||
| * honors an explicit `options.host` override for previews/SSR/tests. | ||
| */ | ||
| declare function detectHostKind(win: DetectableWindow): HostKind; | ||
| /** Build the adapter for an explicit host kind (`options.host` or a detected one). */ | ||
| declare function adapterForKind(kind: HostKind, win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| /** Select and construct the host adapter, honoring `options.host` when set. */ | ||
| declare function selectAdapter(win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| export { type ConnectUIOptions as C, type HostCapabilities as H, SYNAPSE_DATA_ELEMENT_ID as S, type HostKind as a, HostUnsupportedError as b, type SynapseUIClient as c, type SynapseUITheme as d, connectUI as e, detectHostKind as f, type HostAdapter as g, adapterForKind as h, selectAdapter as s }; |
| /** | ||
| * Cross-host UI client — types and wire constants. | ||
| * | ||
| * The cross-host client (`connectUI`) renders one Synapse-authored component in | ||
| * hosts that each speak a different bridge: ChatGPT (OpenAI Apps SDK), Claude | ||
| * (mcp-ui), and plain/standalone. Apps code against `synapse.*` and never touch | ||
| * a host protocol. This is a **push-first** surface: the tool output that spawned | ||
| * the widget is delivered at render (`data()` / `onData()`); `callTool()` is the | ||
| * pull escape hatch, advertised per host via `capabilities()`. | ||
| * | ||
| * This layer intentionally has ZERO dependency on `@modelcontextprotocol/*` — the | ||
| * ChatGPT / mcp-ui / inline bridges are pure `window.openai` + `postMessage`, so | ||
| * the IIFE that apps inline stays tiny (no Zod, no ext-apps schemas). | ||
| */ | ||
| /** | ||
| * The host the client resolved to, as reported by `synapse.host()`. An escape | ||
| * hatch — apps should rarely branch on it; `capabilities()` is the supported way | ||
| * to feature-detect. `"nimblebrain"` is reserved for the runtime adapter (P3). | ||
| */ | ||
| type HostKind = "chatgpt" | "claude" | "nimblebrain" | "generic"; | ||
| /** Resolved theme. `mode` always resolves to light or dark. `tokens` are CSS | ||
| * custom properties the host publishes — the MCP Apps adapter reads them from | ||
| * `hostContext.styles.variables`; where a host sends none they stay empty and the | ||
| * SDK's neutral defaults back them. */ | ||
| interface SynapseUITheme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| /** What the active host actually supports. `data()`/`onData()`/`theme()`/ | ||
| * `resize()` work everywhere; these three vary. */ | ||
| interface HostCapabilities { | ||
| /** `callTool()` can reach the server (widget→server fetch). */ | ||
| pull: boolean; | ||
| /** `sendPrompt()` reaches the agent conversation. */ | ||
| sendPrompt: boolean; | ||
| /** `openLink()` opens an external URL through the host. */ | ||
| openLink: boolean; | ||
| } | ||
| /** Thrown by `callTool()` when the active host offers no widget→server call. */ | ||
| declare class HostUnsupportedError extends Error { | ||
| constructor(feature: string, host: HostKind); | ||
| } | ||
| interface ConnectUIOptions { | ||
| /** App name — informational; forwarded to hosts that accept an appInfo. */ | ||
| name?: string; | ||
| /** App semver — informational. */ | ||
| version?: string; | ||
| /** | ||
| * Force a host adapter instead of auto-detecting. Used by preview harnesses, | ||
| * SSR, and tests; production apps omit it and let the SDK feature-detect. | ||
| * `"claude"` → MCP Apps standard adapter, `"chatgpt"` → OpenAI Apps adapter, | ||
| * `"generic"` → inline adapter. | ||
| */ | ||
| host?: HostKind; | ||
| /** | ||
| * `id` of the `<script type="application/json">` element carrying pushed data | ||
| * baked into the HTML (the mcp-ui / SSR path). Defaults to | ||
| * {@link SYNAPSE_DATA_ELEMENT_ID}. | ||
| */ | ||
| dataElementId?: string; | ||
| /** | ||
| * Auto-report content height to the host on layout changes (mcp-ui only). | ||
| * Defaults to `true`. Set `false` to size manually via `resize()`. | ||
| */ | ||
| autoResize?: boolean; | ||
| /** Window to bind to. Defaults to the global `window`. Injectable for tests. */ | ||
| window?: Window & typeof globalThis; | ||
| } | ||
| /** | ||
| * The public cross-host client. Bound to `synapse` by convention. | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI(); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (may be null → empty state) | ||
| * ``` | ||
| */ | ||
| interface SynapseUIClient { | ||
| /** The current pushed data, or `null` before anything has been delivered. */ | ||
| data<T = unknown>(): T | null; | ||
| /** Subscribe to data updates (NOT replayed — read `data()` for the current | ||
| * value). Returns an unsubscribe. */ | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| /** The current resolved theme. */ | ||
| theme(): SynapseUITheme; | ||
| /** Subscribe to theme changes. The client already applies the theme to the DOM | ||
| * (`data-theme` attribute + CSS variables) before this fires. */ | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| /** Widget→server tool call. Rejects with {@link HostUnsupportedError} where the | ||
| * host advertises no pull (`capabilities().pull === false`). */ | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| /** Send a follow-up message to the agent conversation. No-op where unsupported. */ | ||
| sendPrompt(text: string): void; | ||
| /** Open an external URL through the host (falls back to `window.open`). */ | ||
| openLink(url: string): void; | ||
| /** Report content height to the host. Omit `height` to measure `document.body`. */ | ||
| resize(height?: number): void; | ||
| /** What the active host supports. */ | ||
| capabilities(): HostCapabilities; | ||
| /** The resolved host — an escape hatch; prefer `capabilities()`. */ | ||
| host(): HostKind; | ||
| /** Tear down listeners/observers. */ | ||
| destroy(): void; | ||
| } | ||
| /** | ||
| * Internal adapter contract. One per host bridge; the client is a thin façade | ||
| * over the selected adapter. | ||
| */ | ||
| interface HostAdapter { | ||
| readonly host: HostKind; | ||
| getData<T = unknown>(): T | null; | ||
| onData<T = unknown>(cb: (data: T) => void): () => void; | ||
| getTheme(): SynapseUITheme; | ||
| onTheme(cb: (theme: SynapseUITheme) => void): () => void; | ||
| callTool<O = unknown>(name: string, args?: Record<string, unknown>): Promise<O>; | ||
| sendPrompt(text: string): void; | ||
| openLink(url: string): void; | ||
| resize(height?: number): void; | ||
| capabilities(): HostCapabilities; | ||
| /** Begin listening / send the ready handshake / read baked-in data. Called once | ||
| * by `connectUI` synchronously so `getData()` is populated on return. */ | ||
| start(): void; | ||
| destroy(): void; | ||
| } | ||
| /** Default `id` of the baked-in data `<script type="application/json">`. */ | ||
| declare const SYNAPSE_DATA_ELEMENT_ID = "synapse-ui-data"; | ||
| /** | ||
| * Connect a Synapse-authored component to whatever host it renders in — ChatGPT | ||
| * (OpenAI Apps SDK), Claude (MCP Apps standard), or a plain/standalone page — behind one | ||
| * push-first API. Feature-detects the host, selects an adapter, and applies the | ||
| * host theme to the DOM before returning. | ||
| * | ||
| * Synchronous: `data()` is populated from baked-in data (where present) on | ||
| * return, and pushed updates arrive via `onData`. Bind the result to `synapse`: | ||
| * | ||
| * ```ts | ||
| * const synapse = connectUI({ name: "my-widget", version: "1.0.0" }); | ||
| * synapse.onData(render); // future pushes/updates | ||
| * render(synapse.data()); // current value (null → empty state) | ||
| * ``` | ||
| */ | ||
| declare function connectUI(options?: ConnectUIOptions): SynapseUIClient; | ||
| /** | ||
| * Minimal window shape the detector inspects. Kept structural so detection is | ||
| * unit-testable with a plain object — no full `Window` fake required. | ||
| */ | ||
| interface DetectableWindow { | ||
| openai?: unknown; | ||
| parent?: unknown; | ||
| self?: unknown; | ||
| } | ||
| /** | ||
| * Feature-detect the host from the browsing context. | ||
| * | ||
| * - `window.openai` present → **chatgpt** (OpenAI Apps SDK). | ||
| * - otherwise, a nested browsing context (`parent !== self`, or a cross-origin | ||
| * access that throws) → **claude** (MCP Apps standard postMessage host). | ||
| * - top-level document → **generic** (inline / standalone). | ||
| * | ||
| * Returns the host *kind*; `chooseAdapter` maps it to a concrete adapter and | ||
| * honors an explicit `options.host` override for previews/SSR/tests. | ||
| */ | ||
| declare function detectHostKind(win: DetectableWindow): HostKind; | ||
| /** Build the adapter for an explicit host kind (`options.host` or a detected one). */ | ||
| declare function adapterForKind(kind: HostKind, win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| /** Select and construct the host adapter, honoring `options.host` when set. */ | ||
| declare function selectAdapter(win: Window & typeof globalThis, options: ConnectUIOptions): HostAdapter; | ||
| export { type ConnectUIOptions as C, type HostCapabilities as H, SYNAPSE_DATA_ELEMENT_ID as S, type HostKind as a, HostUnsupportedError as b, type SynapseUIClient as c, type SynapseUITheme as d, connectUI as e, detectHostKind as f, type HostAdapter as g, adapterForKind as h, selectAdapter as s }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
| 'use strict'; | ||
| // src/ui/fonts.ts | ||
| var FONTSHARE_HREF = "https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=erode@400,500&display=swap"; | ||
| var LINK_ID = "nb-synapse-fonts"; | ||
| function injectFonts() { | ||
| if (typeof document === "undefined") return; | ||
| if (document.getElementById(LINK_ID)) return; | ||
| const link = document.createElement("link"); | ||
| link.id = LINK_ID; | ||
| link.rel = "stylesheet"; | ||
| link.href = FONTSHARE_HREF; | ||
| document.head.appendChild(link); | ||
| } | ||
| injectFonts(); | ||
| exports.injectFonts = injectFonts; | ||
| //# sourceMappingURL=fonts.cjs.map | ||
| //# sourceMappingURL=fonts.cjs.map |
| {"version":3,"sources":["../../src/ui/fonts.ts"],"names":[],"mappings":";;;AAuBA,IAAM,cAAA,GACJ,yFAAA;AACF,IAAM,OAAA,GAAU,kBAAA;AAOT,SAAS,WAAA,GAAoB;AAClC,EAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,EAAA,IAAI,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACtC,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,EAAA,IAAA,CAAK,EAAA,GAAK,OAAA;AACV,EAAA,IAAA,CAAK,GAAA,GAAM,YAAA;AACX,EAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,IAAI,CAAA;AAChC;AAEA,WAAA,EAAY","file":"fonts.cjs","sourcesContent":["/**\n * Loads the NimbleBrain brand fonts into the current document. Import for the\n * side effect:\n *\n * ```ts\n * import \"@nimblebrain/synapse/ui/fonts\";\n * ```\n *\n * The token contract names these fonts (`--font-sans: Satoshi`,\n * `--nb-font-heading: Erode`), but a font *name* only renders if the font\n * *files* are loaded in the iframe document — each app iframe is its own\n * document and does not inherit the host's `@font-face`s. This module injects\n * the Fontshare stylesheet so the named fonts resolve instead of silently\n * falling back.\n *\n * JetBrains Mono (`--font-mono`) is not injected here — apps that render code\n * should add `@fontsource-variable/jetbrains-mono` to their own bundle.\n *\n * CSP note: the host iframe must allow `https://api.fontshare.com` in its\n * `style-src`/`font-src` for this to take effect. If blocked, fonts fall back\n * to the neutral system stack in the token contract.\n */\n\nconst FONTSHARE_HREF =\n \"https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=erode@400,500&display=swap\";\nconst LINK_ID = \"nb-synapse-fonts\";\n\n/**\n * Inject the Fontshare brand-font stylesheet once. Idempotent and SSR-safe\n * (no-ops when `document` is unavailable). Called automatically on import;\n * exported for explicit invocation.\n */\nexport function injectFonts(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(LINK_ID)) return;\n const link = document.createElement(\"link\");\n link.id = LINK_ID;\n link.rel = \"stylesheet\";\n link.href = FONTSHARE_HREF;\n document.head.appendChild(link);\n}\n\ninjectFonts();\n"]} |
| /** | ||
| * Loads the NimbleBrain brand fonts into the current document. Import for the | ||
| * side effect: | ||
| * | ||
| * ```ts | ||
| * import "@nimblebrain/synapse/ui/fonts"; | ||
| * ``` | ||
| * | ||
| * The token contract names these fonts (`--font-sans: Satoshi`, | ||
| * `--nb-font-heading: Erode`), but a font *name* only renders if the font | ||
| * *files* are loaded in the iframe document — each app iframe is its own | ||
| * document and does not inherit the host's `@font-face`s. This module injects | ||
| * the Fontshare stylesheet so the named fonts resolve instead of silently | ||
| * falling back. | ||
| * | ||
| * JetBrains Mono (`--font-mono`) is not injected here — apps that render code | ||
| * should add `@fontsource-variable/jetbrains-mono` to their own bundle. | ||
| * | ||
| * CSP note: the host iframe must allow `https://api.fontshare.com` in its | ||
| * `style-src`/`font-src` for this to take effect. If blocked, fonts fall back | ||
| * to the neutral system stack in the token contract. | ||
| */ | ||
| /** | ||
| * Inject the Fontshare brand-font stylesheet once. Idempotent and SSR-safe | ||
| * (no-ops when `document` is unavailable). Called automatically on import; | ||
| * exported for explicit invocation. | ||
| */ | ||
| declare function injectFonts(): void; | ||
| export { injectFonts }; |
| /** | ||
| * Loads the NimbleBrain brand fonts into the current document. Import for the | ||
| * side effect: | ||
| * | ||
| * ```ts | ||
| * import "@nimblebrain/synapse/ui/fonts"; | ||
| * ``` | ||
| * | ||
| * The token contract names these fonts (`--font-sans: Satoshi`, | ||
| * `--nb-font-heading: Erode`), but a font *name* only renders if the font | ||
| * *files* are loaded in the iframe document — each app iframe is its own | ||
| * document and does not inherit the host's `@font-face`s. This module injects | ||
| * the Fontshare stylesheet so the named fonts resolve instead of silently | ||
| * falling back. | ||
| * | ||
| * JetBrains Mono (`--font-mono`) is not injected here — apps that render code | ||
| * should add `@fontsource-variable/jetbrains-mono` to their own bundle. | ||
| * | ||
| * CSP note: the host iframe must allow `https://api.fontshare.com` in its | ||
| * `style-src`/`font-src` for this to take effect. If blocked, fonts fall back | ||
| * to the neutral system stack in the token contract. | ||
| */ | ||
| /** | ||
| * Inject the Fontshare brand-font stylesheet once. Idempotent and SSR-safe | ||
| * (no-ops when `document` is unavailable). Called automatically on import; | ||
| * exported for explicit invocation. | ||
| */ | ||
| declare function injectFonts(): void; | ||
| export { injectFonts }; |
| // src/ui/fonts.ts | ||
| var FONTSHARE_HREF = "https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=erode@400,500&display=swap"; | ||
| var LINK_ID = "nb-synapse-fonts"; | ||
| function injectFonts() { | ||
| if (typeof document === "undefined") return; | ||
| if (document.getElementById(LINK_ID)) return; | ||
| const link = document.createElement("link"); | ||
| link.id = LINK_ID; | ||
| link.rel = "stylesheet"; | ||
| link.href = FONTSHARE_HREF; | ||
| document.head.appendChild(link); | ||
| } | ||
| injectFonts(); | ||
| export { injectFonts }; | ||
| //# sourceMappingURL=fonts.js.map | ||
| //# sourceMappingURL=fonts.js.map |
| {"version":3,"sources":["../../src/ui/fonts.ts"],"names":[],"mappings":";AAuBA,IAAM,cAAA,GACJ,yFAAA;AACF,IAAM,OAAA,GAAU,kBAAA;AAOT,SAAS,WAAA,GAAoB;AAClC,EAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AACrC,EAAA,IAAI,QAAA,CAAS,cAAA,CAAe,OAAO,CAAA,EAAG;AACtC,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,aAAA,CAAc,MAAM,CAAA;AAC1C,EAAA,IAAA,CAAK,EAAA,GAAK,OAAA;AACV,EAAA,IAAA,CAAK,GAAA,GAAM,YAAA;AACX,EAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,IAAI,CAAA;AAChC;AAEA,WAAA,EAAY","file":"fonts.js","sourcesContent":["/**\n * Loads the NimbleBrain brand fonts into the current document. Import for the\n * side effect:\n *\n * ```ts\n * import \"@nimblebrain/synapse/ui/fonts\";\n * ```\n *\n * The token contract names these fonts (`--font-sans: Satoshi`,\n * `--nb-font-heading: Erode`), but a font *name* only renders if the font\n * *files* are loaded in the iframe document — each app iframe is its own\n * document and does not inherit the host's `@font-face`s. This module injects\n * the Fontshare stylesheet so the named fonts resolve instead of silently\n * falling back.\n *\n * JetBrains Mono (`--font-mono`) is not injected here — apps that render code\n * should add `@fontsource-variable/jetbrains-mono` to their own bundle.\n *\n * CSP note: the host iframe must allow `https://api.fontshare.com` in its\n * `style-src`/`font-src` for this to take effect. If blocked, fonts fall back\n * to the neutral system stack in the token contract.\n */\n\nconst FONTSHARE_HREF =\n \"https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=erode@400,500&display=swap\";\nconst LINK_ID = \"nb-synapse-fonts\";\n\n/**\n * Inject the Fontshare brand-font stylesheet once. Idempotent and SSR-safe\n * (no-ops when `document` is unavailable). Called automatically on import;\n * exported for explicit invocation.\n */\nexport function injectFonts(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(LINK_ID)) return;\n const link = document.createElement(\"link\");\n link.id = LINK_ID;\n link.rel = \"stylesheet\";\n link.href = FONTSHARE_HREF;\n document.head.appendChild(link);\n}\n\ninjectFonts();\n"]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2003486
2.21%12868
1.39%535
10.77%98
-5.77%