@ggui-ai/iframe-runtime 0.7.0 → 0.8.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/iframe-runtime.js +72 -60
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/meta-parse.d.ts +3 -3
- package/dist/meta-parse.js +13 -10
- package/dist/react-renderer.js +70 -18
- package/dist/runtime.d.ts +45 -5
- package/dist/runtime.js +88 -24
- package/package.json +7 -7
package/dist/meta-parse.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ import type { McpAppAiGguiMetaParseResult } from './types.js';
|
|
|
11
11
|
* direct-call callers like `parseMetaFromGlobal` / tests; the
|
|
12
12
|
* envelope-driven extractors already enforce this in the protocol-
|
|
13
13
|
* level parser).
|
|
14
|
-
* - At least one of `{live mode (wsUrl+wsToken), codeUrl,
|
|
15
|
-
* be present. Without any mode discriminator the iframe
|
|
16
|
-
* to mount.
|
|
14
|
+
* - At least one of `{live mode (wsUrl+wsToken), codeUrl, codeB64,
|
|
15
|
+
* kind}` MUST be present. Without any mode discriminator the iframe
|
|
16
|
+
* has nothing to mount.
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGguiMetaParseResult;
|
|
19
19
|
/**
|
package/dist/meta-parse.js
CHANGED
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
*
|
|
41
41
|
* - **live** — slice has `wsUrl` + `wsToken`; runtime opens
|
|
42
42
|
* live-channel WS and renders agent-driven frames.
|
|
43
|
-
* - **static-component** — slice has `codeUrl
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* - **static-component** — slice has `codeUrl` (runtime fetches the
|
|
44
|
+
* content-addressable URL) and/or `codeB64` (runtime decodes the
|
|
45
|
+
* inline bytes, no fetch); mounts the React component, never
|
|
46
|
+
* opens WS.
|
|
46
47
|
* - **system-card** — slice has `kind`; runtime maps to a
|
|
47
48
|
* built-in component via the system-card registry.
|
|
48
49
|
*
|
|
@@ -177,9 +178,9 @@ function projectPublicEnv(raw) {
|
|
|
177
178
|
*
|
|
178
179
|
* Invariants enforced here that the combiner doesn't:
|
|
179
180
|
* - `expiresAt` (when present) MUST parse to a valid timestamp. If
|
|
180
|
-
* in the past AND the slice carries static content (codeUrl /
|
|
181
|
-
* DEGRADE to static-only (drop `wsUrl` /
|
|
182
|
-
* otherwise return EXPIRED_BOOTSTRAP.
|
|
181
|
+
* in the past AND the slice carries static content (codeUrl /
|
|
182
|
+
* codeB64 / kind), DEGRADE to static-only (drop `wsUrl` /
|
|
183
|
+
* `wsToken` / `expiresAt`); otherwise return EXPIRED_BOOTSTRAP.
|
|
183
184
|
* - `gadgets` / `publicEnv` / `permissionsPolicy` /
|
|
184
185
|
* `streamWebSocketLocalTools` / `contextSlots` get defensive
|
|
185
186
|
* entry-level validation; malformed → defaulted.
|
|
@@ -260,6 +261,7 @@ function projectMeta(meta, hasStaticContent) {
|
|
|
260
261
|
: {}),
|
|
261
262
|
...(meta.codeUrl !== undefined ? { codeUrl: meta.codeUrl } : {}),
|
|
262
263
|
...(meta.codeHash !== undefined ? { codeHash: meta.codeHash } : {}),
|
|
264
|
+
...(meta.codeB64 !== undefined ? { codeB64: meta.codeB64 } : {}),
|
|
263
265
|
...(meta.kind !== undefined ? { kind: meta.kind } : {}),
|
|
264
266
|
};
|
|
265
267
|
return { ok: true, meta: projected };
|
|
@@ -275,9 +277,9 @@ function projectMeta(meta, hasStaticContent) {
|
|
|
275
277
|
* direct-call callers like `parseMetaFromGlobal` / tests; the
|
|
276
278
|
* envelope-driven extractors already enforce this in the protocol-
|
|
277
279
|
* level parser).
|
|
278
|
-
* - At least one of `{live mode (wsUrl+wsToken), codeUrl,
|
|
279
|
-
* be present. Without any mode discriminator the iframe
|
|
280
|
-
* to mount.
|
|
280
|
+
* - At least one of `{live mode (wsUrl+wsToken), codeUrl, codeB64,
|
|
281
|
+
* kind}` MUST be present. Without any mode discriminator the iframe
|
|
282
|
+
* has nothing to mount.
|
|
281
283
|
*/
|
|
282
284
|
export function validateMeta(meta) {
|
|
283
285
|
// `runtimeUrl` is a hard wire-required field on every render slice
|
|
@@ -292,8 +294,9 @@ export function validateMeta(meta) {
|
|
|
292
294
|
}
|
|
293
295
|
const hasLive = isNonEmptyString(meta.wsUrl) && isNonEmptyString(meta.wsToken);
|
|
294
296
|
const hasCodeUrl = isNonEmptyString(meta.codeUrl);
|
|
297
|
+
const hasCodeB64 = isNonEmptyString(meta.codeB64);
|
|
295
298
|
const hasKind = isNonEmptyString(meta.kind);
|
|
296
|
-
const hasStaticContent = hasCodeUrl || hasKind;
|
|
299
|
+
const hasStaticContent = hasCodeUrl || hasCodeB64 || hasKind;
|
|
297
300
|
if (!hasLive && !hasStaticContent) {
|
|
298
301
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
299
302
|
}
|
package/dist/react-renderer.js
CHANGED
|
@@ -32,7 +32,18 @@
|
|
|
32
32
|
import React, { Component, createElement, Fragment, } from 'react';
|
|
33
33
|
import { createRoot } from 'react-dom/client';
|
|
34
34
|
import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
|
|
35
|
-
import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
|
|
35
|
+
import { hoistImports, loadModule, loadModuleInline, probeUrlModuleLoad, } from '@ggui-ai/design/module-loader';
|
|
36
|
+
/**
|
|
37
|
+
* Document-lifetime verdict: URL-scheme module loading (`blob:` main
|
|
38
|
+
* module + `data:` import shims) is blocked by the host CSP. Set only
|
|
39
|
+
* when the inline path succeeded where the URL path failed AND the
|
|
40
|
+
* known-good `probeUrlModuleLoad()` module ALSO fails — an arbitrary
|
|
41
|
+
* module's failure is not CSP evidence (an import-shim allowlist miss
|
|
42
|
+
* rejects blob-instantiation of that one module while URL loading is
|
|
43
|
+
* healthy, and latching on it would force every later evaluation in
|
|
44
|
+
* the document onto the inline path's narrower semantics).
|
|
45
|
+
*/
|
|
46
|
+
let urlModuleLoadBlocked = false;
|
|
36
47
|
// =============================================================================
|
|
37
48
|
// Scope class helpers — port of RCR's `makeScopeClass` + counter.
|
|
38
49
|
// =============================================================================
|
|
@@ -177,25 +188,66 @@ export async function mountReactRoot(container, opts) {
|
|
|
177
188
|
}
|
|
178
189
|
// 1. Strip metadata markers (__GGUI_META__ / __GGUI_STREAM_SPEC__).
|
|
179
190
|
const cleaned = stripMarkers(resolved);
|
|
180
|
-
// 2. Hoist imports above var declarations.
|
|
191
|
+
// 2. Hoist imports above var declarations. Load-bearing for BOTH
|
|
192
|
+
// execution paths: the inline transform places its bindings at
|
|
193
|
+
// the import statement's position, so a pre-import statement
|
|
194
|
+
// referencing an imported name needs the hoist exactly as the
|
|
195
|
+
// real module semantics do.
|
|
181
196
|
const hoisted = hoistImports(cleaned);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
const pick = (mod) => {
|
|
198
|
+
const Comp = (mod.default ??
|
|
199
|
+
Object.values(mod).find((v) => typeof v === 'function'));
|
|
200
|
+
if (Comp === undefined) {
|
|
201
|
+
throw new Error('Module does not export a default component');
|
|
202
|
+
}
|
|
203
|
+
return Comp;
|
|
204
|
+
};
|
|
205
|
+
const evaluateInline = () => pick(loadModuleInline(hoisted, {
|
|
206
|
+
...(currentOpts.gadgetPackages !== undefined
|
|
207
|
+
? { gadgetPackages: currentOpts.gadgetPackages }
|
|
208
|
+
: {}),
|
|
209
|
+
}));
|
|
210
|
+
// Once URL-scheme module loading has been proven blocked in this
|
|
211
|
+
// document, go straight to inline execution — the CSP verdict
|
|
212
|
+
// cannot change within the document's lifetime.
|
|
213
|
+
if (urlModuleLoadBlocked) {
|
|
214
|
+
return evaluateInline();
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
// 3. Rewrite bare specifiers to data-url shims reading from
|
|
218
|
+
// `globalThis.__ggui__`. The __ggui__ registry is installed
|
|
219
|
+
// by `runtime.ts::bootSequence` BEFORE any mountReactRoot call
|
|
220
|
+
// — see globals.ts docstring on TOCTOU ordering.
|
|
221
|
+
const rewritten = rewriteImports(hoisted, {
|
|
222
|
+
mode: 'data-url',
|
|
223
|
+
gadgetPackages: currentOpts.gadgetPackages,
|
|
224
|
+
});
|
|
225
|
+
// 4. Dynamically import the module.
|
|
226
|
+
const mod = await loadModule(rewritten);
|
|
227
|
+
// 5. Extract default export (or first function export).
|
|
228
|
+
return pick(mod);
|
|
229
|
+
}
|
|
230
|
+
catch (urlError) {
|
|
231
|
+
// A host CSP granting only 'unsafe-inline' rejects the blob:/
|
|
232
|
+
// data: imports above. Attempt the inline classic-script path;
|
|
233
|
+
// a buggy component fails on both paths, and the original error
|
|
234
|
+
// is the honest one to surface.
|
|
235
|
+
try {
|
|
236
|
+
const Comp = evaluateInline();
|
|
237
|
+
// Latch the inline path for the rest of the document ONLY on
|
|
238
|
+
// proven CSP evidence: the known-good probe module must also
|
|
239
|
+
// fail. A module-specific blob failure (import-shim allowlist
|
|
240
|
+
// drift) leaves the latch off so the next evaluation gets real
|
|
241
|
+
// module semantics again.
|
|
242
|
+
if (!(await probeUrlModuleLoad())) {
|
|
243
|
+
urlModuleLoadBlocked = true;
|
|
244
|
+
}
|
|
245
|
+
return Comp;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw urlError;
|
|
249
|
+
}
|
|
197
250
|
}
|
|
198
|
-
return Comp;
|
|
199
251
|
}
|
|
200
252
|
function renderTree(opts) {
|
|
201
253
|
const themeCss = opts.themeId
|
package/dist/runtime.d.ts
CHANGED
|
@@ -342,18 +342,39 @@ export declare function readSelfContainedMeta(): McpAppAiGguiRenderMeta | null;
|
|
|
342
342
|
* no WS at all for spec-compliant MCP-Apps hosts that expose no ggui
|
|
343
343
|
* live channel (claude.ai / ChatGPT / Claude Desktop).
|
|
344
344
|
*
|
|
345
|
-
*
|
|
345
|
+
* Three static-content shapes (the autostart discriminator), in
|
|
346
|
+
* priority order:
|
|
346
347
|
* - `kind` → a system-card seed (`type:'system'`); no fetch.
|
|
348
|
+
* - `codeB64` → a compiled-component seed carried inline; DECODED,
|
|
349
|
+
* never fetched. Preferred over `codeUrl` when both are present —
|
|
350
|
+
* the inline bytes exist precisely for hosts whose iframe CSP
|
|
351
|
+
* blocks the fetch.
|
|
347
352
|
* - `codeUrl` → a compiled-component seed; fetches the
|
|
348
353
|
* content-addressable component bytes.
|
|
349
354
|
*
|
|
350
|
-
* Returns `null` when the meta carries
|
|
355
|
+
* Returns `null` when the meta carries NONE (a live-only meta has
|
|
351
356
|
* nothing to seed — the first WS ack mounts it). Throws when a `codeUrl`
|
|
352
|
-
* fetch fails
|
|
353
|
-
*
|
|
354
|
-
* ack reconciles the seed to a
|
|
357
|
+
* fetch fails or `codeB64` is not valid base64 so the caller can
|
|
358
|
+
* surface a typed boot failure. The four server-assigned ledger fields
|
|
359
|
+
* are intentionally absent — the first ack reconciles the seed to a
|
|
360
|
+
* full `GguiSession` (no fabrication).
|
|
355
361
|
*/
|
|
356
362
|
export declare function buildGguiSessionSeedInput(meta: McpAppAiGguiRenderMeta): Promise<GguiSessionSeedInput | null>;
|
|
363
|
+
/**
|
|
364
|
+
* Does this slice carry STATIC content the mount surface can paint
|
|
365
|
+
* without any network round-trip authorization — `codeUrl` (fetched),
|
|
366
|
+
* `codeB64` (decoded inline), or `kind` (built-in registry)?
|
|
367
|
+
*
|
|
368
|
+
* The ONE discriminator `bootSequence` consults when deciding whether
|
|
369
|
+
* the static seed-mount path runs. `codeB64` counting here is
|
|
370
|
+
* load-bearing for fetch-blocked hosts: their meta carries codeB64
|
|
371
|
+
* PLUS a live trio the iframe can never connect, and missing the
|
|
372
|
+
* inline arm would skip the only paint path and die on the dead WS.
|
|
373
|
+
* Mirrors `validateMeta`'s static-content arm (meta-parse.ts) and the
|
|
374
|
+
* protocol host-helper's `hasMountModeDiscriminator` — drift among
|
|
375
|
+
* the three IS the historical bug class, hence the named export.
|
|
376
|
+
*/
|
|
377
|
+
export declare function hasStaticContentMeta(meta: McpAppAiGguiRenderMeta): boolean;
|
|
357
378
|
/**
|
|
358
379
|
* Extract a {@link McpAppAiGguiRenderMeta} from a
|
|
359
380
|
* `ui/notifications/tool-result` JSON-RPC params payload — the
|
|
@@ -369,6 +390,25 @@ export declare function buildGguiSessionSeedInput(meta: McpAppAiGguiRenderMeta):
|
|
|
369
390
|
* impossible because the shell never inspects the slice-meta shape.
|
|
370
391
|
*/
|
|
371
392
|
export declare function extractMetaFromToolResult(params: unknown): McpAppAiGguiRenderMeta | null;
|
|
393
|
+
/**
|
|
394
|
+
* Drain `window.__GGUI_PENDING_TOOL_RESULTS__` — the buffer the
|
|
395
|
+
* minimal shell populates while messages arrive between
|
|
396
|
+
* shell-load and runtime-load. Returns the NEWEST valid slice meta
|
|
397
|
+
* (each new tool-result supersedes the previous — every emission
|
|
398
|
+
* carries complete state, so last-wins loses nothing). Newest-first
|
|
399
|
+
* matters on the inline-runtime shell: its preflight completes the
|
|
400
|
+
* handshake BEFORE the large bundle parses, so a render AND a
|
|
401
|
+
* follow-up update can both land in the buffer during parse — booting
|
|
402
|
+
* the oldest would paint stale props and drop the update forever
|
|
403
|
+
* (buffered entries never reach the post-mount listener; it hears
|
|
404
|
+
* only post-App arrivals).
|
|
405
|
+
*
|
|
406
|
+
* The shell-side buffer contract: an array whose ELEMENTS are the raw
|
|
407
|
+
* JSON-RPC `params` values of `ui/notifications/tool-result`
|
|
408
|
+
* notifications, in arrival order (shells cap it newest-biased). The
|
|
409
|
+
* runtime reads it once at autostart.
|
|
410
|
+
*/
|
|
411
|
+
export declare function readPendingToolResults(): McpAppAiGguiRenderMeta | null;
|
|
372
412
|
/**
|
|
373
413
|
* Render a submit-action's `data` payload as a short inline string
|
|
374
414
|
* for embedding in a `ui/message` consent prompt. Goal: human-
|
package/dist/runtime.js
CHANGED
|
@@ -461,12 +461,12 @@ export async function bootSequence(opts) {
|
|
|
461
461
|
onLifecycle?.(makeLifecycleEvent('code-ready', { sessionId: meta.sessionId }));
|
|
462
462
|
};
|
|
463
463
|
// Mode discriminators. The mount surface is DECOUPLED from the live
|
|
464
|
-
// channel: static content (codeUrl/kind) paints immediately
|
|
465
|
-
// the live trio (wsUrl+wsToken) is an OPTIONAL
|
|
466
|
-
// delivers props_update / data / re-render frames.
|
|
467
|
-
// NEITHER has nothing to show and nowhere to
|
|
468
|
-
|
|
469
|
-
|
|
464
|
+
// channel: static content (codeUrl/codeB64/kind) paints immediately
|
|
465
|
+
// with no WS; the live trio (wsUrl+wsToken) is an OPTIONAL
|
|
466
|
+
// enhancement that delivers props_update / data / re-render frames.
|
|
467
|
+
// A bootstrap with NEITHER has nothing to show and nowhere to
|
|
468
|
+
// subscribe.
|
|
469
|
+
const hasStaticContent = hasStaticContentMeta(meta);
|
|
470
470
|
const hasLiveTrio = typeof meta.wsUrl === 'string' &&
|
|
471
471
|
meta.wsUrl.length > 0 &&
|
|
472
472
|
typeof meta.wsToken === 'string' &&
|
|
@@ -517,7 +517,7 @@ export async function bootSequence(opts) {
|
|
|
517
517
|
// misconfiguration — surface a typed boot failure.
|
|
518
518
|
if (!hasLiveTrio) {
|
|
519
519
|
if (mountedRender === null) {
|
|
520
|
-
const message = 'bootstrap carries neither static content (codeUrl/kind) nor a live trio (wsUrl/wsToken)';
|
|
520
|
+
const message = 'bootstrap carries neither static content (codeUrl/codeB64/kind) nor a live trio (wsUrl/wsToken)';
|
|
521
521
|
setStatus(refs, message, 'error');
|
|
522
522
|
emitBootFailure('MISSING_META_GGUI_BOOTSTRAP', message);
|
|
523
523
|
return { ok: false, mountedRender };
|
|
@@ -801,16 +801,22 @@ function parseSeedProps(propsJson) {
|
|
|
801
801
|
* no WS at all for spec-compliant MCP-Apps hosts that expose no ggui
|
|
802
802
|
* live channel (claude.ai / ChatGPT / Claude Desktop).
|
|
803
803
|
*
|
|
804
|
-
*
|
|
804
|
+
* Three static-content shapes (the autostart discriminator), in
|
|
805
|
+
* priority order:
|
|
805
806
|
* - `kind` → a system-card seed (`type:'system'`); no fetch.
|
|
807
|
+
* - `codeB64` → a compiled-component seed carried inline; DECODED,
|
|
808
|
+
* never fetched. Preferred over `codeUrl` when both are present —
|
|
809
|
+
* the inline bytes exist precisely for hosts whose iframe CSP
|
|
810
|
+
* blocks the fetch.
|
|
806
811
|
* - `codeUrl` → a compiled-component seed; fetches the
|
|
807
812
|
* content-addressable component bytes.
|
|
808
813
|
*
|
|
809
|
-
* Returns `null` when the meta carries
|
|
814
|
+
* Returns `null` when the meta carries NONE (a live-only meta has
|
|
810
815
|
* nothing to seed — the first WS ack mounts it). Throws when a `codeUrl`
|
|
811
|
-
* fetch fails
|
|
812
|
-
*
|
|
813
|
-
* ack reconciles the seed to a
|
|
816
|
+
* fetch fails or `codeB64` is not valid base64 so the caller can
|
|
817
|
+
* surface a typed boot failure. The four server-assigned ledger fields
|
|
818
|
+
* are intentionally absent — the first ack reconciles the seed to a
|
|
819
|
+
* full `GguiSession` (no fabrication).
|
|
814
820
|
*/
|
|
815
821
|
export async function buildGguiSessionSeedInput(meta) {
|
|
816
822
|
const props = parseSeedProps(meta.propsJson);
|
|
@@ -824,6 +830,15 @@ export async function buildGguiSessionSeedInput(meta) {
|
|
|
824
830
|
...(props !== undefined ? { props } : {}),
|
|
825
831
|
};
|
|
826
832
|
}
|
|
833
|
+
// Inline compiled-component mode — decode, no network.
|
|
834
|
+
if (meta.codeB64 !== undefined) {
|
|
835
|
+
return {
|
|
836
|
+
id: meta.sessionId,
|
|
837
|
+
appId: meta.appId,
|
|
838
|
+
componentCode: decodeCodeB64(meta.codeB64),
|
|
839
|
+
...(props !== undefined ? { props } : {}),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
827
842
|
// Compiled-component mode — fetch the content-addressable bytes.
|
|
828
843
|
if (meta.codeUrl === undefined)
|
|
829
844
|
return null;
|
|
@@ -839,6 +854,45 @@ export async function buildGguiSessionSeedInput(meta) {
|
|
|
839
854
|
...(props !== undefined ? { props } : {}),
|
|
840
855
|
};
|
|
841
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Does this slice carry STATIC content the mount surface can paint
|
|
859
|
+
* without any network round-trip authorization — `codeUrl` (fetched),
|
|
860
|
+
* `codeB64` (decoded inline), or `kind` (built-in registry)?
|
|
861
|
+
*
|
|
862
|
+
* The ONE discriminator `bootSequence` consults when deciding whether
|
|
863
|
+
* the static seed-mount path runs. `codeB64` counting here is
|
|
864
|
+
* load-bearing for fetch-blocked hosts: their meta carries codeB64
|
|
865
|
+
* PLUS a live trio the iframe can never connect, and missing the
|
|
866
|
+
* inline arm would skip the only paint path and die on the dead WS.
|
|
867
|
+
* Mirrors `validateMeta`'s static-content arm (meta-parse.ts) and the
|
|
868
|
+
* protocol host-helper's `hasMountModeDiscriminator` — drift among
|
|
869
|
+
* the three IS the historical bug class, hence the named export.
|
|
870
|
+
*/
|
|
871
|
+
export function hasStaticContentMeta(meta) {
|
|
872
|
+
return ((typeof meta.codeUrl === 'string' && meta.codeUrl.length > 0) ||
|
|
873
|
+
(typeof meta.codeB64 === 'string' && meta.codeB64.length > 0) ||
|
|
874
|
+
(typeof meta.kind === 'string' && meta.kind.length > 0));
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Decode a `codeB64` slice field to UTF-8 component source. `atob`
|
|
878
|
+
* yields a byte string; routing through `TextDecoder` restores
|
|
879
|
+
* multi-byte characters (component source is UTF-8 — string literals
|
|
880
|
+
* carry emoji, quotes, non-Latin text). Throws a labeled error on
|
|
881
|
+
* malformed base64 so the boot-failure surface names the field.
|
|
882
|
+
*/
|
|
883
|
+
function decodeCodeB64(codeB64) {
|
|
884
|
+
try {
|
|
885
|
+
const byteString = atob(codeB64);
|
|
886
|
+
const bytes = new Uint8Array(byteString.length);
|
|
887
|
+
for (let i = 0; i < byteString.length; i++) {
|
|
888
|
+
bytes[i] = byteString.charCodeAt(i);
|
|
889
|
+
}
|
|
890
|
+
return new TextDecoder().decode(bytes);
|
|
891
|
+
}
|
|
892
|
+
catch (cause) {
|
|
893
|
+
throw new Error('buildGguiSessionSeedInput: codeB64 is not valid base64', { cause });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
842
896
|
/**
|
|
843
897
|
* Extract a {@link McpAppAiGguiRenderMeta} from a
|
|
844
898
|
* `ui/notifications/tool-result` JSON-RPC params payload — the
|
|
@@ -860,23 +914,29 @@ export function extractMetaFromToolResult(params) {
|
|
|
860
914
|
/**
|
|
861
915
|
* Drain `window.__GGUI_PENDING_TOOL_RESULTS__` — the buffer the
|
|
862
916
|
* minimal shell populates while messages arrive between
|
|
863
|
-
* shell-load and runtime-load. Returns the
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
|
|
872
|
-
|
|
917
|
+
* shell-load and runtime-load. Returns the NEWEST valid slice meta
|
|
918
|
+
* (each new tool-result supersedes the previous — every emission
|
|
919
|
+
* carries complete state, so last-wins loses nothing). Newest-first
|
|
920
|
+
* matters on the inline-runtime shell: its preflight completes the
|
|
921
|
+
* handshake BEFORE the large bundle parses, so a render AND a
|
|
922
|
+
* follow-up update can both land in the buffer during parse — booting
|
|
923
|
+
* the oldest would paint stale props and drop the update forever
|
|
924
|
+
* (buffered entries never reach the post-mount listener; it hears
|
|
925
|
+
* only post-App arrivals).
|
|
926
|
+
*
|
|
927
|
+
* The shell-side buffer contract: an array whose ELEMENTS are the raw
|
|
928
|
+
* JSON-RPC `params` values of `ui/notifications/tool-result`
|
|
929
|
+
* notifications, in arrival order (shells cap it newest-biased). The
|
|
930
|
+
* runtime reads it once at autostart.
|
|
931
|
+
*/
|
|
932
|
+
export function readPendingToolResults() {
|
|
873
933
|
if (typeof window === 'undefined')
|
|
874
934
|
return null;
|
|
875
935
|
const raw = window.__GGUI_PENDING_TOOL_RESULTS__;
|
|
876
936
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
877
937
|
return null;
|
|
878
|
-
for (
|
|
879
|
-
const meta = extractMetaFromToolResult(
|
|
938
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
939
|
+
const meta = extractMetaFromToolResult(raw[i]);
|
|
880
940
|
if (meta !== null)
|
|
881
941
|
return meta;
|
|
882
942
|
}
|
|
@@ -2609,6 +2669,10 @@ function installPostMountListener() {
|
|
|
2609
2669
|
meta.sessionId,
|
|
2610
2670
|
meta.kind ?? '-',
|
|
2611
2671
|
meta.codeUrl ?? '-',
|
|
2672
|
+
// Inline code participates like codeUrl: an update that ships new
|
|
2673
|
+
// component bytes (re-generation) must re-mount even when
|
|
2674
|
+
// sessionId + props are unchanged.
|
|
2675
|
+
meta.codeB64 ?? '-',
|
|
2612
2676
|
meta.propsJson ?? '-',
|
|
2613
2677
|
liveTrio,
|
|
2614
2678
|
].join('|');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/iframe-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"qrcode-generator": "^2.0.4",
|
|
36
36
|
"react": "^19.0.0",
|
|
37
37
|
"react-dom": "^19.0.0",
|
|
38
|
-
"@ggui-ai/live-channel": "0.
|
|
39
|
-
"@ggui-ai/gadgets": "0.
|
|
40
|
-
"@ggui-ai/design": "0.
|
|
41
|
-
"@ggui-ai/
|
|
42
|
-
"@ggui-ai/
|
|
43
|
-
"@ggui-ai/wire": "0.
|
|
38
|
+
"@ggui-ai/live-channel": "0.8.0",
|
|
39
|
+
"@ggui-ai/gadgets": "0.8.0",
|
|
40
|
+
"@ggui-ai/design": "0.8.0",
|
|
41
|
+
"@ggui-ai/protocol": "0.8.0",
|
|
42
|
+
"@ggui-ai/preview-a2ui": "0.8.0",
|
|
43
|
+
"@ggui-ai/wire": "0.8.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.27.0"
|