@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.
- package/package.json +4 -2
- package/screens/AtomsView.tsx +2 -1
- package/screens/ComponentsView.tsx +2 -1
- package/screens/NativeAppHost.tsx +1 -2
- package/screens/TemplatesLibraryView.tsx +14 -1
- package/screens/TemplatesView.tsx +2 -1
- package/screens/index.ts +4 -0
- package/screens/react/unoverse.tsx +281 -0
- package/vendor/base/src/agent-mcp/index.ts +1 -1
- package/vendor/base/src/agent-mcp/invoke.ts +6 -2
- package/vendor/base/src/agent-mcp/tools.ts +14 -5
- package/vendor/base/src/agent-mcp/types.ts +6 -0
- package/vendor/base/src/auth/auth.ts +90 -23
- package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
- package/vendor/base/src/definitions/load/compile/template.ts +23 -2
- package/vendor/base/src/definitions/model.ts +9 -0
- package/vendor/base/src/definitions/registry/catalog.ts +6 -7
- package/vendor/base/src/executor.ts +25 -1
- package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
- package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
- package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
- package/vendor/base/src/platform/serviceCalls.ts +13 -0
- package/vendor/sdk/appEngine.ts +363 -0
- package/vendor/sdk/lib/actions.ts +36 -0
- package/vendor/sdk/lib/citations.ts +48 -0
- package/vendor/sdk/lib/core/apps.ts +18 -0
- package/vendor/sdk/lib/core/client.ts +3 -3
- package/vendor/sdk/lib/core/store.ts +6 -0
- package/vendor/sdk/lib/dom/app.ts +546 -0
- package/vendor/sdk/lib/dom/chrome.ts +125 -0
- package/vendor/sdk/lib/dom/component.ts +333 -0
- package/vendor/sdk/lib/dom/connection.ts +309 -0
- package/vendor/sdk/lib/dom/element.ts +69 -0
- package/vendor/sdk/lib/dom/isolate.ts +37 -0
- package/vendor/sdk/lib/dom/markdown.ts +388 -0
- package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
- package/vendor/sdk/lib/dom/mounted.ts +29 -0
- package/vendor/sdk/lib/dom/primitives.ts +689 -0
- package/vendor/sdk/lib/dom/region.ts +195 -0
- package/vendor/sdk/lib/dom/render.ts +177 -0
- package/vendor/sdk/lib/dom/theme.ts +38 -0
- package/vendor/sdk/lib/dom/types.ts +36 -0
- package/vendor/sdk/lib/index.ts +26 -17
- package/vendor/sdk/lib/orb/canvas.ts +139 -0
- package/vendor/sdk/lib/orb/index.ts +8 -18
- package/vendor/sdk/lib/orb/presets.ts +1 -1
- package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
- package/vendor/sdk/lib/realtime/capture.ts +152 -0
- package/vendor/sdk/lib/realtime/playback.ts +129 -0
- package/vendor/sdk/lib/realtime/socket.ts +117 -0
- package/vendor/sdk/lib/style.ts +45 -37
- package/vendor/sdk/lib/voiceService.ts +344 -0
- package/vendor/sdk/main.ts +107 -0
- package/vendor/sdk/mcpAppBridge.ts +125 -102
- package/vendor/sdk/pageInterfaces.ts +452 -0
- package/vendor/sdk/responseView.ts +193 -0
- package/vite.config.ts +3 -3
- package/vendor/sdk/appEngine.tsx +0 -395
- package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
- package/vendor/sdk/lib/app.tsx +0 -730
- package/vendor/sdk/lib/connection.tsx +0 -433
- package/vendor/sdk/lib/isolate.tsx +0 -70
- package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
- package/vendor/sdk/lib/orb/theme.ts +0 -83
- package/vendor/sdk/lib/primitives.tsx +0 -943
- package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
- package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
- package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
- package/vendor/sdk/lib/render.tsx +0 -244
- package/vendor/sdk/lib/streamed.tsx +0 -30
- package/vendor/sdk/lib/theme.ts +0 -55
- package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
- package/vendor/sdk/lib/transition/index.ts +0 -9
- package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
- package/vendor/sdk/lib/voice.tsx +0 -422
- package/vendor/sdk/main.tsx +0 -125
- package/vendor/sdk/pageInterfaces.tsx +0 -388
- package/vendor/sdk/responseView.tsx +0 -82
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The MCP Apps host bridge
|
|
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
|
|
6
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
|
31
|
-
*
|
|
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
|
-
|
|
24
|
+
/** The answer keys the tool result named (`structuredContent.asks`). */
|
|
25
|
+
asks?: string[];
|
|
35
26
|
};
|
|
36
27
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
58
|
-
*
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|