@unoverse-platform/studio 0.1.103 → 0.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/package.json +4 -2
  2. package/screens/AtomsView.tsx +2 -1
  3. package/screens/ComponentsView.tsx +2 -1
  4. package/screens/NativeAppHost.tsx +1 -2
  5. package/screens/TemplatesLibraryView.tsx +14 -1
  6. package/screens/TemplatesView.tsx +2 -1
  7. package/screens/index.ts +4 -0
  8. package/screens/react/unoverse.tsx +281 -0
  9. package/vendor/base/src/agent-mcp/index.ts +1 -1
  10. package/vendor/base/src/agent-mcp/invoke.ts +6 -2
  11. package/vendor/base/src/agent-mcp/tools.ts +14 -5
  12. package/vendor/base/src/agent-mcp/types.ts +6 -0
  13. package/vendor/base/src/auth/auth.ts +90 -23
  14. package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
  15. package/vendor/base/src/definitions/load/compile/template.ts +23 -2
  16. package/vendor/base/src/definitions/model.ts +9 -0
  17. package/vendor/base/src/definitions/registry/catalog.ts +6 -7
  18. package/vendor/base/src/executor.ts +25 -1
  19. package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
  20. package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
  21. package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
  22. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
  23. package/vendor/base/src/platform/serviceCalls.ts +13 -0
  24. package/vendor/sdk/appEngine.ts +363 -0
  25. package/vendor/sdk/lib/actions.ts +36 -0
  26. package/vendor/sdk/lib/citations.ts +48 -0
  27. package/vendor/sdk/lib/core/apps.ts +18 -0
  28. package/vendor/sdk/lib/core/client.ts +3 -3
  29. package/vendor/sdk/lib/core/store.ts +6 -0
  30. package/vendor/sdk/lib/dom/app.ts +546 -0
  31. package/vendor/sdk/lib/dom/chrome.ts +125 -0
  32. package/vendor/sdk/lib/dom/component.ts +333 -0
  33. package/vendor/sdk/lib/dom/connection.ts +309 -0
  34. package/vendor/sdk/lib/dom/element.ts +69 -0
  35. package/vendor/sdk/lib/dom/isolate.ts +37 -0
  36. package/vendor/sdk/lib/dom/markdown.ts +388 -0
  37. package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
  38. package/vendor/sdk/lib/dom/mounted.ts +29 -0
  39. package/vendor/sdk/lib/dom/primitives.ts +689 -0
  40. package/vendor/sdk/lib/dom/region.ts +195 -0
  41. package/vendor/sdk/lib/dom/render.ts +177 -0
  42. package/vendor/sdk/lib/dom/theme.ts +38 -0
  43. package/vendor/sdk/lib/dom/types.ts +36 -0
  44. package/vendor/sdk/lib/index.ts +26 -17
  45. package/vendor/sdk/lib/orb/canvas.ts +139 -0
  46. package/vendor/sdk/lib/orb/index.ts +8 -18
  47. package/vendor/sdk/lib/orb/presets.ts +1 -1
  48. package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
  49. package/vendor/sdk/lib/realtime/capture.ts +152 -0
  50. package/vendor/sdk/lib/realtime/playback.ts +129 -0
  51. package/vendor/sdk/lib/realtime/socket.ts +117 -0
  52. package/vendor/sdk/lib/style.ts +45 -37
  53. package/vendor/sdk/lib/voiceService.ts +344 -0
  54. package/vendor/sdk/main.ts +107 -0
  55. package/vendor/sdk/mcpAppBridge.ts +125 -102
  56. package/vendor/sdk/pageInterfaces.ts +452 -0
  57. package/vendor/sdk/responseView.ts +193 -0
  58. package/vite.config.ts +3 -3
  59. package/vendor/sdk/appEngine.tsx +0 -395
  60. package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
  61. package/vendor/sdk/lib/app.tsx +0 -730
  62. package/vendor/sdk/lib/connection.tsx +0 -433
  63. package/vendor/sdk/lib/isolate.tsx +0 -70
  64. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
  65. package/vendor/sdk/lib/orb/theme.ts +0 -83
  66. package/vendor/sdk/lib/primitives.tsx +0 -943
  67. package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
  68. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
  69. package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
  70. package/vendor/sdk/lib/render.tsx +0 -244
  71. package/vendor/sdk/lib/streamed.tsx +0 -30
  72. package/vendor/sdk/lib/theme.ts +0 -55
  73. package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
  74. package/vendor/sdk/lib/transition/index.ts +0 -9
  75. package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
  76. package/vendor/sdk/lib/voice.tsx +0 -422
  77. package/vendor/sdk/main.tsx +0 -125
  78. package/vendor/sdk/pageInterfaces.tsx +0 -388
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -1,24 +1,15 @@
1
1
  /**
2
- * The MCP Apps host bridge (SEP-1865, spec 2026-01-26) — the STANDARD door.
2
+ * The MCP Apps host bridge, on the official MCP Apps SDK (`@modelcontextprotocol/ext-apps`).
3
3
  *
4
4
  * An MCP Apps host (ChatGPT, Claude Desktop, Goose, …) renders this bundle in a sandboxed
5
- * iframe and speaks JSON-RPC over postMessage: the app sends `ui/initialize`, the host
6
- * answers with `hostContext`, then pushes `ui/notifications/tool-input` and
7
- * `ui/notifications/tool-result`. The RESULT notification carries the tool call's full
8
- * `CallToolResult` — content, structuredContent and `_meta` — and `_meta` is where our
5
+ * iframe. The SDK's `App` speaks the protocol: `ui/initialize`, the tool notifications, the
6
+ * host context, display modes and size. The RESULT carries the tool call's `_meta`, where our
9
7
  * server puts the boot facts (`unoverse/serverUrl`, `unoverse/app`, `unoverse/token`).
10
- * This is the lane that still works everywhere: ChatGPT's legacy `window.openai` globals
11
- * now arrive with `toolResponseMetadata` EMPTY (observed live 2026-08-13), and non-OpenAI
12
- * hosts never had them.
13
8
  *
14
- * Deliberately MINIMAL: initialize + the two tool notifications. No display-mode calls, no
15
- * host-proxied tools/call — the SDK keeps its own /mcp + /stream connections (the CSP's
16
- * connectDomains exist exactly for this). Extend only against the spec:
17
- * https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
9
+ * What stays ours: the boot callback, the answer handed back to the host's model, and the
10
+ * mapping from a task's own state to the host's display mode.
18
11
  */
19
-
20
- /** Spec revision we speak. Hosts negotiate; the field is required on ui/initialize. */
21
- const UI_PROTOCOL_VERSION = "2026-01-26";
12
+ import { App } from "@modelcontextprotocol/ext-apps";
22
13
 
23
14
  type ToolResult = {
24
15
  content?: unknown;
@@ -27,109 +18,141 @@ type ToolResult = {
27
18
  };
28
19
 
29
20
  export type McpAppHost = {
30
- /** True once the host answered ui/initialize — the signal that a STANDARD host owns this
31
- * iframe, used to silence the `unoverse:*` embed-host messages (a standard host validates
32
- * every inbound postMessage as JSON-RPC and logs ours as errors). */
21
+ /** True once the host answered ui/initialize: a standard host owns this iframe, so the
22
+ * `unoverse:*` embed-host messages are not posted at it. */
33
23
  connected: boolean;
34
- hostContext?: Record<string, unknown>;
24
+ /** The answer keys the tool result named (`structuredContent.asks`). */
25
+ asks?: string[];
35
26
  };
36
27
 
37
- /** Whether ANY MCP app host owns this iframe — standard bridge or legacy window.openai.
38
- * The embed-host messages (`unoverse:status`/`size`/`event`/`close`) are for OUR OWN
39
- * parent page and must not be posted at an MCP host. */
28
+ const bridge: McpAppHost = { connected: false };
29
+
30
+ const app = new App(
31
+ { name: "unoverse-websdk", version: "0.0.1" },
32
+ // The display modes this window can show (the spec's MUST).
33
+ { availableDisplayModes: ["inline", "fullscreen"] },
34
+ // The SDK reports the content's size to the host, so the frame fits it (spec: size-changed).
35
+ { autoResize: true },
36
+ );
37
+
38
+ /** Whether ANY MCP app host owns this iframe. The embed-host messages are for OUR OWN parent page. */
40
39
  export function hasMcpHost(): boolean {
41
40
  return bridge.connected || Boolean((window as { openai?: unknown }).openai);
42
41
  }
43
42
 
44
- const bridge: McpAppHost = { connected: false };
45
-
46
- /** Post an `unoverse:*` message to OUR OWN embed host's parent page (status badge, size,
47
- * close, analytics). At an MCP host these are noise — the sandbox validates every inbound
48
- * postMessage as JSON-RPC and logs each of ours as an error (observed live in ChatGPT) —
49
- * so the post is skipped entirely there. `window.openai` counts as host detection ONLY;
50
- * no config is read from it (the legacy boot path was removed 2026-08-13). */
43
+ /** Post an `unoverse:*` message to OUR OWN embed host's parent page; skipped at an MCP host. */
51
44
  export function postToEmbedHost(msg: Record<string, unknown>): void {
52
45
  if (hasMcpHost()) return;
53
46
  window.parent?.postMessage(msg, "*");
54
47
  }
55
48
 
49
+ const modeListeners = new Set<(mode: string) => void>();
50
+ const currentMode = () => String(app.getHostContext()?.displayMode ?? "inline");
51
+
56
52
  /**
57
- * Open the standard door: send ui/initialize, resolve boot facts from the first
58
- * tool-result. `onToolResult` fires for EVERY result the host pushes (the first one
53
+ * THE HOST'S ROOM FOR THIS WINDOW, by where the host shows it: in `fullscreen` (ChatGPT's side
54
+ * panel) the page fills the window; in the chat it fits its content, up to the host's
55
+ * `maxHeight`, and `autoResize` reports that height. Decided by the display mode, never by a
56
+ * fixed `height` in containerDimensions: a page held at the window's height cannot be
57
+ * measured smaller (`autoResize` watches only <html> and <body>), so a task closing back to
58
+ * its chip kept the panel's height in the chat.
59
+ */
60
+ function applyContainer(): void {
61
+ const dim = app.getHostContext()?.containerDimensions as Record<string, unknown> | undefined;
62
+ const html = document.documentElement;
63
+ if (currentMode() === "fullscreen") {
64
+ html.style.height = "100vh";
65
+ html.style.maxHeight = "";
66
+ } else {
67
+ html.style.height = "auto";
68
+ html.style.maxHeight = typeof dim?.maxHeight === "number" ? `${dim.maxHeight}px` : "";
69
+ }
70
+ }
71
+
72
+ /** The window's room follows its mode, then everyone who follows the mode hears it. */
73
+ const announceMode = () => {
74
+ applyContainer();
75
+ modeListeners.forEach((fn) => fn(currentMode()));
76
+ };
77
+
78
+ /** Where an MCP Apps host shows this window (`inline`, `fullscreen`), or null when no host has
79
+ * connected (our own pages), where there is no mode to follow. */
80
+ export function hostMode(): string | null {
81
+ return bridge.connected ? currentMode() : null;
82
+ }
83
+
84
+ /** Where the host is showing this window now, and every later move. */
85
+ export function onDisplayMode(fn: (mode: string) => void): () => void {
86
+ modeListeners.add(fn);
87
+ fn(currentMode());
88
+ return () => {
89
+ modeListeners.delete(fn);
90
+ };
91
+ }
92
+
93
+ /**
94
+ * SHOW THE WINDOW THE WAY THE TASK IS: its `inline` face in the chat, any other face (the
95
+ * task open) in the host's `fullscreen`, which on ChatGPT Desktop is the side panel. Only a
96
+ * mode the host offers, and only on a change. Resolves to the mode the host GRANTED (the spec:
97
+ * the host answers with the mode it chose), or undefined when nothing was asked.
98
+ */
99
+ export function showAs(view: string): Promise<string | undefined> {
100
+ if (!bridge.connected) return Promise.resolve(undefined);
101
+ const mode = view === "inline" ? "inline" : "fullscreen";
102
+ if (!app.getHostContext()?.availableDisplayModes?.includes(mode) || currentMode() === mode) return Promise.resolve(undefined);
103
+ console.info(`[unoverse:mode] ask ${mode} (showing ${currentMode()})`);
104
+ return app.requestDisplayMode({ mode }).then(
105
+ (r) => {
106
+ console.info(`[unoverse:mode] answer ${String((r as { mode?: unknown })?.mode)} (showing ${currentMode()})`);
107
+ announceMode();
108
+ return String((r as { mode?: unknown })?.mode ?? currentMode());
109
+ },
110
+ (e) => {
111
+ console.info(`[unoverse:mode] refused ${mode}: ${String((e as Error)?.message ?? e)} (showing ${currentMode()})`);
112
+ return undefined;
113
+ },
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Open the standard door. `onToolResult` fires for EVERY result the host pushes (the first one
59
119
  * carries the boot facts; later ones are re-renders of the same app instance).
60
120
  */
61
121
  export function connectMcpAppHost(onToolResult: (r: ToolResult) => void): McpAppHost {
62
- const INIT_ID = "unoverse-ui-init";
63
- // DIAGNOSTIC-FIRST (main.tsx doctrine): we implement the SPEC's dialect, but the live
64
- // hosts are the ground truth — log a compact line for EVERY inbound message and every
65
- // handshake attempt so one browser reproduction shows exactly what the host speaks.
66
- const log = (label: string, extra?: unknown) =>
67
- // eslint-disable-next-line no-console
68
- console.log(`[unoverse:bridge] ${label}`, extra ?? "");
69
- window.addEventListener("message", (e: MessageEvent) => {
70
- const m = e?.data as
71
- | { jsonrpc?: string; id?: unknown; result?: unknown; error?: unknown; method?: string; params?: unknown }
72
- | undefined;
73
- if (m && typeof m === "object") {
74
- log("inbound", {
75
- jsonrpc: m.jsonrpc,
76
- method: m.method,
77
- id: m.id,
78
- // FULL bodies, stringified: the first diagnostics run summarized these to booleans
79
- // and the one fact that mattered — WHY the host rejected ui/initialize — was in the
80
- // unprinted error object. Never summarize the field you are debugging.
81
- result: m.result !== undefined ? JSON.stringify(m.result).slice(0, 600) : undefined,
82
- error: m.error !== undefined ? JSON.stringify(m.error).slice(0, 600) : undefined,
83
- params: m.params !== undefined ? JSON.stringify(m.params).slice(0, 300) : undefined,
84
- ports: e.ports?.length ?? 0,
85
- origin: e.origin,
86
- });
87
- }
88
- if (m?.jsonrpc !== "2.0") return; // not the host bridge (our own unoverse:config etc.)
89
- if (m.id === INIT_ID && m.result) {
90
- bridge.connected = true;
91
- bridge.hostContext = (m.result as { hostContext?: Record<string, unknown> }).hostContext;
92
- log("ui/initialize answered", bridge.hostContext);
93
- // Complete the handshake — the reference App sends this notification after the
94
- // result (ext-apps src/app.ts `connect`); hosts may hold tool-input/result until it.
95
- window.parent?.postMessage({ jsonrpc: "2.0", method: "ui/notifications/initialized" }, "*");
96
- return;
97
- }
98
- // Answer pings so the host's liveness check never times the app out.
99
- if (m.method === "ping" && m.id !== undefined) {
100
- (e.source as WindowProxy | null)?.postMessage({ jsonrpc: "2.0", id: m.id, result: {} }, "*");
101
- return;
102
- }
103
- if (m.method === "ui/notifications/tool-result") {
104
- // Per spec the result rides params (CallToolResult); tolerate a bare shape.
105
- const p = (m.params ?? {}) as { result?: ToolResult } & ToolResult;
106
- log("tool-result", { metaKeys: Object.keys((p.result ?? p)._meta ?? {}) });
107
- onToolResult(p.result ?? p);
108
- }
109
- });
110
- const sendInit = () => {
111
- if (bridge.connected) return;
112
- log("ui/initialize →");
113
- window.parent?.postMessage(
114
- {
115
- jsonrpc: "2.0",
116
- id: INIT_ID,
117
- method: "ui/initialize",
118
- // Field names per the REFERENCE App (ext-apps src/app.ts), not the docs' prose:
119
- // `appInfo` + `appCapabilities`. The first cut sent MCP-core's `clientInfo` +
120
- // `capabilities` and ChatGPT's Zod schema answered -32603 "Invalid input".
121
- params: {
122
- protocolVersion: UI_PROTOCOL_VERSION,
123
- appCapabilities: {},
124
- appInfo: { name: "unoverse-websdk", version: "0.0.1" },
125
- },
126
- },
127
- "*",
128
- );
122
+ // Handlers BEFORE connect: the SDK delivers the tool notifications once.
123
+ app.ontoolresult = (result) => {
124
+ const r = result as ToolResult;
125
+ const asks = (r.structuredContent as { asks?: unknown } | undefined)?.asks;
126
+ bridge.asks = Array.isArray(asks) && asks.every((k) => typeof k === "string") ? (asks as string[]) : undefined;
127
+ onToolResult(r);
128
+ };
129
+ app.onhostcontextchanged = (change) => {
130
+ // WHAT THE HOST REPORTED, as it reported it: the record to read before reasoning about modes.
131
+ if ("displayMode" in (change ?? {})) console.info(`[unoverse:mode] host says ${String((change as { displayMode?: unknown }).displayMode)}`);
132
+ announceMode();
129
133
  };
130
- // The host's listener can attach after our module runs (web hosts boot a sandbox proxy
131
- // first — `ui/notifications/sandbox-*`), so retry until answered.
132
- sendInit();
133
- for (const ms of [250, 1000, 3000]) setTimeout(sendInit, ms);
134
+ app.connect().then(
135
+ () => {
136
+ bridge.connected = true;
137
+ announceMode();
138
+ },
139
+ // Not inside an MCP Apps host (our own embed and pages): nothing to connect to.
140
+ () => {},
141
+ );
134
142
  return bridge;
135
143
  }
144
+
145
+ /**
146
+ * THE PERSON'S ANSWER, BACK TO THE HOST'S MODEL (`ui/update-model-context` + `ui/message`). An
147
+ * outside host draws the app only after its tool call returns, so the call cannot wait for the
148
+ * answer; the server named the keys in `asks`. True when it was handed back.
149
+ */
150
+ export function handAnswerToHost(state: Record<string, unknown>): boolean {
151
+ if (!bridge.connected || !bridge.asks?.length) return false;
152
+ const answer = Object.fromEntries(bridge.asks.filter((k) => state[k] !== undefined).map((k) => [k, state[k]]));
153
+ if (!Object.keys(answer).length) return false;
154
+ const lines = Object.entries(answer).map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
155
+ void app.updateModelContext({ structuredContent: answer });
156
+ void app.sendMessage({ role: "user", content: [{ type: "text", text: `My answer:\n${lines.join("\n")}` }] });
157
+ return true;
158
+ }