@ggui-ai/iframe-runtime 0.9.0 → 0.10.0
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/dist/compiled-validators.d.ts +16 -7
- package/dist/compiled-validators.js +41 -8
- package/dist/iframe-runtime.js +58 -57
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/meta-parse.d.ts +13 -0
- package/dist/meta-parse.js +28 -0
- package/dist/react-renderer.d.ts +12 -0
- package/dist/react-renderer.js +47 -5
- package/dist/render-item.d.ts +9 -0
- package/dist/render-item.js +6 -0
- package/dist/runtime.d.ts +84 -0
- package/dist/runtime.js +197 -30
- package/dist/server.d.ts +19 -0
- package/dist/server.js +19 -0
- package/dist/shims/app-components.js +1 -0
- package/dist/shims/components.js +1 -0
- package/dist/shims/compositions.js +1 -0
- package/dist/shims/design.js +1 -0
- package/dist/shims/gadgets.js +1 -0
- package/dist/shims/interact.js +1 -0
- package/dist/shims/jsx-runtime.js +1 -0
- package/dist/shims/primitives.js +1 -0
- package/dist/shims/react-dom.js +1 -0
- package/dist/shims/react.js +1 -0
- package/dist/shims/templates.js +1 -0
- package/dist/shims/tokens.js +1 -0
- package/dist/shims/wire.js +1 -0
- package/dist/system-cards/ProtocolProbeCard.js +1 -1
- package/dist/zod-jitless.d.ts +1 -0
- package/dist/zod-jitless.js +18 -0
- package/package.json +8 -7
package/dist/meta-parse.d.ts
CHANGED
|
@@ -30,6 +30,19 @@ export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGgui
|
|
|
30
30
|
* so the runtime can mount without any postMessage round-trip.
|
|
31
31
|
*/
|
|
32
32
|
export declare function parseMetaFromGlobal(): McpAppAiGguiMetaParseResult;
|
|
33
|
+
/**
|
|
34
|
+
* The `ui://ggui/render/…` LOCATOR a tool result publishes when a server
|
|
35
|
+
* runs the read-plane-only posture (`withholdResultMeta`, guuey#209 C3 /
|
|
36
|
+
* ggui#537): the bootstrap material is absent, but the view's durable
|
|
37
|
+
* identity rides `structuredContent.resourceUri` (ggui-aware hosts) and
|
|
38
|
+
* the spec-canonical `_meta.ui.resourceUri` (MCP Apps hosts) — same
|
|
39
|
+
* value, either slot. The runtime resolves such a result through the
|
|
40
|
+
* read-plane door (`app.readServerResource` → the per-render
|
|
41
|
+
* self-contained shell → its inlined envelope). `null` for anything else
|
|
42
|
+
* — a result that carries the slice inline never needs this, and a
|
|
43
|
+
* result with neither is not a view.
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractLocatorFromToolResult(params: unknown): string | null;
|
|
33
46
|
/**
|
|
34
47
|
* Parse the render slice from a `ui/notifications/tool-result`
|
|
35
48
|
* postMessage params payload. `params` is a `CallToolResult` per the
|
package/dist/meta-parse.js
CHANGED
|
@@ -274,6 +274,11 @@ function projectMeta(meta, hasStaticContent) {
|
|
|
274
274
|
: {}),
|
|
275
275
|
...(meta.codeUrl !== undefined ? { codeUrl: meta.codeUrl } : {}),
|
|
276
276
|
...(meta.codeHash !== undefined ? { codeHash: meta.codeHash } : {}),
|
|
277
|
+
// Strict-CSP module-variant twin (ggui#522 slice 2). The protocol
|
|
278
|
+
// parser already enforces the with-a-raw-carrier pairing.
|
|
279
|
+
...(meta.codeModuleUrl !== undefined
|
|
280
|
+
? { codeModuleUrl: meta.codeModuleUrl }
|
|
281
|
+
: {}),
|
|
277
282
|
...(meta.codeB64 !== undefined ? { codeB64: meta.codeB64 } : {}),
|
|
278
283
|
...(meta.kind !== undefined ? { kind: meta.kind } : {}),
|
|
279
284
|
};
|
|
@@ -349,6 +354,29 @@ export function parseMetaFromGlobal() {
|
|
|
349
354
|
}
|
|
350
355
|
return validateMeta(parsed.meta);
|
|
351
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* The `ui://ggui/render/…` LOCATOR a tool result publishes when a server
|
|
359
|
+
* runs the read-plane-only posture (`withholdResultMeta`, guuey#209 C3 /
|
|
360
|
+
* ggui#537): the bootstrap material is absent, but the view's durable
|
|
361
|
+
* identity rides `structuredContent.resourceUri` (ggui-aware hosts) and
|
|
362
|
+
* the spec-canonical `_meta.ui.resourceUri` (MCP Apps hosts) — same
|
|
363
|
+
* value, either slot. The runtime resolves such a result through the
|
|
364
|
+
* read-plane door (`app.readServerResource` → the per-render
|
|
365
|
+
* self-contained shell → its inlined envelope). `null` for anything else
|
|
366
|
+
* — a result that carries the slice inline never needs this, and a
|
|
367
|
+
* result with neither is not a view.
|
|
368
|
+
*/
|
|
369
|
+
export function extractLocatorFromToolResult(params) {
|
|
370
|
+
if (!isPlainObject(params))
|
|
371
|
+
return null;
|
|
372
|
+
const sc = params['structuredContent'];
|
|
373
|
+
const fromStructured = isPlainObject(sc) && typeof sc['resourceUri'] === 'string' ? sc['resourceUri'] : null;
|
|
374
|
+
const meta = params['_meta'];
|
|
375
|
+
const ui = isPlainObject(meta) ? meta['ui'] : undefined;
|
|
376
|
+
const fromUi = isPlainObject(ui) && typeof ui['resourceUri'] === 'string' ? ui['resourceUri'] : null;
|
|
377
|
+
const uri = fromStructured ?? fromUi;
|
|
378
|
+
return uri !== null && uri.startsWith('ui://ggui/render/') ? uri : null;
|
|
379
|
+
}
|
|
352
380
|
/**
|
|
353
381
|
* Parse the render slice from a `ui/notifications/tool-result`
|
|
354
382
|
* postMessage params payload. `params` is a `CallToolResult` per the
|
package/dist/react-renderer.d.ts
CHANGED
|
@@ -41,6 +41,18 @@ export interface ReactRootMountOptions {
|
|
|
41
41
|
readonly id?: string;
|
|
42
42
|
readonly componentCode: string;
|
|
43
43
|
readonly props?: Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Strict-CSP module variant of `componentCode` (ggui#522 slice 2):
|
|
46
|
+
* a server-side import-rewritten twin of the SAME bytes, directly
|
|
47
|
+
* `import()`able (its imports resolve to static shim assets on an
|
|
48
|
+
* allowed origin — no `data:`, no `blob:`, no eval). Tried FIRST;
|
|
49
|
+
* any failure falls through to the blob/inline ladder on the raw
|
|
50
|
+
* bytes. Callers MUST pass it only when it names exactly these
|
|
51
|
+
* `componentCode` bytes — a stale variant would paint the WRONG
|
|
52
|
+
* component, not fail (the runtime's dispatcher guards this by
|
|
53
|
+
* attaching it to the inline seed only).
|
|
54
|
+
*/
|
|
55
|
+
readonly codeModuleUrl?: string;
|
|
44
56
|
};
|
|
45
57
|
readonly themeId?: string;
|
|
46
58
|
/**
|
package/dist/react-renderer.js
CHANGED
|
@@ -235,6 +235,30 @@ export async function mountReactRoot(container, opts) {
|
|
|
235
235
|
? { gadgetPackages: currentOpts.gadgetPackages }
|
|
236
236
|
: {}),
|
|
237
237
|
}));
|
|
238
|
+
// Asset-module path (ggui#522 slice 2) — FIRST when the mount
|
|
239
|
+
// carries the strict-CSP variant URL: a plain https module import,
|
|
240
|
+
// legal under `script-src` origins alone, no scheme sources, no
|
|
241
|
+
// eval. Under a strict host CSP it is the ONLY path that works;
|
|
242
|
+
// under a permissive one it costs a CDN round-trip on cold cache
|
|
243
|
+
// and nothing on warm (the URL is immutable). Any failure — CSP
|
|
244
|
+
// block, network fault, a 404 from a redeployed variant family —
|
|
245
|
+
// falls through to the raw-bytes ladder below. No document latch
|
|
246
|
+
// on failure: unlike the per-evaluation blob probe, this branch
|
|
247
|
+
// runs only when componentCode changes (rare — usually once per
|
|
248
|
+
// mount), so the retry cost is one fetch, and the failure classes
|
|
249
|
+
// (transient network vs permanent CSP) are indistinguishable in
|
|
250
|
+
// the import error anyway.
|
|
251
|
+
const moduleUrl = currentOpts.render.codeModuleUrl;
|
|
252
|
+
if (moduleUrl !== undefined) {
|
|
253
|
+
try {
|
|
254
|
+
const mod = (await import(
|
|
255
|
+
/* webpackIgnore: true */ /* @vite-ignore */ moduleUrl));
|
|
256
|
+
return pick(mod);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Fall through — the raw bytes are the authoritative carrier.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
238
262
|
// Once URL-scheme module loading has been proven blocked in this
|
|
239
263
|
// document, go straight to inline execution — the CSP verdict
|
|
240
264
|
// cannot change within the document's lifetime.
|
|
@@ -278,9 +302,25 @@ export async function mountReactRoot(container, opts) {
|
|
|
278
302
|
}
|
|
279
303
|
}
|
|
280
304
|
function renderTree(opts) {
|
|
281
|
-
|
|
305
|
+
let themeCss = opts.themeId
|
|
282
306
|
? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
|
|
283
307
|
: getScopedCssTokens(scopeClass, opts.themeMode);
|
|
308
|
+
if (opts.appTheme) {
|
|
309
|
+
const decls = Object.entries(opts.appTheme.cssVariables)
|
|
310
|
+
.map(([k, v]) => `${k}: ${v};`)
|
|
311
|
+
.join('');
|
|
312
|
+
// The overlay must be merged HERE, into the scoped style that
|
|
313
|
+
// mounts INSIDE the scope div — the `:root` append below is
|
|
314
|
+
// cascade-dead for tree content: the in-scope block sits later
|
|
315
|
+
// in document order than anything in `<head>`, so at equal
|
|
316
|
+
// specificity the base scoped tokens always win over a head-level
|
|
317
|
+
// override (browser-verified in rnd/gen-ui/beauty/experiments/001,
|
|
318
|
+
// the probe sequence that found this). Appended AFTER the base
|
|
319
|
+
// block and BEFORE `cssOverrides`, so: base < operator overlay <
|
|
320
|
+
// explicit overrides. Values validated injection-safe upstream
|
|
321
|
+
// (same guarantee the `:root` append cites).
|
|
322
|
+
themeCss += `.${scopeClass}{${decls}}`;
|
|
323
|
+
}
|
|
284
324
|
// Also inject theme CSS at `:root` on `document.head`. The scoped
|
|
285
325
|
// injection above isolates token resolution to the React tree, but
|
|
286
326
|
// the embedding shell's body styles (font-family, color, background)
|
|
@@ -298,10 +338,12 @@ export async function mountReactRoot(container, opts) {
|
|
|
298
338
|
const decls = Object.entries(opts.appTheme.cssVariables)
|
|
299
339
|
.map(([k, v]) => `${k}: ${v};`)
|
|
300
340
|
.join('');
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
341
|
+
// BODY-CHROME ONLY. This `:root` append themes the embedding
|
|
342
|
+
// shell's body styles (font/color/background outside the scope
|
|
343
|
+
// div). It does NOT reach tree content — the scoped in-body
|
|
344
|
+
// token block wins by document order (see the scoped merge in
|
|
345
|
+
// `renderTree`, and rnd/gen-ui/beauty/experiments/001 for the
|
|
346
|
+
// probe that falsified the old "inherits into the tree" claim).
|
|
305
347
|
// Values were validated injection-safe upstream (write-side
|
|
306
348
|
// `appThemeSchema` + the wire parser re-validates), so the
|
|
307
349
|
// string-join here is safe — do NOT re-sanitize.
|
package/dist/render-item.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ export interface RenderItemOptions {
|
|
|
29
29
|
* this. STDLIB `@ggui-ai/gadgets` is always rewritten regardless.
|
|
30
30
|
*/
|
|
31
31
|
readonly gadgetPackages?: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Strict-CSP module variant of the render's `componentCode`
|
|
34
|
+
* (ggui#522 slice 2), forwarded to
|
|
35
|
+
* {@link ReactRootMountOptions.render.codeModuleUrl}. The CALLER
|
|
36
|
+
* owns the bytes↔URL association: pass it only when it names
|
|
37
|
+
* exactly `render.componentCode`'s bytes (the runtime attaches it
|
|
38
|
+
* to the inline seed mount only — a WS-delivered render drops it).
|
|
39
|
+
*/
|
|
40
|
+
readonly codeModuleUrl?: string;
|
|
32
41
|
/** Theme id passed to ReactComponentRenderer for scoped CSS. */
|
|
33
42
|
readonly themeId?: string;
|
|
34
43
|
/**
|
package/dist/render-item.js
CHANGED
|
@@ -169,6 +169,9 @@ export async function mountRender(container, opts) {
|
|
|
169
169
|
...(render.props !== undefined
|
|
170
170
|
? { props: render.props }
|
|
171
171
|
: {}),
|
|
172
|
+
...(currentOpts.codeModuleUrl !== undefined
|
|
173
|
+
? { codeModuleUrl: currentOpts.codeModuleUrl }
|
|
174
|
+
: {}),
|
|
172
175
|
},
|
|
173
176
|
...(gadgetPackages !== undefined ? { gadgetPackages } : {}),
|
|
174
177
|
...(currentOpts.themeId !== undefined ? { themeId: currentOpts.themeId } : {}),
|
|
@@ -234,6 +237,9 @@ export async function mountRender(container, opts) {
|
|
|
234
237
|
...(render.props !== undefined
|
|
235
238
|
? { props: render.props }
|
|
236
239
|
: {}),
|
|
240
|
+
...(next.codeModuleUrl !== undefined
|
|
241
|
+
? { codeModuleUrl: next.codeModuleUrl }
|
|
242
|
+
: {}),
|
|
237
243
|
},
|
|
238
244
|
...(next.themeId !== undefined ? { themeId: next.themeId } : {}),
|
|
239
245
|
...(next.themeMode !== undefined ? { themeMode: next.themeMode } : {}),
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/iframe-runtime` iframe runtime entry.
|
|
3
|
+
*
|
|
4
|
+
* This is the file esbuild bundles into `dist/iframe-runtime.js`. The
|
|
5
|
+
* thin-shell HTML loads it via `<script type="module" src=".../iframe-runtime.js">`;
|
|
6
|
+
* on import the side-effects below take over: build a status DOM,
|
|
7
|
+
* postMessage `ui/initialize` to the parent, parse the bootstrap, open
|
|
8
|
+
* the WebSocket, run the version handshake, and mount the render
|
|
9
|
+
* placeholder.
|
|
10
|
+
*
|
|
11
|
+
* Boot sequence:
|
|
12
|
+
* - Boot from the `_meta["ai.ggui/render"]` slice received via
|
|
13
|
+
* `ui/initialize`.
|
|
14
|
+
* - Open the WebSocket, run the subscribe handshake.
|
|
15
|
+
* - Mount the render — either a structural placeholder or, when
|
|
16
|
+
* the renderer hooks are wired, a React mount of `componentCode`.
|
|
17
|
+
*
|
|
18
|
+
* The runtime advertises its build version via a post-`ui/initialize`
|
|
19
|
+
* notification (`ggui:renderer-ready`).
|
|
20
|
+
*
|
|
21
|
+
* Failure surfacing: every parse / handshake failure goes to two
|
|
22
|
+
* places — (1) the in-iframe status line (operator-visible) and
|
|
23
|
+
* (2) a parent-bound `postMessage({type:'ggui:bootstrap-failed', ...})`.
|
|
24
|
+
* The post-message envelope is consumed by the `<McpAppIframe>` host
|
|
25
|
+
* wrapper, which routes it to the host's `onError` callback.
|
|
26
|
+
*/
|
|
27
|
+
import './zod-jitless.js';
|
|
1
28
|
import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-ai/protocol';
|
|
2
29
|
import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
|
|
3
30
|
import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type McpAppRendererReadyMessage } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
@@ -47,6 +74,29 @@ export type ConnectAppResult = {
|
|
|
47
74
|
* without invoking the full boot pipeline.
|
|
48
75
|
*/
|
|
49
76
|
export declare function setCurrentApp(app: App): void;
|
|
77
|
+
/**
|
|
78
|
+
* The host's announced color mode, read from the connected App's
|
|
79
|
+
* pre-merged hostContext (`ui/initialize` result, kept current by the
|
|
80
|
+
* App across `host-context-changed`). `undefined` when no App is
|
|
81
|
+
* connected, the host sent no `theme`, or it sent something outside
|
|
82
|
+
* the spec's `'light' | 'dark'` literals (dropped, never guessed —
|
|
83
|
+
* the same tolerant posture as `projectHostContext`).
|
|
84
|
+
*
|
|
85
|
+
* This is the INPUT leg of host-theme adaptation
|
|
86
|
+
* (rnd/gen-ui/beauty/experiments/002-host-theme-adaptation.md, ggui#551):
|
|
87
|
+
* `hostContext.theme` alone reaches only `data-theme` +
|
|
88
|
+
* `color-scheme` via `applyDocumentTheme` — nothing in the token
|
|
89
|
+
* pipeline keys off it, so a dark host painted the light ladder
|
|
90
|
+
* (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
|
|
91
|
+
* `buildOpts` consumes this ONLY when the slice stamps no `themeMode`:
|
|
92
|
+
* every operator layer (live provider > per-render override > app
|
|
93
|
+
* default > server default) still wins by being stamped; the host
|
|
94
|
+
* fills the gap where no operator had an opinion.
|
|
95
|
+
*
|
|
96
|
+
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
97
|
+
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function hostAnnouncedThemeMode(): 'light' | 'dark' | undefined;
|
|
50
100
|
/**
|
|
51
101
|
* @internal — exported for unit tests to reset module state between
|
|
52
102
|
* scenarios.
|
|
@@ -193,6 +243,17 @@ export interface BootSequenceOptions {
|
|
|
193
243
|
* arrives, and `hostContext` is captured from `app.getHostContext()`.
|
|
194
244
|
*/
|
|
195
245
|
readonly preResolvedMeta?: McpAppAiGguiRenderMeta;
|
|
246
|
+
/**
|
|
247
|
+
* Autostart-layer pre-resolution of the read-plane door (ggui#537):
|
|
248
|
+
* a tool result that arrived before `bootSequence` ran carried the
|
|
249
|
+
* view's LOCATOR (`ui://ggui/render/…`) but no bootstrap material —
|
|
250
|
+
* the read-plane-only posture. The runtime cannot read the resource
|
|
251
|
+
* before the App is connected, so the locator is threaded here and
|
|
252
|
+
* resolved via {@link resolveMetaViaReadDoor} right after the
|
|
253
|
+
* handshake, ahead of the Tier 2 tool-result wait. Ignored when
|
|
254
|
+
* `preResolvedMeta` is set.
|
|
255
|
+
*/
|
|
256
|
+
readonly preResolvedLocator?: string;
|
|
196
257
|
/**
|
|
197
258
|
* How long to wait for the spec-canonical `ui/notifications/tool-result`
|
|
198
259
|
* notification (Tier 2 of the resolver chain) before failing with
|
|
@@ -409,6 +470,29 @@ export declare function extractMetaFromToolResult(params: unknown): McpAppAiGgui
|
|
|
409
470
|
* runtime reads it once at autostart.
|
|
410
471
|
*/
|
|
411
472
|
export declare function readPendingToolResults(): McpAppAiGguiRenderMeta | null;
|
|
473
|
+
/**
|
|
474
|
+
* Sibling of {@link readPendingToolResults} for the read-plane-only
|
|
475
|
+
* posture (ggui#537): the NEWEST buffered tool result that carries a
|
|
476
|
+
* `ui://ggui/render/…` locator but no bootstrap material. Consulted only
|
|
477
|
+
* when the buffer yielded no inline slice; the autostart threads it to
|
|
478
|
+
* `bootSequence` as `preResolvedLocator`, resolved after the handshake.
|
|
479
|
+
*/
|
|
480
|
+
export declare function readPendingToolResultLocator(): string | null;
|
|
481
|
+
/**
|
|
482
|
+
* The read-plane door (ggui#537). A server running the read-plane-only
|
|
483
|
+
* posture publishes only the view's identity on the tool result — the
|
|
484
|
+
* `ui://ggui/render/…` locator — and no bootstrap material; the
|
|
485
|
+
* per-render resource that locator names is the self-contained shell,
|
|
486
|
+
* whose document inlines the very envelope this runtime boots from.
|
|
487
|
+
* Ask the HOST to read it (`app.readServerResource` → `resources/read`,
|
|
488
|
+
* proxied by the host's own MCP client — no fetch from this sandbox, so
|
|
489
|
+
* a host CSP without `connect-src` to the server is not in the way),
|
|
490
|
+
* recover the envelope with the protocol's writer/reader pair
|
|
491
|
+
* ({@link readGguiShellEnvelope}), and validate it exactly as an inline
|
|
492
|
+
* slice would be. `null` on any miss (host cannot proxy reads, resource
|
|
493
|
+
* gone, no envelope) — the caller's other tiers still decide.
|
|
494
|
+
*/
|
|
495
|
+
export declare function resolveMetaViaReadDoor(app: App, locator: string): Promise<McpAppAiGguiRenderMeta | null>;
|
|
412
496
|
/**
|
|
413
497
|
* Render a submit-action's `data` payload as a short inline string
|
|
414
498
|
* for embedding in a `ui/message` consent prompt. Goal: human-
|
package/dist/runtime.js
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/iframe-runtime` iframe runtime entry.
|
|
3
|
+
*
|
|
4
|
+
* This is the file esbuild bundles into `dist/iframe-runtime.js`. The
|
|
5
|
+
* thin-shell HTML loads it via `<script type="module" src=".../iframe-runtime.js">`;
|
|
6
|
+
* on import the side-effects below take over: build a status DOM,
|
|
7
|
+
* postMessage `ui/initialize` to the parent, parse the bootstrap, open
|
|
8
|
+
* the WebSocket, run the version handshake, and mount the render
|
|
9
|
+
* placeholder.
|
|
10
|
+
*
|
|
11
|
+
* Boot sequence:
|
|
12
|
+
* - Boot from the `_meta["ai.ggui/render"]` slice received via
|
|
13
|
+
* `ui/initialize`.
|
|
14
|
+
* - Open the WebSocket, run the subscribe handshake.
|
|
15
|
+
* - Mount the render — either a structural placeholder or, when
|
|
16
|
+
* the renderer hooks are wired, a React mount of `componentCode`.
|
|
17
|
+
*
|
|
18
|
+
* The runtime advertises its build version via a post-`ui/initialize`
|
|
19
|
+
* notification (`ggui:renderer-ready`).
|
|
20
|
+
*
|
|
21
|
+
* Failure surfacing: every parse / handshake failure goes to two
|
|
22
|
+
* places — (1) the in-iframe status line (operator-visible) and
|
|
23
|
+
* (2) a parent-bound `postMessage({type:'ggui:bootstrap-failed', ...})`.
|
|
24
|
+
* The post-message envelope is consumed by the `<McpAppIframe>` host
|
|
25
|
+
* wrapper, which routes it to the host's `onError` callback.
|
|
26
|
+
*/
|
|
27
|
+
// FIRST import on purpose — declares zod jitless before any
|
|
28
|
+
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
|
+
import './zod-jitless.js';
|
|
30
|
+
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
31
|
+
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
3
32
|
import { projectHostContext } from '@ggui-ai/protocol';
|
|
4
33
|
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
|
|
5
34
|
import { applyHostContextStyling, attachListener as attachHostContextListener, seed as seedHostContext, } from './host-context-emitter.js';
|
|
@@ -131,6 +160,32 @@ export function setCurrentApp(app) {
|
|
|
131
160
|
function getCurrentApp() {
|
|
132
161
|
return currentApp;
|
|
133
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The host's announced color mode, read from the connected App's
|
|
165
|
+
* pre-merged hostContext (`ui/initialize` result, kept current by the
|
|
166
|
+
* App across `host-context-changed`). `undefined` when no App is
|
|
167
|
+
* connected, the host sent no `theme`, or it sent something outside
|
|
168
|
+
* the spec's `'light' | 'dark'` literals (dropped, never guessed —
|
|
169
|
+
* the same tolerant posture as `projectHostContext`).
|
|
170
|
+
*
|
|
171
|
+
* This is the INPUT leg of host-theme adaptation
|
|
172
|
+
* (rnd/gen-ui/beauty/experiments/002-host-theme-adaptation.md, ggui#551):
|
|
173
|
+
* `hostContext.theme` alone reaches only `data-theme` +
|
|
174
|
+
* `color-scheme` via `applyDocumentTheme` — nothing in the token
|
|
175
|
+
* pipeline keys off it, so a dark host painted the light ladder
|
|
176
|
+
* (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
|
|
177
|
+
* `buildOpts` consumes this ONLY when the slice stamps no `themeMode`:
|
|
178
|
+
* every operator layer (live provider > per-render override > app
|
|
179
|
+
* default > server default) still wins by being stamped; the host
|
|
180
|
+
* fills the gap where no operator had an opinion.
|
|
181
|
+
*
|
|
182
|
+
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
183
|
+
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
184
|
+
*/
|
|
185
|
+
export function hostAnnouncedThemeMode() {
|
|
186
|
+
const theme = currentApp?.getHostContext()?.theme;
|
|
187
|
+
return theme === 'light' || theme === 'dark' ? theme : undefined;
|
|
188
|
+
}
|
|
134
189
|
/**
|
|
135
190
|
* @internal — exported for unit tests to reset module state between
|
|
136
191
|
* scenarios.
|
|
@@ -314,7 +369,16 @@ export async function bootSequence(opts) {
|
|
|
314
369
|
parsed = inline;
|
|
315
370
|
}
|
|
316
371
|
else {
|
|
317
|
-
|
|
372
|
+
// Tier 1.5 — the read-plane door (ggui#537). A pre-boot tool
|
|
373
|
+
// result named the view but carried no material; now that the
|
|
374
|
+
// App is connected, resolve the locator through the host's
|
|
375
|
+
// `resources/read` proxy. One round trip, ahead of the Tier 2
|
|
376
|
+
// wait; a miss falls through to Tier 2 unchanged (its listener
|
|
377
|
+
// is already armed and resolves locator-only arrivals too).
|
|
378
|
+
const viaDoor = opts.preResolvedLocator !== undefined
|
|
379
|
+
? await resolveMetaViaReadDoor(app, opts.preResolvedLocator)
|
|
380
|
+
: null;
|
|
381
|
+
const fromToolResult = viaDoor ?? (await toolResultPromise);
|
|
318
382
|
if (fromToolResult !== null) {
|
|
319
383
|
parsed = { ok: true, meta: fromToolResult };
|
|
320
384
|
}
|
|
@@ -1052,6 +1116,26 @@ export function readPendingToolResults() {
|
|
|
1052
1116
|
}
|
|
1053
1117
|
return null;
|
|
1054
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Sibling of {@link readPendingToolResults} for the read-plane-only
|
|
1121
|
+
* posture (ggui#537): the NEWEST buffered tool result that carries a
|
|
1122
|
+
* `ui://ggui/render/…` locator but no bootstrap material. Consulted only
|
|
1123
|
+
* when the buffer yielded no inline slice; the autostart threads it to
|
|
1124
|
+
* `bootSequence` as `preResolvedLocator`, resolved after the handshake.
|
|
1125
|
+
*/
|
|
1126
|
+
export function readPendingToolResultLocator() {
|
|
1127
|
+
if (typeof window === 'undefined')
|
|
1128
|
+
return null;
|
|
1129
|
+
const raw = window.__GGUI_PENDING_TOOL_RESULTS__;
|
|
1130
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
1131
|
+
return null;
|
|
1132
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
1133
|
+
const locator = extractLocatorFromToolResult(raw[i]);
|
|
1134
|
+
if (locator !== null)
|
|
1135
|
+
return locator;
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1055
1139
|
/**
|
|
1056
1140
|
* Listen for a `ui/notifications/tool-result` notification via the
|
|
1057
1141
|
* App's spec-canonical event surface and resolve to the extracted
|
|
@@ -1067,36 +1151,75 @@ export function readPendingToolResults() {
|
|
|
1067
1151
|
function awaitToolResultMetaFromApp(app, timeoutMs) {
|
|
1068
1152
|
return new Promise((resolve) => {
|
|
1069
1153
|
let settled = false;
|
|
1070
|
-
const
|
|
1154
|
+
const settle = (value) => {
|
|
1071
1155
|
if (settled)
|
|
1072
1156
|
return;
|
|
1073
|
-
const meta = extractMetaFromToolResult(params);
|
|
1074
|
-
if (meta === null)
|
|
1075
|
-
return;
|
|
1076
1157
|
settled = true;
|
|
1077
1158
|
app.removeEventListener('toolresult', handler);
|
|
1078
1159
|
clearTimeout(timer);
|
|
1079
|
-
resolve(
|
|
1160
|
+
resolve(value);
|
|
1080
1161
|
};
|
|
1081
|
-
|
|
1082
|
-
const timer = setTimeout(() => {
|
|
1162
|
+
const handler = (params) => {
|
|
1083
1163
|
if (settled)
|
|
1084
1164
|
return;
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1165
|
+
const meta = extractMetaFromToolResult(params);
|
|
1166
|
+
if (meta !== null) {
|
|
1167
|
+
settle(meta);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
// Read-plane door (ggui#537): identity-only result → resolve the
|
|
1171
|
+
// locator through the host's `resources/read` proxy. Async, and
|
|
1172
|
+
// it never settles `null` — a later result carrying the slice
|
|
1173
|
+
// inline (or the timeout) still decides.
|
|
1174
|
+
const locator = extractLocatorFromToolResult(params);
|
|
1175
|
+
if (locator === null)
|
|
1176
|
+
return;
|
|
1177
|
+
void resolveMetaViaReadDoor(app, locator).then((resolved) => {
|
|
1178
|
+
if (resolved !== null)
|
|
1179
|
+
settle(resolved);
|
|
1180
|
+
});
|
|
1181
|
+
};
|
|
1182
|
+
app.addEventListener('toolresult', handler);
|
|
1183
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
1089
1184
|
});
|
|
1090
1185
|
}
|
|
1091
1186
|
/**
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
|
|
1187
|
+
* The read-plane door (ggui#537). A server running the read-plane-only
|
|
1188
|
+
* posture publishes only the view's identity on the tool result — the
|
|
1189
|
+
* `ui://ggui/render/…` locator — and no bootstrap material; the
|
|
1190
|
+
* per-render resource that locator names is the self-contained shell,
|
|
1191
|
+
* whose document inlines the very envelope this runtime boots from.
|
|
1192
|
+
* Ask the HOST to read it (`app.readServerResource` → `resources/read`,
|
|
1193
|
+
* proxied by the host's own MCP client — no fetch from this sandbox, so
|
|
1194
|
+
* a host CSP without `connect-src` to the server is not in the way),
|
|
1195
|
+
* recover the envelope with the protocol's writer/reader pair
|
|
1196
|
+
* ({@link readGguiShellEnvelope}), and validate it exactly as an inline
|
|
1197
|
+
* slice would be. `null` on any miss (host cannot proxy reads, resource
|
|
1198
|
+
* gone, no envelope) — the caller's other tiers still decide.
|
|
1199
|
+
*/
|
|
1200
|
+
export async function resolveMetaViaReadDoor(app, locator) {
|
|
1201
|
+
let text;
|
|
1202
|
+
try {
|
|
1203
|
+
const res = await app.readServerResource({ uri: locator });
|
|
1204
|
+
const first = res.contents[0];
|
|
1205
|
+
text = first !== undefined && 'text' in first && typeof first.text === 'string' ? first.text : undefined;
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
// eslint-disable-next-line no-console -- operator-visible: the door is a boot tier, its miss must be legible
|
|
1209
|
+
console.warn(`[ggui-runtime] read-plane door: host could not read ${locator} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
if (text === undefined)
|
|
1213
|
+
return null;
|
|
1214
|
+
const envelope = readGguiShellEnvelope(text);
|
|
1215
|
+
if (envelope === undefined)
|
|
1216
|
+
return null;
|
|
1217
|
+
const parsed = parseMcpAppAiGguiRenderMeta(envelope);
|
|
1218
|
+
if (!parsed.ok || parsed.meta === undefined)
|
|
1219
|
+
return null;
|
|
1220
|
+
const validated = validateMeta(parsed.meta);
|
|
1221
|
+
return validated.ok ? validated.meta : null;
|
|
1222
|
+
}
|
|
1100
1223
|
function awaitToolResultMeta(timeoutMs) {
|
|
1101
1224
|
if (typeof window === 'undefined')
|
|
1102
1225
|
return Promise.resolve(null);
|
|
@@ -1119,8 +1242,17 @@ function awaitToolResultMeta(timeoutMs) {
|
|
|
1119
1242
|
return;
|
|
1120
1243
|
}
|
|
1121
1244
|
const meta = extractMetaFromToolResult(m.params);
|
|
1122
|
-
if (meta !== null)
|
|
1123
|
-
settle(meta);
|
|
1245
|
+
if (meta !== null) {
|
|
1246
|
+
settle({ kind: 'meta', meta });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
// Identity-only result (ggui#537): settle NOW with the locator so
|
|
1250
|
+
// boot starts immediately and the door runs after the handshake —
|
|
1251
|
+
// waiting the full timeout for a slice this posture never sends
|
|
1252
|
+
// would cost every spec-host view 30 s for nothing.
|
|
1253
|
+
const locator = extractLocatorFromToolResult(m.params);
|
|
1254
|
+
if (locator !== null)
|
|
1255
|
+
settle({ kind: 'locator', locator });
|
|
1124
1256
|
};
|
|
1125
1257
|
window.addEventListener('message', onMessage);
|
|
1126
1258
|
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
@@ -2969,7 +3101,7 @@ function readLiveBootstrapShape() {
|
|
|
2969
3101
|
* re-await the same postMessage or re-parse the same global), saving
|
|
2970
3102
|
* up to the 30s postMessage timeout for spec-strict hosts.
|
|
2971
3103
|
*/
|
|
2972
|
-
function runBootProduction(preResolvedMeta) {
|
|
3104
|
+
function runBootProduction(preResolvedMeta, preResolvedLocator) {
|
|
2973
3105
|
const { app, transport } = createDefaultApp();
|
|
2974
3106
|
void bootProduction({
|
|
2975
3107
|
doc: document,
|
|
@@ -2985,6 +3117,7 @@ function runBootProduction(preResolvedMeta) {
|
|
|
2985
3117
|
onObserve: postObservabilityToParent,
|
|
2986
3118
|
onLifecycle: postLifecycleToParent,
|
|
2987
3119
|
...(preResolvedMeta !== undefined ? { preResolvedMeta } : {}),
|
|
3120
|
+
...(preResolvedLocator !== undefined ? { preResolvedLocator } : {}),
|
|
2988
3121
|
});
|
|
2989
3122
|
}
|
|
2990
3123
|
if (shouldAutostart() && typeof window !== 'undefined') {
|
|
@@ -3015,15 +3148,26 @@ if (shouldAutostart() && typeof window !== 'undefined') {
|
|
|
3015
3148
|
}
|
|
3016
3149
|
else {
|
|
3017
3150
|
const buffered = readPendingToolResults();
|
|
3151
|
+
const bufferedLocator = buffered === null ? readPendingToolResultLocator() : null;
|
|
3018
3152
|
if (buffered !== null) {
|
|
3019
3153
|
runBootProduction(buffered);
|
|
3020
3154
|
}
|
|
3155
|
+
else if (bufferedLocator !== null) {
|
|
3156
|
+
// Read-plane-only posture (ggui#537): the buffered result named
|
|
3157
|
+
// the view; the door resolves it right after the handshake.
|
|
3158
|
+
runBootProduction(undefined, bufferedLocator);
|
|
3159
|
+
}
|
|
3021
3160
|
else if (readLiveBootstrapShape()) {
|
|
3022
3161
|
runBootProduction();
|
|
3023
3162
|
}
|
|
3024
3163
|
else {
|
|
3025
|
-
void awaitToolResultMeta(POSTMESSAGE_BOOT_TIMEOUT_MS).then((
|
|
3026
|
-
|
|
3164
|
+
void awaitToolResultMeta(POSTMESSAGE_BOOT_TIMEOUT_MS).then((pre) => {
|
|
3165
|
+
if (pre === null)
|
|
3166
|
+
runBootProduction();
|
|
3167
|
+
else if (pre.kind === 'meta')
|
|
3168
|
+
runBootProduction(pre.meta);
|
|
3169
|
+
else
|
|
3170
|
+
runBootProduction(undefined, pre.locator);
|
|
3027
3171
|
});
|
|
3028
3172
|
}
|
|
3029
3173
|
}
|
|
@@ -3302,10 +3446,30 @@ async function bootProduction(opts) {
|
|
|
3302
3446
|
...(meta.gadgets !== undefined
|
|
3303
3447
|
? { gadgetPackages: meta.gadgets.map((g) => g.package) }
|
|
3304
3448
|
: {}),
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3449
|
+
// Strict-CSP module variant (ggui#522 slice 2) — attached to
|
|
3450
|
+
// the INLINE SEED mount only, detected by the absence of the
|
|
3451
|
+
// server-assigned ledger fields (`buildGguiSessionSeedInput`
|
|
3452
|
+
// omits them; every wire-delivered GguiSession carries them).
|
|
3453
|
+
// The variant URL names the SEEDED bytes: a WS-delivered
|
|
3454
|
+
// body may differ, and importing a stale variant would paint
|
|
3455
|
+
// the WRONG component rather than fail. A WS redelivery of
|
|
3456
|
+
// the SAME bytes loses nothing — same code ⇒ the renderer
|
|
3457
|
+
// skips re-evaluation entirely.
|
|
3458
|
+
...(meta.codeModuleUrl !== undefined &&
|
|
3459
|
+
render.type !== 'mcpApps' &&
|
|
3460
|
+
render.type !== 'system' &&
|
|
3461
|
+
!('createdAt' in render)
|
|
3462
|
+
? { codeModuleUrl: meta.codeModuleUrl }
|
|
3308
3463
|
: {}),
|
|
3464
|
+
...(meta.themeId !== undefined ? { themeId: meta.themeId } : {}),
|
|
3465
|
+
// Mode: the slice's stamped `themeMode` (every operator layer
|
|
3466
|
+
// resolves into it) wins; when absent, the HOST's announced
|
|
3467
|
+
// theme fills the gap (ggui#551 — see `hostAnnouncedThemeMode`).
|
|
3468
|
+
// Absent-in-slice means "no operator opinion", not "light".
|
|
3469
|
+
...(() => {
|
|
3470
|
+
const themeMode = meta.themeMode ?? hostAnnouncedThemeMode();
|
|
3471
|
+
return themeMode !== undefined ? { themeMode } : {};
|
|
3472
|
+
})(),
|
|
3309
3473
|
// Per-app theme overlay (St3 M2.2). Threaded straight from the
|
|
3310
3474
|
// bootstrap's `_meta["ai.ggui/render"].theme` (typed `AppTheme`,
|
|
3311
3475
|
// already injection-validated by the wire parser) onto the mount
|
|
@@ -3493,5 +3657,8 @@ async function bootProduction(opts) {
|
|
|
3493
3657
|
...(opts.preResolvedMeta !== undefined
|
|
3494
3658
|
? { preResolvedMeta: opts.preResolvedMeta }
|
|
3495
3659
|
: {}),
|
|
3660
|
+
...(opts.preResolvedLocator !== undefined
|
|
3661
|
+
? { preResolvedLocator: opts.preResolvedLocator }
|
|
3662
|
+
: {}),
|
|
3496
3663
|
});
|
|
3497
3664
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -34,3 +34,22 @@ export declare const RUNTIME_DIST_DIR: string;
|
|
|
34
34
|
* `bootstrap.runtimeUrl`, never hardcodes it.
|
|
35
35
|
*/
|
|
36
36
|
export declare const RUNTIME_BUNDLE_URL_PATH: "/_ggui/iframe-runtime.js";
|
|
37
|
+
/**
|
|
38
|
+
* Directory of the static shim modules (`dist/shims/<name>.js`) the
|
|
39
|
+
* build emits alongside the bundle (ggui#522 slice 2 — `asset-url`
|
|
40
|
+
* delivery). The mcp-server captures these files at composition and
|
|
41
|
+
* serves them under `/_ggui/shims/<runtime-hash>/<name>.js` with the
|
|
42
|
+
* same immutable posture as the hashed bundle route: the shims ship in
|
|
43
|
+
* the same dist as the bundle, so the bundle's content hash versions
|
|
44
|
+
* both — a shim-affecting change in the design package also changes
|
|
45
|
+
* the bundle bytes.
|
|
46
|
+
*/
|
|
47
|
+
export declare const RUNTIME_SHIMS_DIR: string;
|
|
48
|
+
/**
|
|
49
|
+
* The URL prefix under which the mcp-server publishes the shim
|
|
50
|
+
* directory family. The full shim directory for a given build is
|
|
51
|
+
* `<origin>${RUNTIME_SHIMS_URL_PREFIX}/<runtime-hash>` — the server-
|
|
52
|
+
* side `asset-url` rewrite composes shim URLs on it, and the shim
|
|
53
|
+
* route parses it back.
|
|
54
|
+
*/
|
|
55
|
+
export declare const RUNTIME_SHIMS_URL_PREFIX: "/_ggui/shims";
|