@ggui-ai/iframe-runtime 0.2.0-alpha.4 → 0.3.0-rc.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.
Files changed (57) hide show
  1. package/dist/channel-transport.d.ts +20 -30
  2. package/dist/channel-transport.js +24 -24
  3. package/dist/channels/data.d.ts +5 -20
  4. package/dist/channels/data.js +4 -52
  5. package/dist/channels/index.d.ts +0 -1
  6. package/dist/channels/index.js +0 -1
  7. package/dist/channels/props-update.d.ts +9 -9
  8. package/dist/channels/props-update.js +9 -9
  9. package/dist/channels/render-frame.d.ts +5 -5
  10. package/dist/channels/render-frame.js +5 -5
  11. package/dist/compiled-validators.d.ts +2 -2
  12. package/dist/compiled-validators.js +1 -1
  13. package/dist/context-observer.d.ts +4 -4
  14. package/dist/context-observer.js +3 -3
  15. package/dist/events-polling.d.ts +7 -7
  16. package/dist/events-polling.js +5 -5
  17. package/dist/globals.d.ts +1 -1
  18. package/dist/host-context-emitter.d.ts +4 -4
  19. package/dist/host-context-emitter.js +9 -9
  20. package/dist/iframe-runtime.js +68 -78
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +1 -1
  23. package/dist/lifecycle.d.ts +3 -3
  24. package/dist/lifecycle.js +29 -6
  25. package/dist/mcp-app-iframe-host.d.ts +8 -8
  26. package/dist/mcp-app-iframe-host.js +4 -4
  27. package/dist/meta-parse.d.ts +3 -4
  28. package/dist/meta-parse.js +17 -44
  29. package/dist/observability.d.ts +14 -36
  30. package/dist/observability.js +2 -1
  31. package/dist/protocol-error.d.ts +38 -48
  32. package/dist/protocol-error.js +5 -23
  33. package/dist/provisional-renderer.js +3 -3
  34. package/dist/react-renderer.d.ts +16 -6
  35. package/dist/react-renderer.js +22 -40
  36. package/dist/registry-subscribe.d.ts +7 -7
  37. package/dist/registry-subscribe.js +28 -14
  38. package/dist/render-item.d.ts +22 -15
  39. package/dist/render-item.js +7 -14
  40. package/dist/runtime.d.ts +79 -105
  41. package/dist/runtime.js +219 -269
  42. package/dist/status-dom.d.ts +12 -12
  43. package/dist/status-dom.js +11 -11
  44. package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
  45. package/dist/system-cards/NoCredentialsCard.js +1 -1
  46. package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
  47. package/dist/system-cards/ProtocolProbeCard.js +21 -25
  48. package/dist/system-cards/index.d.ts +5 -5
  49. package/dist/system-cards/index.js +5 -5
  50. package/dist/types.d.ts +29 -34
  51. package/dist/validation.d.ts +4 -5
  52. package/dist/validation.js +5 -6
  53. package/dist/wire-config.d.ts +43 -106
  54. package/dist/wire-config.js +29 -195
  55. package/package.json +12 -11
  56. package/dist/channels/feedback.d.ts +0 -14
  57. package/dist/channels/feedback.js +0 -19
package/dist/runtime.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, } from '@ggui-ai/protocol/integrations/mcp-apps';
1
2
  import { parseMetaFromGlobal, parseMetaFromToolResult, } from './meta-parse.js';
2
3
  import { projectHostContext } from '@ggui-ai/protocol';
3
4
  import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
@@ -5,7 +6,7 @@ import { applyHostContextStyling, attachListener as attachHostContextListener, s
5
6
  import { buildRootWireConfig, StreamBus, } from './wire-config.js';
6
7
  import { createChannelTransportRouter, } from './channel-transport.js';
7
8
  import { ChannelRegistry } from '@ggui-ai/live-channel';
8
- import { createChannelErrorHandler, createChannelPayloadHandler, createDataHandler, createDrainAckHandler, createFeedbackHandler, createPropsUpdateHandler, createRenderHandler, createSystemHandler, } from './channels/index.js';
9
+ import { createChannelErrorHandler, createChannelPayloadHandler, createDataHandler, createDrainAckHandler, createPropsUpdateHandler, createRenderHandler, createSystemHandler, } from './channels/index.js';
9
10
  import { connectViaRegistry, } from './registry-subscribe.js';
10
11
  import { buildEventsPolling } from './events-polling.js';
11
12
  import { ensureStatusDom, setStatus, setConnectedStatus, } from './status-dom.js';
@@ -16,15 +17,7 @@ import { fromBootstrapFailure, } from './protocol-error.js';
16
17
  import { postObservabilityToParent, } from './observability.js';
17
18
  import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
18
19
  import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
19
- // =============================================================================
20
- // Build-time-stamped renderer version. Surfaced to the parent in the
21
- // `ggui:renderer-ready` notification so hosts (and console views)
22
- // can correlate runtime behavior with a specific bundle.
23
- //
24
- // Hard-coded to the package.json version; future build tooling can
25
- // substitute via esbuild `define` if a non-package value is wanted.
26
- // =============================================================================
27
- const RENDERER_VERSION = '0.1.0';
20
+ const RENDERER_VERSION = typeof __GGUI_RUNTIME_VERSION__ === 'string' ? __GGUI_RUNTIME_VERSION__ : 'dev';
28
21
  /**
29
22
  * How long the spec-canonical postMessage tier (Tier 3 in
30
23
  * `bootSequence`'s resolver chain + the autostart resolver's race
@@ -82,7 +75,7 @@ const APP_INFO = { name: 'ggui-iframe-runtime', version: RENDERER_VERSION };
82
75
  * `ui/initialize` so spec-compliant hosts know `ui/request-display-mode`
83
76
  * requests for these values are honored. The runtime emits
84
77
  * `ui/request-display-mode` from the `Element.requestFullscreen`
85
- * interceptor + the canvas-mode display-mode escalation policy; both
78
+ * interceptor + the display-mode escalation policy; both
86
79
  * target this enum.
87
80
  */
88
81
  const APP_CAPABILITIES = {
@@ -100,14 +93,14 @@ function createDefaultApp() {
100
93
  return { app, transport };
101
94
  }
102
95
  /**
103
- * Module-level connected App handle. Set by `bootProduction` /
104
- * `bootSelfContained` once `app.connect()` resolves; consumed by the
96
+ * Module-level connected App handle. Set by `bootSequence` once
97
+ * `app.connect()` resolves; consumed by the
105
98
  * outbound dispatch path (`tools/call` via `app.callServerTool`) so
106
99
  * spec-compliant frame routing is used in place of the legacy
107
100
  * hand-rolled JSON-RPC pump.
108
101
  *
109
102
  * `null` between the moment the module loads and the moment one of
110
- * the boot paths assigns it — `dispatchWiredAction`'s callsites are
103
+ * the boot paths assigns it — `dispatchSubmitAction`'s callsites are
111
104
  * gated on this, so calls fired pre-boot drop with a console warning.
112
105
  */
113
106
  let currentApp = null;
@@ -117,8 +110,8 @@ let currentApp = null;
117
110
  * a replace only happens in tests that reuse the module across
118
111
  * scenarios (and want each spec to bind its own App).
119
112
  *
120
- * @internal — production callers are the boot paths
121
- * (`bootProduction`, `bootSelfContained`); tests inject directly to
113
+ * @internal — the production caller is `bootSequence` (reached via
114
+ * `bootProduction`); tests inject directly to
122
115
  * drive outbound `tools/call` through a `MockTransport`-bound App
123
116
  * without invoking the full boot pipeline.
124
117
  */
@@ -165,7 +158,11 @@ async function connectApp(app, transport) {
165
158
  */
166
159
  function postRendererReady() {
167
160
  try {
168
- window.parent.postMessage({ type: 'ggui:renderer-ready', version: RENDERER_VERSION }, '*');
161
+ const envelope = {
162
+ type: MCP_APP_RENDERER_READY_TYPE,
163
+ version: RENDERER_VERSION,
164
+ };
165
+ window.parent.postMessage(envelope, '*');
169
166
  }
170
167
  catch {
171
168
  // postMessage failure here means the parent is unreachable; the
@@ -177,7 +174,7 @@ function postRendererReady() {
177
174
  function postBootFailure(reason, message) {
178
175
  try {
179
176
  const envelope = {
180
- type: 'ggui:bootstrap-failed',
177
+ type: MCP_APP_BOOTSTRAP_FAILED_TYPE,
181
178
  reason,
182
179
  message,
183
180
  };
@@ -204,7 +201,7 @@ export async function bootSequence(opts) {
204
201
  // `onError` callback.
205
202
  const emitBootFailure = (reason, message) => {
206
203
  notifyParent({
207
- type: 'ggui:bootstrap-failed',
204
+ type: MCP_APP_BOOTSTRAP_FAILED_TYPE,
208
205
  // The legacy envelope's reason type is `RendererBootFailureReason`
209
206
  // (a subset of `BootstrapFailureReason`). Every value we emit here
210
207
  // is a member of that subset; the cast is a narrow-to-narrow
@@ -223,7 +220,7 @@ export async function bootSequence(opts) {
223
220
  // `BootSequenceResult`.
224
221
  let mountedRender = null;
225
222
  setStatus(refs, 'Negotiating with host…', 'connecting');
226
- notifyParent({ type: 'ggui:renderer-ready', version: RENDERER_VERSION });
223
+ notifyParent({ type: MCP_APP_RENDERER_READY_TYPE, version: RENDERER_VERSION });
227
224
  // Lifecycle `mounting` — the renderer is alive + status DOM is up,
228
225
  // but no IO has run yet. Hosts mirroring lifecycle to outer DOM see
229
226
  // this transition concurrent with `ggui:renderer-ready`. Idempotent
@@ -256,7 +253,7 @@ export async function bootSequence(opts) {
256
253
  return { ok: false, mountedRender };
257
254
  }
258
255
  // Connected — expose the App on the module-level slot so outbound
259
- // `tools/call` (dispatchWiredAction, channel-transport router)
256
+ // `tools/call` (dispatchSubmitAction, channel-transport router)
260
257
  // routes through `app.callServerTool` instead of the legacy raw
261
258
  // postMessage pump. Idempotent: re-boot with the same App is a
262
259
  // no-op; a different App throws (the iframe is single-tenant).
@@ -273,7 +270,7 @@ export async function bootSequence(opts) {
273
270
  //
274
271
  // Tier 1 parseMetaFromGlobal — synchronous `__GGUI_META__` inline
275
272
  // global. Self-contained shells (`buildSelfContainedHtml`,
276
- // per-session resource shells) populate this before this
273
+ // per-render resource shells) populate this before this
277
274
  // bundle's `<script type="module">` evaluates.
278
275
  // Opportunistic — its absence (the common case for
279
276
  // postMessage-delivered hosts) never surfaces as a parse
@@ -351,10 +348,10 @@ export async function bootSequence(opts) {
351
348
  setActiveValidatorSet(await loadCompiledValidatorsFromUrl(meta.validatorsUrl));
352
349
  // Pin — this iframe binds to exactly one render id for its lifetime
353
350
  // (post-render-identity-collapse each iframe = one mounted render,
354
- // per [[kill-displaymode-divergence]]). The bootstrap's `renderId`
351
+ // per [[kill-displaymode-divergence]]). The bootstrap's `sessionId`
355
352
  // is the pin; the render handler drops any frame addressed
356
353
  // elsewhere.
357
- const pinnedRenderId = meta.renderId;
354
+ const pinnedSessionId = meta.sessionId;
358
355
  // Renderer wiring — when supplied, the handler routes frames through
359
356
  // the single-render mount surface + WireConfig + StreamBus. When
360
357
  // absent, the placeholder path runs (boot.test.ts relies on the
@@ -365,12 +362,12 @@ export async function bootSequence(opts) {
365
362
  const renderer = rendererHooks !== undefined
366
363
  ? rendererHooks.setup({
367
364
  meta,
368
- renderInto: refs.stack,
365
+ renderInto: refs.renderRoot,
369
366
  statusRefs: refs,
370
367
  ...(onObserve !== undefined ? { onObserve } : {}),
371
368
  })
372
369
  : null;
373
- setStatus(refs, `Connecting to ${meta.renderId}…`, 'connecting');
370
+ setStatus(refs, `Connecting to ${meta.sessionId}…`, 'connecting');
374
371
  // Boot-without-renderer path: we still need a ChannelRegistry to
375
372
  // receive frames, because the registry is the only dispatch
376
373
  // surface. Build a minimal one with just the `render` placeholder
@@ -381,13 +378,13 @@ export async function bootSequence(opts) {
381
378
  ? createPlaceholderRegistry({
382
379
  meta,
383
380
  statusRefs: refs,
384
- pinnedRenderId,
381
+ pinnedSessionId,
385
382
  })
386
383
  : null;
387
384
  const activeRegistry = renderer?.channelRegistry ?? placeholderRegistry;
388
385
  /**
389
386
  * Apply an ack's render snapshot to the runtime — when the ack
390
- * carries a render matching `pinnedRenderId`, mount it through the
387
+ * carries a render matching `pinnedSessionId`, mount it through the
391
388
  * renderer. Used by:
392
389
  *
393
390
  * - The initial bootSequence path (first ack after subscribe).
@@ -399,13 +396,13 @@ export async function bootSequence(opts) {
399
396
  * Idempotent on identical inputs — `applyRender` patches in place
400
397
  * via {@link RenderItemHandle.update} when called with the same
401
398
  * render id; `channelTransport.applyRender` is server-side
402
- * idempotent on the (renderId, channelName) tuple.
399
+ * idempotent on the (sessionId, channelName) tuple.
403
400
  */
404
401
  const applyAck = async (ackPayload) => {
405
- const target = ackPayload.render;
402
+ const target = ackPayload.session;
406
403
  if (target === undefined)
407
404
  return;
408
- if (target.id !== pinnedRenderId) {
405
+ if (target.id !== pinnedSessionId) {
409
406
  // Server's snapshot is for a different render — likely a stale
410
407
  // re-subscribe after the server pruned ours. Nothing to mount.
411
408
  return;
@@ -415,7 +412,7 @@ export async function bootSequence(opts) {
415
412
  await renderer.applyRender(target);
416
413
  if (target.type !== 'mcpApps' && target.type !== 'system') {
417
414
  renderer.channelTransport.applyRender({
418
- renderId: target.id,
415
+ sessionId: target.id,
419
416
  ...(target.streamSpec !== undefined
420
417
  ? { streamSpec: target.streamSpec }
421
418
  : {}),
@@ -432,7 +429,7 @@ export async function bootSequence(opts) {
432
429
  if (codeReadyEmitted)
433
430
  return;
434
431
  codeReadyEmitted = true;
435
- onLifecycle?.(makeLifecycleEvent('code-ready', { renderId: meta.renderId }));
432
+ onLifecycle?.(makeLifecycleEvent('code-ready', { sessionId: meta.sessionId }));
436
433
  };
437
434
  // Mode discriminators. The mount surface is DECOUPLED from the live
438
435
  // channel: static content (codeUrl/kind) paints immediately with no WS;
@@ -450,7 +447,7 @@ export async function bootSequence(opts) {
450
447
  // ggui live channel (claude.ai / ChatGPT / Claude Desktop), AND the
451
448
  // instant first paint for first-party hosts that ALSO open a WS (the
452
449
  // WS ack then reconciles in place — `applyRender` is idempotent on the
453
- // pinned renderId, and the ack's componentCode is byte-identical to the
450
+ // pinned sessionId, and the ack's componentCode is byte-identical to the
454
451
  // seed's `codeUrl` bytes, so it's a props-only update, no remount).
455
452
  if (renderer !== null && hasStaticContent) {
456
453
  // Await the 3rd-party gadget merge before first paint when the
@@ -462,7 +459,7 @@ export async function bootSequence(opts) {
462
459
  }
463
460
  let seed = null;
464
461
  try {
465
- seed = await buildRenderSeedInput(meta);
462
+ seed = await buildGguiSessionSeedInput(meta);
466
463
  }
467
464
  catch (err) {
468
465
  const message = err instanceof Error ? err.message : String(err);
@@ -504,7 +501,7 @@ export async function bootSequence(opts) {
504
501
  // (no-op) echo. WS hosts get the real echo on the live-trio path below.
505
502
  if (parsed.hostContext !== undefined) {
506
503
  seedHostContext({
507
- renderId: meta.renderId,
504
+ sessionId: meta.sessionId,
508
505
  send: () => { },
509
506
  initial: parsed.hostContext,
510
507
  });
@@ -534,9 +531,8 @@ export async function bootSequence(opts) {
534
531
  // + version + protocol errors on its own, we just plumb them
535
532
  // along.
536
533
  ...(onProtocolError !== undefined ? { onProtocolError } : {}),
537
- // Forward observability emissions (schema-version-mismatch,
538
- // subscribe-failed). The renderer owns `contract-error-emitted`
539
- // via the data channel handler; connectFn owns the other two.
534
+ // Forward observability emissions — connectFn owns the
535
+ // schema-version-mismatch + subscribe-failed kinds.
540
536
  ...(onObserve !== undefined ? { onObserve } : {}),
541
537
  // Reconnect-with-rebootstrap — on every ack received AFTER the
542
538
  // initial handshake settled, reapply the server's authoritative
@@ -547,7 +543,7 @@ export async function bootSequence(opts) {
547
543
  },
548
544
  // R7 — registry-level events-polling fallback. Composed once at
549
545
  // bind time from `meta.pollingUrl` (server-stamped wsToken-
550
- // gated /api/renders/<id>/events URL) + `meta.lastSequence`
546
+ // gated /api/sessions/<id>/events URL) + `meta.lastSequence`
551
547
  // (cursor seed). FailoverHandle uses this when WS reaches
552
548
  // 'failed'; absent → no polling fallback (WS-only mode).
553
549
  //
@@ -599,7 +595,7 @@ export async function bootSequence(opts) {
599
595
  return { ok: false, mountedRender };
600
596
  }
601
597
  // Attach the live transport handle to the renderer — flushes any
602
- // buffered outbound frames (feedback / action) that were queued
598
+ // buffered outbound `action` frames that were queued
603
599
  // while the subscribe handshake completed. `handle.handle.send` is
604
600
  // the canonical send surface for the bound WS transport.
605
601
  if (renderer !== null && rendererHooks?.attachManager !== undefined) {
@@ -613,7 +609,7 @@ export async function bootSequence(opts) {
613
609
  // didn't emit a HostContext (parsed.hostContext === undefined).
614
610
  if (parsed.hostContext !== undefined) {
615
611
  seedHostContext({
616
- renderId: meta.renderId,
612
+ sessionId: meta.sessionId,
617
613
  send: (msg) => handle.handle.send(msg),
618
614
  initial: parsed.hostContext,
619
615
  });
@@ -624,7 +620,7 @@ export async function bootSequence(opts) {
624
620
  attachHostContextListener({ app });
625
621
  }
626
622
  // First ack — apply the server's render snapshot (when matching
627
- // `pinnedRenderId`) and mount it. Reuses the same `applyAck` helper
623
+ // `pinnedSessionId`) and mount it. Reuses the same `applyAck` helper
628
624
  // the reconnect-rebootstrap path uses — so a server-restart-driven
629
625
  // snapshot replay and the first-boot snapshot apply flow through
630
626
  // one implementation.
@@ -641,7 +637,7 @@ export async function bootSequence(opts) {
641
637
  /**
642
638
  * Build a minimal `ChannelRegistry` for boot paths without renderer
643
639
  * wiring (boot.test.ts + the C7a placeholder-only spec). The registry
644
- * carries just the `push` handler — which logs status but does not
640
+ * carries just the `render` handler — which logs status but does not
645
641
  * mount React — so consumers can observe bootstrap-orchestration
646
642
  * outcomes without paying React import cost. Every other frame type
647
643
  * silently drops. Production boots through `bootProduction` which
@@ -652,7 +648,7 @@ function createPlaceholderRegistry(params) {
652
648
  subscribeFrameBuilder: () => ({
653
649
  type: 'subscribe',
654
650
  payload: {
655
- renderId: params.meta.renderId,
651
+ sessionId: params.meta.sessionId,
656
652
  appId: params.meta.appId,
657
653
  ...(params.meta.wsToken !== undefined
658
654
  ? { wsToken: params.meta.wsToken }
@@ -662,25 +658,23 @@ function createPlaceholderRegistry(params) {
662
658
  });
663
659
  registry.register(createRenderHandler({
664
660
  statusRefs: params.statusRefs,
665
- pinnedRenderId: params.pinnedRenderId,
661
+ pinnedSessionId: params.pinnedSessionId,
666
662
  }));
667
663
  return registry;
668
664
  }
669
665
  /**
670
666
  * Frame dispatch lives inside `@ggui-ai/live-channel`'s
671
667
  * `ChannelRegistry`. Every WS frame type (`render`, `data`,
672
- * `props_update`, `drain_ack`, `feedback`, `channel_payload`,
668
+ * `props_update`, `drain_ack`, `channel_payload`,
673
669
  * `channel_error`, `system`) has a registered handler in
674
670
  * `channels/*.ts`; the registry's bound transport routes inbound
675
- * frames directly to them. Observability emissions
676
- * (`contract-error-emitted`, `auth-required`) are now inside the
677
- * data + system handlers respectively.
671
+ * frames directly to them. Observability emission (`auth-required`)
672
+ * is inside the system handler.
678
673
  *
679
674
  * The pre-B3b helpers (`handleServerMessage`, `handleRendererMessage`,
680
- * `handleObservableMessage`, `emitContractErrorFromDataFrame`,
681
- * `emitAuthRequiredFromSystemFrame`, `BufferedManagerShim`) lived
682
- * here and have been retired — see commit message + plan B3b for the
683
- * full retirement notes.
675
+ * `handleObservableMessage`, `emitAuthRequiredFromSystemFrame`,
676
+ * `BufferedManagerShim`) lived here and have been retired — see
677
+ * commit message + plan B3b for the full retirement notes.
684
678
  */
685
679
  /**
686
680
  * Type guard for the `UpgradeRequiredError` class without importing
@@ -701,6 +695,38 @@ function shouldAutostart() {
701
695
  return true;
702
696
  return process.env?.GGUI_RENDERER_AUTOSTART !== 'false';
703
697
  }
698
+ // =============================================================================
699
+ // Self-contained bootstrap (`window.__GGUI_META__`).
700
+ //
701
+ // The default boot path (postMessage `ui/initialize` → parse the
702
+ // `_meta["ai.ggui/render"]` slice → open WebSocket → subscribe →
703
+ // render from frames) is strictly first-party: it requires the host to
704
+ // speak ggui's custom postMessage protocol AND a reachable live-channel
705
+ // WebSocket the renderer can subscribe against. MCP Apps hosts in the
706
+ // wild (Claude Desktop, claude.ai web) speak only the canonical MCP
707
+ // Apps lifecycle and have no commitment to forward the slice key back
708
+ // through `ui/initialize`. The full first-party path stays intact for
709
+ // callers that own both ends; this self-contained path is what makes
710
+ // the same runtime bundle work in third-party MCP Apps hosts.
711
+ //
712
+ // Contract: when the embedding HTML inlines a global of shape
713
+ // { sessionId: string, appId: string, componentCode: string }
714
+ // (where `componentCode` is base64-encoded compiled ES module source
715
+ // of a React component) BEFORE this bundle's `<script type="module">`
716
+ // executes, the runtime takes over synchronously, mounts the compiled
717
+ // component, and never speaks postMessage / opens a WebSocket. The
718
+ // global is read at module load — a global set later (via deferred
719
+ // scripts, async imports) is too late and the runtime falls through
720
+ // to the legacy postMessage path.
721
+ //
722
+ // `componentCode` carries base64 because the embedding HTML inlines
723
+ // it as a JS string literal next to other `<script>` content; raw
724
+ // JS source contains every character that breaks string-literal
725
+ // embedding (quotes, backticks, `</script>`, newlines, backslash
726
+ // escapes). Base64 sidesteps every escape concern with a 4/3 size
727
+ // overhead that's negligible compared to the network/round-trip
728
+ // savings of skipping postMessage + WS bootstrap.
729
+ // =============================================================================
704
730
  /**
705
731
  * Read `globalThis.__GGUI_META__` synchronously, validate against
706
732
  * the unified slice-meta shape, and return the typed meta (or null
@@ -740,8 +766,8 @@ function parseSeedProps(propsJson) {
740
766
  }
741
767
  /**
742
768
  * Project the inline `__GGUI_META__` bootstrap into a
743
- * {@link RenderSeedInput} the mount surface can paint immediately,
744
- * BEFORE the authoritative wire `Render` arrives over the WS — and with
769
+ * {@link GguiSessionSeedInput} the mount surface can paint immediately,
770
+ * BEFORE the authoritative wire `GguiSession` arrives over the WS — and with
745
771
  * no WS at all for spec-compliant MCP-Apps hosts that expose no ggui
746
772
  * live channel (claude.ai / ChatGPT / Claude Desktop).
747
773
  *
@@ -754,14 +780,14 @@ function parseSeedProps(propsJson) {
754
780
  * nothing to seed — the first WS ack mounts it). Throws when a `codeUrl`
755
781
  * fetch fails so the caller can surface a typed boot failure. The four
756
782
  * server-assigned ledger fields are intentionally absent — the first
757
- * ack reconciles the seed to a full `Render` (no fabrication).
783
+ * ack reconciles the seed to a full `GguiSession` (no fabrication).
758
784
  */
759
- export async function buildRenderSeedInput(meta) {
785
+ export async function buildGguiSessionSeedInput(meta) {
760
786
  const props = parseSeedProps(meta.propsJson);
761
787
  // System-card mode — `kind` keyed against the built-in registry.
762
788
  if (meta.kind !== undefined) {
763
789
  return {
764
- id: meta.renderId,
790
+ id: meta.sessionId,
765
791
  appId: meta.appId,
766
792
  type: 'system',
767
793
  kind: meta.kind,
@@ -773,18 +799,18 @@ export async function buildRenderSeedInput(meta) {
773
799
  return null;
774
800
  const res = await fetch(meta.codeUrl);
775
801
  if (!res.ok) {
776
- throw new Error(`buildRenderSeedInput: codeUrl fetch failed (${res.status}): ${meta.codeUrl}`);
802
+ throw new Error(`buildGguiSessionSeedInput: codeUrl fetch failed (${res.status}): ${meta.codeUrl}`);
777
803
  }
778
804
  const componentCode = await res.text();
779
805
  return {
780
- id: meta.renderId,
806
+ id: meta.sessionId,
781
807
  appId: meta.appId,
782
808
  componentCode,
783
809
  ...(props !== undefined ? { props } : {}),
784
810
  };
785
811
  }
786
812
  /**
787
- * Extract a {@link SelfContainedMcpAppAiGguiMeta} from a
813
+ * Extract a {@link McpAppAiGguiRenderMeta} from a
788
814
  * `ui/notifications/tool-result` JSON-RPC params payload — the
789
815
  * postMessage delivery shape MCP Apps hosts (Claude Desktop, claude.ai
790
816
  * web) use to push the active tool's `_meta` to the iframe.
@@ -903,19 +929,19 @@ function awaitToolResultMeta(timeoutMs) {
903
929
  /**
904
930
  * Module-level guard for the App-mediated post-mount toolresult
905
931
  * re-listener. Ensures exactly one persistent
906
- * `app.addEventListener('toolresult', …)` registration even when
907
- * `bootSelfContained` is called multiple times (e.g. an agent fires a
908
- * second `ggui_render` and we re-mount).
932
+ * `app.addEventListener('toolresult', …)` registration even across
933
+ * re-mounts (e.g. an agent fires a second `ggui_render` and the
934
+ * listener re-applies through the published `applyRender`).
909
935
  */
910
936
  let postMountListenerInstalled = false;
911
937
  /**
912
938
  * Module-level handle to the active renderer's `applyRender`, published
913
939
  * by `bootProduction`'s `setup()` once the renderer is built. The
914
940
  * module-level {@link installPostMountListener} resolves this to re-mount
915
- * on a host-re-emitted tool-result WITHOUT a fresh boot — no second mount
916
- * stack, no second WS. This is the no-WS live-re-render channel for
941
+ * on a host-re-emitted tool-result WITHOUT a fresh boot — no second mount,
942
+ * no second WS. This is the no-WS live-re-render channel for
917
943
  * spec-compliant MCP-Apps hosts (claude.ai / ChatGPT / Claude Desktop)
918
- * that re-broadcast the tool-result instead of pushing a WS frame.
944
+ * that re-broadcast the tool-result instead of sending a WS render frame.
919
945
  *
920
946
  * `null` until a renderer is published (e.g. unit tests that drive
921
947
  * `bootSequence` with no renderer never set it → the listener no-ops).
@@ -933,12 +959,12 @@ let anchorClickInterceptInstalled = false;
933
959
  * Module-level guard for {@link installFullscreenInterceptors}. The
934
960
  * fullscreen interceptors REPLACE prototype methods on `Element` and
935
961
  * `Document`; re-overriding on every re-mount would chain wrappers
936
- * and (if the captured `args` differ) leak stale `renderId`/`appId`.
962
+ * and (if the captured `args` differ) leak stale `sessionId`/`appId`.
937
963
  * The guard ensures exactly one prototype patch.
938
964
  */
939
965
  let fullscreenInterceptInstalled = false;
940
966
  /**
941
- * Compute a short deterministic action-id from a wired-action
967
+ * Compute a short deterministic action-id from a submit-action
942
968
  * payload. FNV-1a 32-bit, 8 hex chars — not cryptographically
943
969
  * strong, just collision-resistant enough for in-flight
944
970
  * correlation between the silent context-update and the loud
@@ -953,7 +979,7 @@ function fnv1aHex(payload) {
953
979
  return (hash >>> 0).toString(16).padStart(8, '0');
954
980
  }
955
981
  /**
956
- * Render a wired-action's `data` payload as a short inline string
982
+ * Render a submit-action's `data` payload as a short inline string
957
983
  * for embedding in a `ui/message` consent prompt. Goal: human-
958
984
  * readable, not a JSON dump. Falls back to truncated JSON for
959
985
  * nested values so the prompt doesn't drop information silently.
@@ -964,7 +990,7 @@ function fnv1aHex(payload) {
964
990
  * the LLM think every dispatch was a contentless "Please proceed
965
991
  * with **<intent>**" request.
966
992
  */
967
- export function formatWiredActionDataInline(data) {
993
+ export function formatSubmitActionDataInline(data) {
968
994
  if (data === null || data === undefined)
969
995
  return '';
970
996
  // Bare primitives: render verbatim. Strings unquoted (most legible
@@ -1002,37 +1028,6 @@ export function formatWiredActionDataInline(data) {
1002
1028
  });
1003
1029
  return parts.join(', ');
1004
1030
  }
1005
- /**
1006
- * Dispatch a wired-action via the empirically-validated bridge
1007
- * chain (validated against claude.ai):
1008
- *
1009
- * 1. `tools/call` to {@link toolName} with the action envelope
1010
- * — server-side gateway log + audit. Hosts MUST honor
1011
- * `_meta.ui.visibility:['app']` per spec §401, otherwise the
1012
- * call is silently rejected (probe found this empirically).
1013
- * 2. `ui/update-model-context` (silent) — drops a structured
1014
- * `[ggui:pending-action]` payload into the LLM's persistent
1015
- * context. Carries the exact intent + data + actionId so the
1016
- * LLM has unambiguous tool args (no natural-language
1017
- * paraphrase risk).
1018
- * 3. `ui/message` (consent prompt) — natural-language
1019
- * authorization the user confirms in chat. Spec §1032 + §401
1020
- * together: this is the prompt-injection firewall — the
1021
- * iframe can ASK the LLM to act, but the user is the one who
1022
- * introduces the message into the LLM's context.
1023
- *
1024
- * Why all three: the probe empirically proved that `tools/call`
1025
- * alone never reaches the LLM (host-side scope:['app'] firewall),
1026
- * and `ui/message` alone forces the LLM to parse natural language
1027
- * for tool args (lossy + hallucination-prone). Pairing them with
1028
- * a hash-bound id (#3) gives the LLM both verifiable args AND
1029
- * explicit user authorization.
1030
- *
1031
- * Parts 2+3 fail-soft: if the host rejects either, the user can
1032
- * still observe the click via #1's audit log on the server side,
1033
- * but the LLM won't act on it. Acceptable degraded UX vs. the
1034
- * pre-bridge "silent button click" status quo.
1035
- */
1036
1031
  /**
1037
1032
  * Post an arbitrary JSON-RPC envelope to the iframe's parent
1038
1033
  * window. Internal helper shared across {@link emitAudit} (the
@@ -1125,7 +1120,7 @@ const productionContextSnapshotPoster = {
1125
1120
  params: {
1126
1121
  name: 'ggui_runtime_sync_context',
1127
1122
  arguments: {
1128
- renderId: params.renderId,
1123
+ sessionId: params.sessionId,
1129
1124
  appId: params.appId,
1130
1125
  snapshot: params.snapshot,
1131
1126
  },
@@ -1136,7 +1131,7 @@ const productionContextSnapshotPoster = {
1136
1131
  /**
1137
1132
  * Outbound `tools/call` shim. Routes through the spec-canonical
1138
1133
  * `app.callServerTool` API on the module-level App handle. Production
1139
- * always has the handle set (bootSequence / bootSelfContained call
1134
+ * always has the handle set (`bootSequence` calls
1140
1135
  * `setCurrentApp(app)` after handshake); tests that exercise dispatch
1141
1136
  * routing install one explicitly via {@link setCurrentApp} bound to
1142
1137
  * a `MockTransport`.
@@ -1179,10 +1174,10 @@ async function callServerToolSpec(toolName, args) {
1179
1174
  * Fire a single canonical action envelope (`tools/call
1180
1175
  * ggui_runtime_submit_action`) carrying the typed `{kind, payload, …}` shape
1181
1176
  * defined in `@ggui-ai/protocol/integrations/mcp-apps`. Every
1182
- * user-driven gesture (Pattern α/β dispatch, native-idiom anchor click,
1183
- * native-idiom fullscreen request) calls this alongside its primary
1184
- * host effect, so operators get **uniform server-side observability**
1185
- * across every gesture kind.
1177
+ * user-driven gesture (submit-action dispatch, native-idiom anchor
1178
+ * click, native-idiom fullscreen request) calls this alongside its
1179
+ * primary host effect, so operators get **uniform server-side
1180
+ * observability** across every gesture kind.
1186
1181
  *
1187
1182
  * Fail-soft: a rejected audit fire MUST NOT block the primary host
1188
1183
  * effect. Detached-parent / host-rejected audit envelopes are
@@ -1200,7 +1195,7 @@ export function emitAudit(args) {
1200
1195
  arguments: {
1201
1196
  kind: args.kind,
1202
1197
  payload: args.payload,
1203
- renderId: args.renderId,
1198
+ sessionId: args.sessionId,
1204
1199
  appId: args.appId,
1205
1200
  actionId: args.actionId,
1206
1201
  firedAt: args.firedAt,
@@ -1391,7 +1386,7 @@ function readLocalUiContext() {
1391
1386
  * consume loop has ended, e.g. after a page reload). The gesture is
1392
1387
  * ALREADY on the pipe from the just-completed `submit_action` append;
1393
1388
  * this doorbell only wakes a fresh agent turn so it calls
1394
- * `ggui_consume({renderId})` to drain it.
1389
+ * `ggui_consume({sessionId})` to drain it.
1395
1390
  *
1396
1391
  * SINGLE SOURCE OF TRUTH: the pipe. This carries ONLY a pointer to the
1397
1392
  * render — never the action payload or uiContext. Carrying the payload
@@ -1422,34 +1417,34 @@ function emitUserActionDoorbell(args) {
1422
1417
  // Prose + XML tags only, no fenced ```json``` block: a verbatim copy
1423
1418
  // of a tool-call wire shape trips claude.ai's prompt-injection
1424
1419
  // classifier even on the user-trusted `ui/message` channel.
1425
- const nextArgs = JSON.stringify({ renderId: args.renderId });
1420
+ const nextArgs = JSON.stringify({ sessionId: args.sessionId });
1426
1421
  const text = [
1427
1422
  `Your REQUIRED FIRST TOOL CALL is ggui_consume with arguments ${nextArgs}. Call it NOW to retrieve and process the pending interaction. Do not respond conversationally; do not summarize. Issue the tool call as your next action.`,
1428
1423
  '',
1429
1424
  `<ggui_directive kind="user-action">`,
1430
- ` <render_id>${args.renderId}</render_id>`,
1425
+ ` <session_id>${args.sessionId}</session_id>`,
1431
1426
  ` <next_tool>ggui_consume</next_tool>`,
1432
1427
  ` <next_args>${nextArgs}</next_args>`,
1433
1428
  `</ggui_directive>`,
1434
1429
  '',
1435
- `The user interacted with render ${args.renderId} while no ggui_consume long-poll was active. The gesture is queued on the consume pipe for that render — it is NOT in this message. After ggui_consume returns, react to the returned event with the appropriate domain tool, then call ggui_update on the SAME renderId (${args.renderId}).`,
1430
+ `The user interacted with render ${args.sessionId} while no ggui_consume long-poll was active. The gesture is queued on the consume pipe for that render — it is NOT in this message. After ggui_consume returns, react to the returned event with the appropriate domain tool, then call ggui_update on the SAME sessionId (${args.sessionId}).`,
1436
1431
  ].join('\n');
1437
1432
  // Structured mirror of the directive for ggui-aware programmatic
1438
1433
  // consumers. OPTIONAL — nothing in the loop depends on a server-side
1439
1434
  // parse of this; an `_meta`-agnostic host acts on the text above
1440
1435
  // alone. Typed against the protocol interface (no runtime guard
1441
1436
  // exists — the shape is locked at compile time here).
1442
- const description = `User interacted with render ${args.renderId}; call ggui_consume to retrieve and process it.`;
1437
+ const description = `User interacted with render ${args.sessionId}; call ggui_consume to retrieve and process it.`;
1443
1438
  const userAction = {
1444
1439
  kind: 'user-action',
1445
1440
  description,
1446
- renderId: args.renderId,
1441
+ sessionId: args.sessionId,
1447
1442
  actionId: args.actionId,
1448
1443
  submittedAt: args.submittedAt,
1449
1444
  intent: args.intent,
1450
1445
  nextStep: {
1451
1446
  tool: 'ggui_consume',
1452
- args: { renderId: args.renderId },
1447
+ args: { sessionId: args.sessionId },
1453
1448
  },
1454
1449
  };
1455
1450
  // RAW postMessage — NOT `app.sendMessage`. The doorbell MUST bypass
@@ -1494,14 +1489,26 @@ function emitUserActionDoorbell(args) {
1494
1489
  },
1495
1490
  });
1496
1491
  }
1497
- /** @internal — exported for unit tests. */
1498
- export function dispatchWiredAction(args) {
1492
+ /**
1493
+ * Dispatch a submit-action via the empirically-validated bridge
1494
+ * chain (validated against claude.ai): a `tools/call` to
1495
+ * {@link toolName} (the `ggui_runtime_submit_action` receiver)
1496
+ * appends the gesture to the render's consume pipe, paired with a
1497
+ * silent `ui/update-model-context` prime and — only when the server
1498
+ * reports no active `ggui_consume` long-poll — a `ui/message`
1499
+ * doorbell so a fresh agent turn drains the pipe. Hosts MUST honor
1500
+ * `_meta.ui.visibility:['app']` per spec §401, otherwise the
1501
+ * `tools/call` is silently rejected (probe found this empirically).
1502
+ *
1503
+ * @internal — exported for unit tests.
1504
+ */
1505
+ export function dispatchSubmitAction(args) {
1499
1506
  if (typeof window === 'undefined')
1500
1507
  return;
1501
- const { toolName, intent, data, renderId, appId } = args;
1508
+ const { toolName, intent, data, sessionId, appId } = args;
1502
1509
  const firedAt = new Date().toISOString();
1503
1510
  const actionId = fnv1aHex(`${intent}|${JSON.stringify(data ?? null)}|${firedAt}`);
1504
- const inlineData = formatWiredActionDataInline(data);
1511
+ const inlineData = formatSubmitActionDataInline(data);
1505
1512
  const dataPart = inlineData === '' ? '' : ` (${inlineData})`;
1506
1513
  const uiContext = readLocalUiContext();
1507
1514
  // (1) Silent context update — fires FIRST and ALWAYS. Primes the
@@ -1517,7 +1524,7 @@ export function dispatchWiredAction(args) {
1517
1524
  intent,
1518
1525
  data: data ?? null,
1519
1526
  firedAt,
1520
- renderId,
1527
+ sessionId,
1521
1528
  appId,
1522
1529
  })}`,
1523
1530
  },
@@ -1556,7 +1563,7 @@ export function dispatchWiredAction(args) {
1556
1563
  actionData: data ?? null,
1557
1564
  uiContext,
1558
1565
  },
1559
- renderId,
1566
+ sessionId,
1560
1567
  appId,
1561
1568
  actionId,
1562
1569
  firedAt,
@@ -1572,7 +1579,7 @@ export function dispatchWiredAction(args) {
1572
1579
  showActionToast(`💬 ${intent}${dataPart} — agent not listening, sent to chat`, 'action_required');
1573
1580
  emitUserActionDoorbell({
1574
1581
  intent,
1575
- renderId,
1582
+ sessionId,
1576
1583
  actionId,
1577
1584
  submittedAt: firedAt,
1578
1585
  });
@@ -1590,57 +1597,8 @@ export function dispatchWiredAction(args) {
1590
1597
  })();
1591
1598
  }
1592
1599
  /**
1593
- * Fire a wired action via Pattern α — direct `tools/call` against a
1594
- * same-server, app-visible target tool. The iframe is allowed to fire
1595
- * the tool directly from this server connection per MCP-Apps spec
1596
- * §2026-01-26 visibility rules. Skips the submit_action pipe entirely:
1597
- * the tool fires without the agent's involvement (the whole point of
1598
- * Pattern α), chat stays clean.
1599
- *
1600
- * PIPE-2 design note: Pattern α deliberately does NOT fire
1601
- * `submit_action` — a pipe append would queue the gesture for the
1602
- * agent's `ggui_consume` long-poll, causing double-processing
1603
- * (host's tool relay AND agent's reaction). RenderInspector loses
1604
- * Pattern α observability for now; re-add via a dedicated audit-only
1605
- * gesture kind if operators need it.
1606
- *
1607
- * @internal — exported for unit tests.
1608
- */
1609
- export function fireDirectToolCall(args) {
1610
- if (typeof window === 'undefined')
1611
- return;
1612
- const { targetToolName, data } = args;
1613
- // Direct tools/call — fires on the same MCP server connection the
1614
- // iframe was bootstrapped against. Spec §2026-01-26: "app"
1615
- // visibility tools are callable by the app from the same server
1616
- // connection only.
1617
- postToParent({
1618
- jsonrpc: '2.0',
1619
- id: Math.floor(Math.random() * 1e9),
1620
- method: 'tools/call',
1621
- params: {
1622
- name: targetToolName,
1623
- arguments: (data ?? {}),
1624
- },
1625
- });
1626
- }
1627
- /**
1628
- * Pure routing helper for `WireConfig.dispatch` — chooses Pattern α
1629
- * (direct `tools/call`) when the action's wired tool is in
1630
- * `appCallableTools`, otherwise falls back to Pattern β (the 3-message
1631
- * bridge).
1632
- *
1633
- * The routing logic was previously inlined as a closure body inside
1634
- * `bootSelfContained`'s `wireConfig.dispatch`, with a verbatim
1635
- * re-creation in `__tests__/dispatch-routing.test.ts`. Tests could
1636
- * pass against stale code if either drifted. Extraction means
1637
- * production + tests now exercise the same code path.
1638
- *
1639
- * Pattern α (same-server, app-visible target tool): chat stays clean,
1640
- * no LLM consent loop. Pattern β (anything else: cross-server tool, no
1641
- * wired tool, or tool not in `appCallableTools`): the canonical
1642
- * 3-message workaround the LLM brokers when the tool isn't directly
1643
- * callable from the iframe.
1600
+ * Resolve the name of the spec-canonical submit-action receiver tool
1601
+ * the dispatch path targets with `tools/call`.
1644
1602
  *
1645
1603
  * @internal — exported for unit tests + production reuse.
1646
1604
  */
@@ -1659,34 +1617,28 @@ export function resolveDispatchToolName() {
1659
1617
  : 'ggui_runtime_submit_action';
1660
1618
  }
1661
1619
  /**
1620
+ * Routing helper for `WireConfig.dispatch` — every user action takes
1621
+ * the same path: enqueue the gesture onto the render's pending-event
1622
+ * pipe via `submit_action`. The agent retrieves it EXCLUSIVELY via
1623
+ * `ggui_consume` — the gesture never travels inline, so there is no
1624
+ * `nextStep` hint to forward client-side (the server derives the
1625
+ * agent-facing hint from the render's `actionSpec[name].nextStep`
1626
+ * when it builds the consume event).
1627
+ *
1628
+ * Lives as a named export (rather than a closure body inside the
1629
+ * boot path) so production + tests exercise the same code path.
1630
+ *
1662
1631
  * @internal — exported for unit tests + production reuse.
1663
1632
  */
1664
1633
  export function routeDispatch(args) {
1665
1634
  const { actionName, data, meta, dispatchToolName } = args;
1666
- const tool = meta.actionNextSteps?.[actionName];
1667
- const appCallable = typeof tool === 'string' &&
1668
- (meta.appCallableTools ?? []).includes(tool);
1669
- if (appCallable && tool !== undefined) {
1670
- fireDirectToolCall({
1671
- targetToolName: tool,
1672
- data,
1673
- });
1674
- }
1675
- else {
1676
- // Pattern β: enqueue the gesture onto the render's pending-event
1677
- // pipe via `submit_action`. The agent retrieves it EXCLUSIVELY via
1678
- // `ggui_consume` — the gesture never travels inline, so there is no
1679
- // `nextStep` hint to forward (the doorbell carries only a pointer to
1680
- // the render). This branch is also where
1681
- // `actionSpec[*].dispatch.kind === 'agent'` lands.
1682
- dispatchWiredAction({
1683
- toolName: dispatchToolName,
1684
- intent: actionName,
1685
- data,
1686
- renderId: meta.renderId,
1687
- appId: meta.appId,
1688
- });
1689
- }
1635
+ dispatchSubmitAction({
1636
+ toolName: dispatchToolName,
1637
+ intent: actionName,
1638
+ data,
1639
+ sessionId: meta.sessionId,
1640
+ appId: meta.appId,
1641
+ });
1690
1642
  }
1691
1643
  /**
1692
1644
  * `ui/open-link` direct dispatch helper — used by the anchor-click
@@ -1697,7 +1649,7 @@ export function routeDispatch(args) {
1697
1649
  export function openLinkInParent(args) {
1698
1650
  if (typeof window === 'undefined')
1699
1651
  return;
1700
- const { toolName, url, renderId, appId } = args;
1652
+ const { toolName, url, sessionId, appId } = args;
1701
1653
  if (typeof url !== 'string' || url.length === 0) {
1702
1654
  throw new RangeError('wire.openLink(url): `url` must be a non-empty string.');
1703
1655
  }
@@ -1707,7 +1659,7 @@ export function openLinkInParent(args) {
1707
1659
  toolName,
1708
1660
  kind: 'openLink',
1709
1661
  payload: { url },
1710
- renderId,
1662
+ sessionId,
1711
1663
  appId,
1712
1664
  actionId,
1713
1665
  firedAt,
@@ -1724,14 +1676,14 @@ export function openLinkInParent(args) {
1724
1676
  export function requestDisplayModeInParent(args) {
1725
1677
  if (typeof window === 'undefined')
1726
1678
  return;
1727
- const { toolName, mode, renderId, appId } = args;
1679
+ const { toolName, mode, sessionId, appId } = args;
1728
1680
  const firedAt = new Date().toISOString();
1729
1681
  const actionId = fnv1aHex(`requestDisplayMode|${mode}|${firedAt}`);
1730
1682
  emitAudit({
1731
1683
  toolName,
1732
1684
  kind: 'requestDisplayMode',
1733
1685
  payload: { mode },
1734
- renderId,
1686
+ sessionId,
1735
1687
  appId,
1736
1688
  actionId,
1737
1689
  firedAt,
@@ -1781,7 +1733,7 @@ export function installAnchorClickInterceptor(args) {
1781
1733
  if (typeof document === 'undefined')
1782
1734
  return;
1783
1735
  anchorClickInterceptInstalled = true;
1784
- const { dispatchToolName, renderId, appId } = args;
1736
+ const { dispatchToolName, sessionId, appId } = args;
1785
1737
  const onClick = (event) => {
1786
1738
  if (event.defaultPrevented)
1787
1739
  return;
@@ -1826,7 +1778,7 @@ export function installAnchorClickInterceptor(args) {
1826
1778
  openLinkInParent({
1827
1779
  toolName: dispatchToolName,
1828
1780
  url: absoluteHref,
1829
- renderId,
1781
+ sessionId,
1830
1782
  appId,
1831
1783
  });
1832
1784
  };
@@ -1866,7 +1818,7 @@ export function installAnchorClickInterceptor(args) {
1866
1818
  *
1867
1819
  * Idempotent: the {@link fullscreenInterceptInstalled} guard prevents
1868
1820
  * re-mounts from chaining wrappers (which would also leak the prior
1869
- * mount's `renderId`/`appId` if they ever differed).
1821
+ * mount's `sessionId`/`appId` if they ever differed).
1870
1822
  */
1871
1823
  /** @internal — exported for unit tests. */
1872
1824
  export function installFullscreenInterceptors(args) {
@@ -1878,12 +1830,12 @@ export function installFullscreenInterceptors(args) {
1878
1830
  return;
1879
1831
  }
1880
1832
  fullscreenInterceptInstalled = true;
1881
- const { dispatchToolName, renderId, appId } = args;
1833
+ const { dispatchToolName, sessionId, appId } = args;
1882
1834
  Element.prototype.requestFullscreen = function (_options) {
1883
1835
  requestDisplayModeInParent({
1884
1836
  toolName: dispatchToolName,
1885
1837
  mode: 'fullscreen',
1886
- renderId,
1838
+ sessionId,
1887
1839
  appId,
1888
1840
  });
1889
1841
  return Promise.resolve();
@@ -1892,7 +1844,7 @@ export function installFullscreenInterceptors(args) {
1892
1844
  requestDisplayModeInParent({
1893
1845
  toolName: dispatchToolName,
1894
1846
  mode: 'inline',
1895
- renderId,
1847
+ sessionId,
1896
1848
  appId,
1897
1849
  });
1898
1850
  return Promise.resolve();
@@ -1912,24 +1864,24 @@ export function __resetInterceptorsForTest() {
1912
1864
  * Install a persistent `app.addEventListener('toolresult', …)` listener
1913
1865
  * that catches `ui/notifications/tool-result` notifications arriving
1914
1866
  * AFTER the initial mount. Each new tool-result that carries a
1915
- * different bootstrap (different renderId / codeUrl / kind) triggers
1916
- * a re-mount via {@link bootSelfContained}. This closes the boot-only-
1867
+ * different bootstrap (different sessionId / codeUrl / kind) triggers
1868
+ * a re-mount through the published `applyRender`. This closes the boot-only-
1917
1869
  * listener gap that prevented live re-render when an agent issued
1918
1870
  * a second `ggui_render` to the same render-resource.
1919
1871
  *
1920
1872
  * Idempotent: subsequent calls no-op via {@link postMountListenerInstalled}.
1921
1873
  *
1922
- * Why module-level vs scoped to a single mount: bootSelfContained may
1923
- * itself trigger a re-mount, and the listener should outlive any
1924
- * single mount cycle. The guard ensures we don't stack listeners
1925
- * across re-mounts.
1874
+ * Why module-level vs scoped to a single mount: re-mounts re-apply
1875
+ * through `applyRender` while the listener stays live, and the
1876
+ * listener should outlive any single mount cycle. The guard ensures
1877
+ * we don't stack listeners across re-mounts.
1926
1878
  *
1927
1879
  * Spec-canonical (post-Phase-1.19b.3): the previous hand-rolled
1928
1880
  * `window.addEventListener('message', …)` is gone; App handles every
1929
1881
  * inbound `ui/notifications/tool-result` envelope and dispatches via
1930
1882
  * its event system. Per-iframe single-tenancy means the App handle
1931
- * comes from `getCurrentApp()` — set by bootSelfContained's own
1932
- * connect path before this helper runs.
1883
+ * comes from `getCurrentApp()` — set by `bootSequence`'s connect
1884
+ * path before this helper runs.
1933
1885
  */
1934
1886
  function installPostMountListener() {
1935
1887
  if (postMountListenerInstalled)
@@ -1952,9 +1904,9 @@ function installPostMountListener() {
1952
1904
  // claude.ai) often emit the initial meta WITHOUT the wsUrl+token
1953
1905
  // pair (the Anthropic SDK strips `_meta` from tool results), then
1954
1906
  // refetch + re-emit the FULL envelope. The two envelopes share
1955
- // renderId/kind/codeUrl/propsJson but differ on the live trio —
1907
+ // sessionId/kind/codeUrl/propsJson but differ on the live trio —
1956
1908
  // without trio in the key, the second arrival deduped silently and
1957
- // bootSelfContained never opened the WS, so `ggui_update` props_update
1909
+ // the re-mount never opened the WS, so `ggui_update` props_update
1958
1910
  // frames fanned to zero subscribers.
1959
1911
  const liveTrio = typeof meta.wsUrl === 'string' &&
1960
1912
  meta.wsUrl.length > 0 &&
@@ -1963,7 +1915,7 @@ function installPostMountListener() {
1963
1915
  ? 'live'
1964
1916
  : '-';
1965
1917
  const key = [
1966
- meta.renderId,
1918
+ meta.sessionId,
1967
1919
  meta.kind ?? '-',
1968
1920
  meta.codeUrl ?? '-',
1969
1921
  meta.propsJson ?? '-',
@@ -1980,7 +1932,7 @@ function installPostMountListener() {
1980
1932
  const apply = activeApplyRender;
1981
1933
  if (apply === null)
1982
1934
  return; // no renderer published yet — no-op safely
1983
- void buildRenderSeedInput(meta).then((seed) => {
1935
+ void buildGguiSessionSeedInput(meta).then((seed) => {
1984
1936
  if (seed === null)
1985
1937
  return;
1986
1938
  void apply(seed).then(() => {
@@ -1990,7 +1942,7 @@ function installPostMountListener() {
1990
1942
  // the regular debounced flow once the component mutates values.
1991
1943
  // Filter to slot names declared by the FRESHLY mounted contract.
1992
1944
  const activeSlotNames = new Set((meta.contextSlots ?? []).map((s) => s.name));
1993
- reemitLastContextValues(productionContextSnapshotPoster, activeSlotNames, { renderId: meta.renderId, appId: meta.appId });
1945
+ reemitLastContextValues(productionContextSnapshotPoster, activeSlotNames, { sessionId: meta.sessionId, appId: meta.appId });
1994
1946
  });
1995
1947
  });
1996
1948
  });
@@ -1998,7 +1950,7 @@ function installPostMountListener() {
1998
1950
  /**
1999
1951
  * Detect a live-channel bootstrap shape inlined onto `__GGUI_META__`.
2000
1952
  * The first-party render shells (`/r/<shortCode>`,
2001
- * `ui://ggui/render/<renderId>`, the embedded-ui RenderViewer's thin
1953
+ * `ui://ggui/render/<sessionId>`, the embedded-ui GguiSessionViewer's thin
2002
1954
  * shell) populate this synchronously before the runtime loads.
2003
1955
  *
2004
1956
  * Post-Phase-B the global carries a slice ENVELOPE (same shape as the
@@ -2032,8 +1984,8 @@ function readLiveBootstrapShape() {
2032
1984
  renderBag['wsUrl'].length > 0 &&
2033
1985
  typeof renderBag['wsToken'] === 'string' &&
2034
1986
  renderBag['wsToken'].length > 0 &&
2035
- typeof renderBag['renderId'] === 'string' &&
2036
- renderBag['renderId'].length > 0 &&
1987
+ typeof renderBag['sessionId'] === 'string' &&
1988
+ renderBag['sessionId'].length > 0 &&
2037
1989
  typeof renderBag['appId'] === 'string' &&
2038
1990
  renderBag['appId'].length > 0);
2039
1991
  }
@@ -2059,7 +2011,7 @@ function runBootProduction(preResolvedMeta) {
2059
2011
  app,
2060
2012
  transport,
2061
2013
  notifyParent: (msg) => {
2062
- if (msg.type === 'ggui:renderer-ready') {
2014
+ if (msg.type === MCP_APP_RENDERER_READY_TYPE) {
2063
2015
  postRendererReady();
2064
2016
  return;
2065
2017
  }
@@ -2124,8 +2076,9 @@ async function bootProduction(opts) {
2124
2076
  import('@ggui-ai/design/interact'),
2125
2077
  import('@ggui-ai/design/tokens'),
2126
2078
  import('@ggui-ai/wire'),
2127
- // STDLIB gadget hooks. See the `bootSelfContained` callsite
2128
- // above for the rationale.
2079
+ // STDLIB gadget hooks — seeds `__ggui__.gadgets` synchronously in
2080
+ // the registry composition below, so a generated component's
2081
+ // gadget data-URL shims resolve at mount time.
2129
2082
  import('@ggui-ai/gadgets'),
2130
2083
  ]);
2131
2084
  const [globalsMod, gadgetLoaderMod] = await Promise.all([
@@ -2136,11 +2089,10 @@ async function bootProduction(opts) {
2136
2089
  // + wire config on demand inside bootSequence.
2137
2090
  const renderer = {
2138
2091
  setup: ({ meta, renderInto, statusRefs, onObserve }) => {
2139
- // Post-Phase-B `meta` is the flat render slice — `renderId` /
2092
+ // Post-Phase-B `meta` is the flat render slice — `sessionId` /
2140
2093
  // `appId` / `runtimeUrl` / `wsUrl` / `wsToken` / `themeId` /
2141
- // `gadgets` / `publicEnv` / `contextSlots` / `actionNextSteps` /
2142
- // `appCallableTools` / `streamWebSocketLocalTools` all live
2143
- // directly on `meta`.
2094
+ // `gadgets` / `publicEnv` / `contextSlots` /
2095
+ // `streamWebSocketLocalTools` all live directly on `meta`.
2144
2096
  // Compose the gadget registry: STDLIB seed PLUS any
2145
2097
  // operator-registered wrappers carried on the bootstrap.
2146
2098
  // Awaited inside the synchronous `setup` callback via an IIFE
@@ -2196,7 +2148,7 @@ async function bootProduction(opts) {
2196
2148
  });
2197
2149
  // Install the React Context registry + build a ContextStateHost
2198
2150
  // so the WS-driven shells (Studio, Portal, OSS console)
2199
- // participate in contextSpec the same way `bootSelfContained`
2151
+ // participate in contextSpec the same way the static-seed mount
2200
2152
  // does. Without this the WS-driven path installed neither —
2201
2153
  // `globalThis.__ggui__.contexts` stayed empty, the boilerplate's
2202
2154
  // destructure resolved to `undefined`, and any declared
@@ -2215,7 +2167,7 @@ async function bootProduction(opts) {
2215
2167
  ? console.warn.bind(console)
2216
2168
  : undefined,
2217
2169
  identity: {
2218
- renderId: meta.renderId,
2170
+ sessionId: meta.sessionId,
2219
2171
  appId: meta.appId,
2220
2172
  },
2221
2173
  });
@@ -2247,17 +2199,18 @@ async function bootProduction(opts) {
2247
2199
  };
2248
2200
  // Spec-canonical outbound dispatch. The WS pipe is for streamSpec
2249
2201
  // subscriptions ONLY (inbound `ggui_emit` fanout + `props_update` +
2250
- // `render` + `data` + `feedback` + `drain_ack` + `channel_payload`).
2202
+ // `render` + `data` + `drain_ack` + `channel_payload`).
2251
2203
  // Outbound user actions go through the MCP-Apps host relay per
2252
2204
  // spec §401: postMessage `tools/call:ggui_runtime_submit_action`
2253
2205
  // to the parent → `AppRenderer.onCallTool` → sample agent's
2254
2206
  // `/relay/tools-call` → ggui MCP server →
2255
2207
  // `createGguiSubmitActionHandler.append` → `pendingEventConsumer`
2256
2208
  // → `ggui_consume` wakes the agent. The server's WS
2257
- // `handleInboundAction` writes to the session ledger only — no
2209
+ // `handleInboundAction` writes to the render ledger only — no
2258
2210
  // downstream consumer — so the WS action path silently drops
2259
- // clicks. `routeDispatch` is the same helper `bootSelfContained`
2260
- // uses; threading it here aligns LIVE-mode with self-contained.
2211
+ // clicks. `routeDispatch` is the shared named-export helper
2212
+ // (production + tests exercise the same code path); threading it
2213
+ // here keeps LIVE-mode on the canonical dispatch pipeline.
2261
2214
  // Single mounted render — populated by `applyRender` on the first
2262
2215
  // render frame. The wire config + data channel handler read it
2263
2216
  // through `currentRender`-returning thunks so they always see the
@@ -2271,26 +2224,25 @@ async function bootProduction(opts) {
2271
2224
  // through this single surface — the WS-ack render, the inline
2272
2225
  // static seed, and re-mounts — has anchor-click + fullscreen
2273
2226
  // capture live before the first `applyRender`. Pre-consolidation
2274
- // these lived only in `bootSelfContained`; the WS-driven path
2275
- // installed neither, so anchor clicks + fullscreen requests in a
2276
- // live-rendered component silently no-op'd.
2227
+ // these lived only in the retired self-contained boot path; the
2228
+ // WS-driven path installed neither, so anchor clicks + fullscreen
2229
+ // requests in a live-rendered component silently no-op'd.
2277
2230
  installAnchorClickInterceptor({
2278
2231
  dispatchToolName,
2279
- renderId: meta.renderId,
2232
+ sessionId: meta.sessionId,
2280
2233
  appId: meta.appId,
2281
2234
  });
2282
2235
  installFullscreenInterceptors({
2283
2236
  dispatchToolName,
2284
- renderId: meta.renderId,
2237
+ sessionId: meta.sessionId,
2285
2238
  appId: meta.appId,
2286
2239
  });
2287
2240
  const rootConfig = buildRootWireConfig({
2288
- renderId: meta.renderId,
2241
+ sessionId: meta.sessionId,
2289
2242
  appId: meta.appId,
2290
- getCurrentRender: () => currentRender,
2243
+ getCurrentGguiSession: () => currentRender,
2291
2244
  manager,
2292
2245
  streamBus,
2293
- ...(onObserve !== undefined ? { onObserve } : {}),
2294
2246
  onDispatchEnvelope: (envelope) => {
2295
2247
  if (envelope.type !== 'data:submit')
2296
2248
  return;
@@ -2304,14 +2256,8 @@ async function bootProduction(opts) {
2304
2256
  actionName: payload.action,
2305
2257
  data: payload.data,
2306
2258
  meta: {
2307
- renderId: meta.renderId,
2259
+ sessionId: meta.sessionId,
2308
2260
  appId: meta.appId,
2309
- ...(meta.appCallableTools !== undefined
2310
- ? { appCallableTools: meta.appCallableTools }
2311
- : {}),
2312
- ...(meta.actionNextSteps !== undefined
2313
- ? { actionNextSteps: meta.actionNextSteps }
2314
- : {}),
2315
2261
  },
2316
2262
  dispatchToolName,
2317
2263
  });
@@ -2338,7 +2284,7 @@ async function bootProduction(opts) {
2338
2284
  // host has its own contract (adapter-boundary rule).
2339
2285
  // Post-render-identity-collapse the WireConfig is bound to the
2340
2286
  // single render at boot, so there's no per-render scope factory
2341
- // — every dispatch resolves through `getCurrentRender`.
2287
+ // — every dispatch resolves through `getCurrentGguiSession`.
2342
2288
  const buildScopedWireFor = (render) => {
2343
2289
  if (render.type === 'mcpApps' || render.type === 'system')
2344
2290
  return null;
@@ -2350,19 +2296,18 @@ async function bootProduction(opts) {
2350
2296
  // Without this, react-renderer.ts falls back to
2351
2297
  // `getScopedCssTokens` (no preset) and the iframe renders with
2352
2298
  // the default ggui theme even when `_meta["ai.ggui/render"].themeId`
2353
- // is `'indigo'`. Sibling `bootSelfContained` path threads the
2354
- // same fields onto its `mountOpts` for parity.
2299
+ // is `'indigo'`.
2355
2300
  const buildOpts = (render) => {
2356
2301
  const wrapOuter = buildOuterWrapper(render);
2357
2302
  return {
2358
2303
  render,
2359
2304
  scopedWireConfig: buildScopedWireFor(render),
2360
2305
  streamBus,
2361
- renderId: meta.renderId,
2306
+ sessionId: meta.sessionId,
2362
2307
  // Thread the bootstrap's 3rd-party gadget packages so the import
2363
2308
  // rewriter resolves non-STDLIB gadget imports even for a static
2364
2309
  // seed mount (which carries no gadgetDescriptors). A full
2365
- // WS-delivered Render carries gadgetDescriptors and ignores this.
2310
+ // WS-delivered GguiSession carries gadgetDescriptors and ignores this.
2366
2311
  ...(meta.gadgets !== undefined
2367
2312
  ? { gadgetPackages: meta.gadgets.map((g) => g.package) }
2368
2313
  : {}),
@@ -2370,6 +2315,13 @@ async function bootProduction(opts) {
2370
2315
  ...(meta.themeMode !== undefined
2371
2316
  ? { themeMode: meta.themeMode }
2372
2317
  : {}),
2318
+ // Per-app theme overlay (St3 M2.2). Threaded straight from the
2319
+ // bootstrap's `_meta["ai.ggui/render"].theme` (typed `AppTheme`,
2320
+ // already injection-validated by the wire parser) onto the mount
2321
+ // options so the renderer applies the `--ggui-*` overrides +
2322
+ // `color-scheme` at `:root`. THEME IS COMPONENT-ONLY — system
2323
+ // cards theme via the SystemCardHost/ThemeProvider path.
2324
+ ...(meta.theme !== undefined ? { appTheme: meta.theme } : {}),
2373
2325
  ...(wrapOuter !== undefined ? { wrapOuter } : {}),
2374
2326
  };
2375
2327
  };
@@ -2399,7 +2351,7 @@ async function bootProduction(opts) {
2399
2351
  // the listener re-mounts through THIS `applyRender` — no fresh boot,
2400
2352
  // no second WS. The no-WS live-re-render channel; for WS hosts the
2401
2353
  // render-frame handler already covers re-render, so it's a
2402
- // redundant-safe fallback (guarded by the renderId pin + liveTrio
2354
+ // redundant-safe fallback (guarded by the sessionId pin + liveTrio
2403
2355
  // dedupe). Runs only on the production renderer path (tests that
2404
2356
  // drive bootSequence without a renderer never reach setup()).
2405
2357
  activeApplyRender = applyRender;
@@ -2420,7 +2372,7 @@ async function bootProduction(opts) {
2420
2372
  // frame handler (which calls `channelTransport.applyRender` on
2421
2373
  // every render fold).
2422
2374
  const channelTransport = createChannelTransportRouter({
2423
- renderId: meta.renderId,
2375
+ sessionId: meta.sessionId,
2424
2376
  appId: meta.appId,
2425
2377
  ...(meta.streamWebSocketLocalTools !== undefined
2426
2378
  ? {
@@ -2432,8 +2384,8 @@ async function bootProduction(opts) {
2432
2384
  // Iframe-polling transport — `tools/call` against the
2433
2385
  // parent MCP host via `app.callServerTool` (spec-canonical)
2434
2386
  // when the App handle is set; falls back to raw postMessage
2435
- // pre-handshake or in tests. Pattern α direct call (no LLM
2436
- // consent loop). Returns the tool's structuredContent (or
2387
+ // pre-handshake or in tests. Direct call (no LLM consent
2388
+ // loop). Returns the tool's structuredContent (or
2437
2389
  // `content[0]` if that's where the payload landed) as a
2438
2390
  // JsonValue. On RPC error we throw — the router catches
2439
2391
  // and silently retries on the next tick.
@@ -2476,7 +2428,7 @@ async function bootProduction(opts) {
2476
2428
  subscribeFrameBuilder: () => ({
2477
2429
  type: 'subscribe',
2478
2430
  payload: {
2479
- renderId: meta.renderId,
2431
+ sessionId: meta.sessionId,
2480
2432
  appId: meta.appId,
2481
2433
  ...(meta.wsToken !== undefined
2482
2434
  ? { wsToken: meta.wsToken }
@@ -2486,22 +2438,20 @@ async function bootProduction(opts) {
2486
2438
  });
2487
2439
  channelRegistry.register(createRenderHandler({
2488
2440
  statusRefs,
2489
- pinnedRenderId: meta.renderId,
2441
+ pinnedSessionId: meta.sessionId,
2490
2442
  applyRender,
2491
2443
  getChannelTransport: () => channelTransport,
2492
2444
  }));
2493
2445
  channelRegistry.register(createDataHandler({
2494
- getCurrentRender: () => currentRender,
2446
+ getCurrentGguiSession: () => currentRender,
2495
2447
  streamBus,
2496
2448
  validatorCtx,
2497
- ...(onObserve !== undefined ? { onObserve } : {}),
2498
2449
  }));
2499
2450
  channelRegistry.register(createPropsUpdateHandler({
2500
- getCurrentRender: () => currentRender,
2451
+ getCurrentGguiSession: () => currentRender,
2501
2452
  applyRender,
2502
2453
  }));
2503
2454
  channelRegistry.register(createDrainAckHandler({ dispatch: dispatchDrainAck }));
2504
- channelRegistry.register(createFeedbackHandler());
2505
2455
  channelRegistry.register(createChannelPayloadHandler({
2506
2456
  getChannelTransport: () => channelTransport,
2507
2457
  }));
@@ -2515,7 +2465,7 @@ async function bootProduction(opts) {
2515
2465
  rootWireConfig: rootConfig,
2516
2466
  streamBus,
2517
2467
  applyRender,
2518
- getCurrentRender: () => currentRender,
2468
+ getCurrentGguiSession: () => currentRender,
2519
2469
  validatorCtx,
2520
2470
  manager,
2521
2471
  channelTransport,