@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.
- package/dist/channel-transport.d.ts +20 -30
- package/dist/channel-transport.js +24 -24
- package/dist/channels/data.d.ts +5 -20
- package/dist/channels/data.js +4 -52
- package/dist/channels/index.d.ts +0 -1
- package/dist/channels/index.js +0 -1
- package/dist/channels/props-update.d.ts +9 -9
- package/dist/channels/props-update.js +9 -9
- package/dist/channels/render-frame.d.ts +5 -5
- package/dist/channels/render-frame.js +5 -5
- package/dist/compiled-validators.d.ts +2 -2
- package/dist/compiled-validators.js +1 -1
- package/dist/context-observer.d.ts +4 -4
- package/dist/context-observer.js +3 -3
- package/dist/events-polling.d.ts +7 -7
- package/dist/events-polling.js +5 -5
- package/dist/globals.d.ts +1 -1
- package/dist/host-context-emitter.d.ts +4 -4
- package/dist/host-context-emitter.js +9 -9
- package/dist/iframe-runtime.js +68 -78
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +29 -6
- package/dist/mcp-app-iframe-host.d.ts +8 -8
- package/dist/mcp-app-iframe-host.js +4 -4
- package/dist/meta-parse.d.ts +3 -4
- package/dist/meta-parse.js +17 -44
- package/dist/observability.d.ts +14 -36
- package/dist/observability.js +2 -1
- package/dist/protocol-error.d.ts +38 -48
- package/dist/protocol-error.js +5 -23
- package/dist/provisional-renderer.js +3 -3
- package/dist/react-renderer.d.ts +16 -6
- package/dist/react-renderer.js +22 -40
- package/dist/registry-subscribe.d.ts +7 -7
- package/dist/registry-subscribe.js +28 -14
- package/dist/render-item.d.ts +22 -15
- package/dist/render-item.js +7 -14
- package/dist/runtime.d.ts +79 -105
- package/dist/runtime.js +219 -269
- package/dist/status-dom.d.ts +12 -12
- package/dist/status-dom.js +11 -11
- package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
- package/dist/system-cards/NoCredentialsCard.js +1 -1
- package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
- package/dist/system-cards/ProtocolProbeCard.js +21 -25
- package/dist/system-cards/index.d.ts +5 -5
- package/dist/system-cards/index.js +5 -5
- package/dist/types.d.ts +29 -34
- package/dist/validation.d.ts +4 -5
- package/dist/validation.js +5 -6
- package/dist/wire-config.d.ts +43 -106
- package/dist/wire-config.js +29 -195
- package/package.json +12 -11
- package/dist/channels/feedback.d.ts +0 -14
- 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,
|
|
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
|
|
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 `
|
|
104
|
-
* `
|
|
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 — `
|
|
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
|
|
121
|
-
*
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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` (
|
|
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-
|
|
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 `
|
|
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
|
|
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.
|
|
365
|
+
renderInto: refs.renderRoot,
|
|
369
366
|
statusRefs: refs,
|
|
370
367
|
...(onObserve !== undefined ? { onObserve } : {}),
|
|
371
368
|
})
|
|
372
369
|
: null;
|
|
373
|
-
setStatus(refs, `Connecting to ${meta.
|
|
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
|
-
|
|
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 `
|
|
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 (
|
|
399
|
+
* idempotent on the (sessionId, channelName) tuple.
|
|
403
400
|
*/
|
|
404
401
|
const applyAck = async (ackPayload) => {
|
|
405
|
-
const target = ackPayload.
|
|
402
|
+
const target = ackPayload.session;
|
|
406
403
|
if (target === undefined)
|
|
407
404
|
return;
|
|
408
|
-
if (target.id !==
|
|
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
|
-
|
|
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', {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
538
|
-
// subscribe-failed
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
// `
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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`, `
|
|
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
|
|
676
|
-
*
|
|
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`, `
|
|
681
|
-
* `
|
|
682
|
-
*
|
|
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
|
|
744
|
-
* BEFORE the authoritative wire `
|
|
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 `
|
|
783
|
+
* ack reconciles the seed to a full `GguiSession` (no fabrication).
|
|
758
784
|
*/
|
|
759
|
-
export async function
|
|
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.
|
|
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(`
|
|
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.
|
|
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
|
|
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
|
|
907
|
-
*
|
|
908
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1183
|
-
* native-idiom fullscreen request) calls this alongside its
|
|
1184
|
-
* host effect, so operators get **uniform server-side
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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
|
-
` <
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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: {
|
|
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
|
-
/**
|
|
1498
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1594
|
-
*
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 `
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1916
|
-
* a re-mount
|
|
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:
|
|
1923
|
-
*
|
|
1924
|
-
* single mount cycle. The guard ensures
|
|
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
|
|
1932
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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, {
|
|
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/<
|
|
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['
|
|
2036
|
-
renderBag['
|
|
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 ===
|
|
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
|
|
2128
|
-
//
|
|
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 — `
|
|
2092
|
+
// Post-Phase-B `meta` is the flat render slice — `sessionId` /
|
|
2140
2093
|
// `appId` / `runtimeUrl` / `wsUrl` / `wsToken` / `themeId` /
|
|
2141
|
-
// `gadgets` / `publicEnv` / `contextSlots` /
|
|
2142
|
-
// `
|
|
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
|
|
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
|
-
|
|
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` + `
|
|
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
|
|
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
|
|
2260
|
-
//
|
|
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
|
|
2275
|
-
// installed neither, so anchor clicks + fullscreen
|
|
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
|
-
|
|
2232
|
+
sessionId: meta.sessionId,
|
|
2280
2233
|
appId: meta.appId,
|
|
2281
2234
|
});
|
|
2282
2235
|
installFullscreenInterceptors({
|
|
2283
2236
|
dispatchToolName,
|
|
2284
|
-
|
|
2237
|
+
sessionId: meta.sessionId,
|
|
2285
2238
|
appId: meta.appId,
|
|
2286
2239
|
});
|
|
2287
2240
|
const rootConfig = buildRootWireConfig({
|
|
2288
|
-
|
|
2241
|
+
sessionId: meta.sessionId,
|
|
2289
2242
|
appId: meta.appId,
|
|
2290
|
-
|
|
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
|
-
|
|
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 `
|
|
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'`.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
2436
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
2441
|
+
pinnedSessionId: meta.sessionId,
|
|
2490
2442
|
applyRender,
|
|
2491
2443
|
getChannelTransport: () => channelTransport,
|
|
2492
2444
|
}));
|
|
2493
2445
|
channelRegistry.register(createDataHandler({
|
|
2494
|
-
|
|
2446
|
+
getCurrentGguiSession: () => currentRender,
|
|
2495
2447
|
streamBus,
|
|
2496
2448
|
validatorCtx,
|
|
2497
|
-
...(onObserve !== undefined ? { onObserve } : {}),
|
|
2498
2449
|
}));
|
|
2499
2450
|
channelRegistry.register(createPropsUpdateHandler({
|
|
2500
|
-
|
|
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
|
-
|
|
2468
|
+
getCurrentGguiSession: () => currentRender,
|
|
2519
2469
|
validatorCtx,
|
|
2520
2470
|
manager,
|
|
2521
2471
|
channelTransport,
|