@ggui-ai/iframe-runtime 0.10.0 → 0.12.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/host-capabilities.d.ts +6 -4
- package/dist/host-palette-bridge.d.ts +43 -0
- package/dist/host-palette-bridge.js +107 -0
- package/dist/iframe-runtime.js +83 -72
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/mcp-app-iframe-host.js +7 -2
- package/dist/react-renderer.d.ts +48 -6
- package/dist/react-renderer.js +54 -13
- package/dist/render-item.d.ts +17 -0
- package/dist/render-item.js +4 -0
- package/dist/runtime.d.ts +82 -1
- package/dist/runtime.js +214 -62
- package/dist/ui-feedback-chrome.d.ts +16 -0
- package/dist/ui-feedback-chrome.js +20 -0
- package/dist/wire-config.d.ts +7 -3
- package/package.json +7 -7
package/dist/runtime.js
CHANGED
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
29
|
import './zod-jitless.js';
|
|
30
30
|
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
31
|
+
import { effectiveThemeId, effectiveThemeMode, } from '@ggui-ai/protocol/integrations/theme-binding';
|
|
31
32
|
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
32
33
|
import { projectHostContext } from '@ggui-ai/protocol';
|
|
33
34
|
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
|
|
34
35
|
import { applyHostContextStyling, attachListener as attachHostContextListener, seed as seedHostContext, } from './host-context-emitter.js';
|
|
36
|
+
import { mapHostPaletteToGguiVars } from './host-palette-bridge.js';
|
|
35
37
|
import { buildRootWireConfig, StreamBus, } from './wire-config.js';
|
|
36
38
|
import { createChannelTransportRouter, } from './channel-transport.js';
|
|
37
39
|
import { RelayIncapableError } from './relay-incapability.js';
|
|
@@ -47,7 +49,7 @@ import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
|
|
|
47
49
|
import { mountRender, } from './render-item.js';
|
|
48
50
|
import { fromBootstrapFailure, } from './protocol-error.js';
|
|
49
51
|
import { postObservabilityToParent, } from './observability.js';
|
|
50
|
-
import { mountUiFeedbackChrome } from './ui-feedback-chrome.js';
|
|
52
|
+
import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome } from './ui-feedback-chrome.js';
|
|
51
53
|
import { hostCanReceiveMessages, hostCanRelayToolCalls, hostCapabilitiesCaptured, setHostCapabilities, } from './host-capabilities.js';
|
|
52
54
|
import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
|
|
53
55
|
import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
|
|
@@ -87,7 +89,7 @@ function dispatchDrainAck(payload) {
|
|
|
87
89
|
// App-class plumbing (Phase 1.19b.3). The renderer used to roll its own
|
|
88
90
|
// JSON-RPC pump (a closure-scoped `makeJsonRpcCaller` for the single
|
|
89
91
|
// `ui/initialize` request + a module-level `ensurePostRpcToParentListener`
|
|
90
|
-
// for `tools/call` and a `
|
|
92
|
+
// for `tools/call` and a `installPersistentToolResultListener` for inbound
|
|
91
93
|
// `ui/notifications/tool-result`). Every one of those was a hand-rolled
|
|
92
94
|
// reimplementation of a primitive `@modelcontextprotocol/ext-apps`'s
|
|
93
95
|
// `App` class already ships — drift across them produced the Reading-B
|
|
@@ -186,6 +188,94 @@ export function hostAnnouncedThemeMode() {
|
|
|
186
188
|
const theme = currentApp?.getHostContext()?.theme;
|
|
187
189
|
return theme === 'light' || theme === 'dark' ? theme : undefined;
|
|
188
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the mount's base-stylesheet color mode from the slice, with
|
|
193
|
+
* the host as the final fallback (ggui#589, completing the ggui#551
|
|
194
|
+
* precedence ruling — "slice wins, host is the fallback, absent ≠
|
|
195
|
+
* light"):
|
|
196
|
+
*
|
|
197
|
+
* stamped `themeMode` > `theme.mode` (the slice theme OBJECT) >
|
|
198
|
+
* host-announced theme > undefined
|
|
199
|
+
*
|
|
200
|
+
* The middle leg is the ggui#589 fix: a render envelope carrying a
|
|
201
|
+
* per-app theme (`theme: {mode, cssVariables}`) but no top-level
|
|
202
|
+
* `themeMode` is still a slice-stamped mode opinion — before this,
|
|
203
|
+
* the base token ladder ignored it and painted the LIGHT token set
|
|
204
|
+
* under a dark overlay whenever the host (correctly, per the
|
|
205
|
+
* adapter-boundary doctrine in the native host helpers) announced no
|
|
206
|
+
* `hostContext.theme`: a light skeleton in a dark skin. The theme
|
|
207
|
+
* object's own `mode` already drove `color-scheme`; now it also
|
|
208
|
+
* selects the ladder.
|
|
209
|
+
*
|
|
210
|
+
* `undefined` still means "no opinion anywhere" — the renderer's
|
|
211
|
+
* default applies; never coerced to `'light'`.
|
|
212
|
+
*
|
|
213
|
+
* @internal — exported for unit tests; production caller is
|
|
214
|
+
* `buildOpts` inside `bootSequence`.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveMountThemeMode(meta) {
|
|
217
|
+
// The CLIENT projection of the protocol's normative theme-binding
|
|
218
|
+
// total order (@ggui-ai/protocol/integrations/theme-binding,
|
|
219
|
+
// ggui#598 leg 4) — the server stamps via the sibling projection and
|
|
220
|
+
// the protocol's composition-law test pins that the two compose
|
|
221
|
+
// without rank drift.
|
|
222
|
+
return effectiveThemeMode({
|
|
223
|
+
stamped: meta.themeMode,
|
|
224
|
+
sessionSidecar: meta.theme?.mode,
|
|
225
|
+
hostAnnounced: hostAnnouncedThemeMode(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the mount's base-theme id from the slice (ggui#589 ask 3):
|
|
230
|
+
*
|
|
231
|
+
* stamped `themeId` > `theme.name` (the slice theme OBJECT's name)
|
|
232
|
+
* > undefined (renderer default ladder)
|
|
233
|
+
*
|
|
234
|
+
* The name leg makes a slice theme whose `name` matches a REGISTERED
|
|
235
|
+
* theme id select that theme as the BASE token ladder — the overlay's
|
|
236
|
+
* cssVariables still apply above it, so the base carries the full
|
|
237
|
+
* brand ramp coverage a sparse overlay cannot (the "only the mapped
|
|
238
|
+
* vars carry brand" store-frame class). An unregistered name is
|
|
239
|
+
* harmless by construction: `getScopedThemeCss` falls back to the
|
|
240
|
+
* default theme, which is byte-identical to the no-themeId path
|
|
241
|
+
* (`getScopedCssTokens` ≡ `getScopedThemeCss(default)`), so a slice
|
|
242
|
+
* naming its theme "My Custom" renders exactly as before.
|
|
243
|
+
*
|
|
244
|
+
* @internal — exported for unit tests; production caller is
|
|
245
|
+
* `buildOpts` inside `bootSequence`.
|
|
246
|
+
*/
|
|
247
|
+
export function resolveMountThemeId(meta) {
|
|
248
|
+
// Client projection of the normative theme-binding order — see
|
|
249
|
+
// resolveMountThemeMode above.
|
|
250
|
+
return effectiveThemeId({
|
|
251
|
+
stamped: meta.themeId,
|
|
252
|
+
sidecarName: meta.theme?.name,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The host's announced palette, read from the connected App's
|
|
257
|
+
* pre-merged hostContext and translated onto `--ggui-*` tokens by the
|
|
258
|
+
* host-palette bridge. `undefined` when no App is connected, the host
|
|
259
|
+
* sent no `styles.variables`, or nothing in it maps/survives
|
|
260
|
+
* sanitization — callers spread-skip the option, exactly like
|
|
261
|
+
* {@link hostAnnouncedThemeMode}.
|
|
262
|
+
*
|
|
263
|
+
* This is the palette input leg of host-theme adaptation (ggui#572,
|
|
264
|
+
* the color analog of #551's mode leg above): the spec `--color-*`
|
|
265
|
+
* variables land as inline custom properties on `<html>` via the
|
|
266
|
+
* canonical ext-apps helper, but nothing ggui renders consumes that
|
|
267
|
+
* vocabulary, and the scoped token block shadows root inheritance
|
|
268
|
+
* anyway — so without this mapping a host palette repaints nothing.
|
|
269
|
+
* `buildOpts` threads it onto the mount options; the renderer merges
|
|
270
|
+
* it as the fallback layer BENEATH the slice's own theme (ggui#573
|
|
271
|
+
* ruling: slice wins, host fallback).
|
|
272
|
+
*
|
|
273
|
+
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
274
|
+
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
275
|
+
*/
|
|
276
|
+
export function hostAnnouncedPalette() {
|
|
277
|
+
return mapHostPaletteToGguiVars(currentApp?.getHostContext()?.styles?.variables);
|
|
278
|
+
}
|
|
189
279
|
/**
|
|
190
280
|
* @internal — exported for unit tests to reset module state between
|
|
191
281
|
* scenarios.
|
|
@@ -314,6 +404,14 @@ export async function bootSequence(opts) {
|
|
|
314
404
|
const toolResultPromise = opts.preResolvedMeta !== undefined
|
|
315
405
|
? Promise.resolve(null)
|
|
316
406
|
: awaitToolResultMetaFromApp(app, toolResultTimeoutMs);
|
|
407
|
+
// Persistent re-mount listener — ALSO pre-connect (ggui#586). On a
|
|
408
|
+
// Tier-1/preResolved boot the one-shot listener above is skipped,
|
|
409
|
+
// which made the post-mount installation the FIRST toolresult
|
|
410
|
+
// registration — after the handshake, tripping the ext-apps timing
|
|
411
|
+
// notice and leaning on SDK replay for correctness. Registering here
|
|
412
|
+
// covers every boot path; arrivals before a renderer publishes
|
|
413
|
+
// `applyRender` no-op inside the listener.
|
|
414
|
+
installPersistentToolResultListener(app);
|
|
317
415
|
const initResult = await connectApp(app, transport);
|
|
318
416
|
if (!initResult.ok) {
|
|
319
417
|
setStatus(refs, `ui/initialize failed: ${initResult.message}`, 'error');
|
|
@@ -1259,17 +1357,19 @@ function awaitToolResultMeta(timeoutMs) {
|
|
|
1259
1357
|
});
|
|
1260
1358
|
}
|
|
1261
1359
|
/**
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1360
|
+
* Per-App guard for the persistent toolresult re-listener. Ensures
|
|
1361
|
+
* exactly one `app.addEventListener('toolresult', …)` registration per
|
|
1362
|
+
* App instance even across re-mounts (e.g. an agent fires a second
|
|
1363
|
+
* `ggui_render` and the listener re-applies through the published
|
|
1364
|
+
* `applyRender`). Keyed by App — not a module boolean — so a fresh App
|
|
1365
|
+
* (a re-booted document in tests; production is single-tenant per
|
|
1366
|
+
* iframe) always gets its own registration, never a stale latch.
|
|
1267
1367
|
*/
|
|
1268
|
-
|
|
1368
|
+
const persistentToolResultListenerApps = new WeakSet();
|
|
1269
1369
|
/**
|
|
1270
1370
|
* Module-level handle to the active renderer's `applyRender`, published
|
|
1271
1371
|
* by `bootProduction`'s `setup()` once the renderer is built. The
|
|
1272
|
-
* module-level {@link
|
|
1372
|
+
* module-level {@link installPersistentToolResultListener} resolves this to re-mount
|
|
1273
1373
|
* on a host-re-emitted tool-result WITHOUT a fresh boot — no second mount,
|
|
1274
1374
|
* no second WS. This is the no-WS live-re-render channel for
|
|
1275
1375
|
* spec-compliant MCP-Apps hosts (claude.ai / ChatGPT / Claude Desktop)
|
|
@@ -1281,8 +1381,8 @@ let postMountListenerInstalled = false;
|
|
|
1281
1381
|
let activeApplyRender = null;
|
|
1282
1382
|
/**
|
|
1283
1383
|
* Module-level guard for {@link installAnchorClickInterceptor}. Same
|
|
1284
|
-
* rationale as {@link
|
|
1285
|
-
* triggered by `
|
|
1384
|
+
* rationale as {@link persistentToolResultListenerApps}: re-mounts are
|
|
1385
|
+
* triggered by `installPersistentToolResultListener`, and stacking capture-phase
|
|
1286
1386
|
* click listeners across re-mounts would multiply the audit envelopes
|
|
1287
1387
|
* that fire on a single click.
|
|
1288
1388
|
*/
|
|
@@ -1545,9 +1645,11 @@ export function emitAudit(args) {
|
|
|
1545
1645
|
* Returns `'success'` when the handler said `ok:true`. Returns
|
|
1546
1646
|
* `'fallback'` for any other outcome — pipe missing
|
|
1547
1647
|
* (`PIPE_NOT_FOUND`), envelope rejected (`INVALID_ACTION_KIND`),
|
|
1548
|
-
* relay error, no host relay wired, or any unexpected shape.
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1648
|
+
* relay error, no host relay wired, or any unexpected shape. On
|
|
1649
|
+
* `'fallback'` the gesture was NOT enqueued and is dropped with local
|
|
1650
|
+
* feedback only (toast / #440 latch) — no `ui/message` fires (the
|
|
1651
|
+
* doorbell is success-path-only: there is nothing to point it at), and
|
|
1652
|
+
* no alternate transport is attempted today (ggui#599's open leg).
|
|
1551
1653
|
*/
|
|
1552
1654
|
/**
|
|
1553
1655
|
* Unwrap the payload record of a relayed submit-action tool result.
|
|
@@ -1613,6 +1715,16 @@ function isRelayShapedFailure(resp) {
|
|
|
1613
1715
|
* consumer" — the doorbell rings. Shares {@link submitActionPayload}
|
|
1614
1716
|
* with {@link classifySubmitActionResponse} so both reads agree on
|
|
1615
1717
|
* which envelope tier carries the payload.
|
|
1718
|
+
*
|
|
1719
|
+
* RELAY CONTRACT (ggui#603): this read only works when the host's
|
|
1720
|
+
* `tools/call` relay threads the server's tool result VERBATIM — the
|
|
1721
|
+
* `consumerPresent` marker rides `structuredContent` on the real
|
|
1722
|
+
* result. A relay that wraps or synthesizes the response silently
|
|
1723
|
+
* strips the marker, and every healthy gesture then misclassifies as
|
|
1724
|
+
* "agent not listening" (a persistent `action_required` toast per
|
|
1725
|
+
* gesture). Verbatim threading is the same obligation that keeps
|
|
1726
|
+
* `_meta` alive on `resources/read` relays; a conformance vector for
|
|
1727
|
+
* it is tracked on ggui#603.
|
|
1616
1728
|
*/
|
|
1617
1729
|
function extractConsumerPresent(resp) {
|
|
1618
1730
|
const inner = submitActionPayload(resp);
|
|
@@ -1823,7 +1935,7 @@ function showActionToast(text, kind, onDismiss) {
|
|
|
1823
1935
|
'bottom:16px',
|
|
1824
1936
|
'left:50%',
|
|
1825
1937
|
'transform:translateX(-50%)',
|
|
1826
|
-
|
|
1938
|
+
`z-index:${ACTION_TOAST_Z_INDEX}`,
|
|
1827
1939
|
'pointer-events:auto',
|
|
1828
1940
|
'max-width:90%',
|
|
1829
1941
|
'padding:8px 14px',
|
|
@@ -2813,7 +2925,7 @@ export function requestDisplayModeInParent(args) {
|
|
|
2813
2925
|
* or honor an upstream `defaultPrevented` from a higher-priority
|
|
2814
2926
|
* handler.
|
|
2815
2927
|
*
|
|
2816
|
-
* Idempotent: re-mounts (via `
|
|
2928
|
+
* Idempotent: re-mounts (via `installPersistentToolResultListener`) call this
|
|
2817
2929
|
* helper again; the {@link anchorClickInterceptInstalled} guard makes
|
|
2818
2930
|
* subsequent calls a no-op.
|
|
2819
2931
|
*/
|
|
@@ -2954,15 +3066,26 @@ export function __resetInterceptorsForTest() {
|
|
|
2954
3066
|
fullscreenInterceptInstalled = false;
|
|
2955
3067
|
}
|
|
2956
3068
|
/**
|
|
2957
|
-
* Install
|
|
2958
|
-
* that catches `ui/notifications/tool-result` notifications
|
|
2959
|
-
*
|
|
2960
|
-
* different bootstrap (different sessionId / codeUrl / kind)
|
|
2961
|
-
* a re-mount through the published `applyRender
|
|
3069
|
+
* Install the persistent `app.addEventListener('toolresult', …)`
|
|
3070
|
+
* listener that catches `ui/notifications/tool-result` notifications
|
|
3071
|
+
* arriving at ANY point in the document's life. Each tool-result that
|
|
3072
|
+
* carries a different bootstrap (different sessionId / codeUrl / kind)
|
|
3073
|
+
* triggers a re-mount through the published `applyRender`; arrivals
|
|
3074
|
+
* before a renderer publishes `applyRender` no-op safely (and do NOT
|
|
3075
|
+
* latch the dedupe key — see below). This closes the boot-only-
|
|
2962
3076
|
* listener gap that prevented live re-render when an agent issued
|
|
2963
3077
|
* a second `ggui_render` to the same render-resource.
|
|
2964
3078
|
*
|
|
2965
|
-
*
|
|
3079
|
+
* REGISTRATION TIMING (ggui#586): called by `bootSequence` BEFORE
|
|
3080
|
+
* `app.connect(transport)` — on a Tier-1/preResolved boot the one-shot
|
|
3081
|
+
* Tier-2 listener never registers, so this used to be the FIRST
|
|
3082
|
+
* `toolresult` registration and landed post-handshake (the ext-apps
|
|
3083
|
+
* `_assertHandlerTiming` notice a guuey production render surfaced).
|
|
3084
|
+
* The SDK's replay made that harmless; pre-registering removes the
|
|
3085
|
+
* dependence on replay entirely.
|
|
3086
|
+
*
|
|
3087
|
+
* Idempotent per App: repeat calls with the same App no-op via
|
|
3088
|
+
* {@link persistentToolResultListenerApps}.
|
|
2966
3089
|
*
|
|
2967
3090
|
* Why module-level vs scoped to a single mount: re-mounts re-apply
|
|
2968
3091
|
* through `applyRender` while the listener stays live, and the
|
|
@@ -2972,17 +3095,13 @@ export function __resetInterceptorsForTest() {
|
|
|
2972
3095
|
* Spec-canonical (post-Phase-1.19b.3): the previous hand-rolled
|
|
2973
3096
|
* `window.addEventListener('message', …)` is gone; App handles every
|
|
2974
3097
|
* inbound `ui/notifications/tool-result` envelope and dispatches via
|
|
2975
|
-
* its event system.
|
|
2976
|
-
*
|
|
2977
|
-
* path before this helper runs.
|
|
3098
|
+
* its event system. The App handle is passed by the boot path
|
|
3099
|
+
* directly (pre-connect, so `getCurrentApp()` is not yet set).
|
|
2978
3100
|
*/
|
|
2979
|
-
function
|
|
2980
|
-
if (
|
|
3101
|
+
function installPersistentToolResultListener(app) {
|
|
3102
|
+
if (persistentToolResultListenerApps.has(app))
|
|
2981
3103
|
return;
|
|
2982
|
-
|
|
2983
|
-
if (app === null)
|
|
2984
|
-
return; // pre-connect; caller bug, swallow safely
|
|
2985
|
-
postMountListenerInstalled = true;
|
|
3104
|
+
persistentToolResultListenerApps.add(app);
|
|
2986
3105
|
let lastMetaKey = null;
|
|
2987
3106
|
app.addEventListener('toolresult', (params) => {
|
|
2988
3107
|
const meta = extractMetaFromToolResult(params);
|
|
@@ -3020,15 +3139,21 @@ function installPostMountListener() {
|
|
|
3020
3139
|
].join('|');
|
|
3021
3140
|
if (key === lastMetaKey)
|
|
3022
3141
|
return;
|
|
3023
|
-
lastMetaKey = key;
|
|
3024
3142
|
// Re-mount through the EXISTING renderer's `applyRender` (published
|
|
3025
3143
|
// module-level by bootProduction's setup) — NOT a fresh boot. One
|
|
3026
3144
|
// mount surface, one WS; the prior stale-closure / second-WS race
|
|
3027
3145
|
// (the #290 root cause) is structurally impossible. Project the new
|
|
3028
3146
|
// tool-result meta into a seed and re-apply it.
|
|
3147
|
+
//
|
|
3148
|
+
// The apply-null check runs BEFORE the dedupe key latches (ggui#586
|
|
3149
|
+
// ordering): now that this listener registers pre-connect, the
|
|
3150
|
+
// BOOT-path tool-result also flows through here while no renderer
|
|
3151
|
+
// is published yet — latching its key then would silently dedupe a
|
|
3152
|
+
// legitimate post-mount re-broadcast of the same envelope.
|
|
3029
3153
|
const apply = activeApplyRender;
|
|
3030
3154
|
if (apply === null)
|
|
3031
3155
|
return; // no renderer published yet — no-op safely
|
|
3156
|
+
lastMetaKey = key;
|
|
3032
3157
|
void buildGguiSessionSeedInput(meta).then((seed) => {
|
|
3033
3158
|
if (seed === null)
|
|
3034
3159
|
return;
|
|
@@ -3306,20 +3431,30 @@ async function bootProduction(opts) {
|
|
|
3306
3431
|
}
|
|
3307
3432
|
},
|
|
3308
3433
|
};
|
|
3309
|
-
// Spec-canonical outbound dispatch
|
|
3310
|
-
//
|
|
3311
|
-
// `
|
|
3312
|
-
//
|
|
3313
|
-
//
|
|
3314
|
-
//
|
|
3315
|
-
//
|
|
3316
|
-
// `
|
|
3317
|
-
//
|
|
3318
|
-
// `
|
|
3319
|
-
//
|
|
3320
|
-
//
|
|
3321
|
-
//
|
|
3322
|
-
//
|
|
3434
|
+
// Spec-canonical outbound dispatch: user actions go through the
|
|
3435
|
+
// MCP-Apps host relay (SPEC §4.7): postMessage
|
|
3436
|
+
// `tools/call:ggui_runtime_submit_action` to the parent →
|
|
3437
|
+
// `AppRenderer.onCallTool` → host's MCP client → ggui MCP server
|
|
3438
|
+
// → `createGguiSubmitActionHandler.append` → `pendingEventConsumer`
|
|
3439
|
+
// → `ggui_consume` wakes the agent. NOTE (truth, ggui#596 +
|
|
3440
|
+
// adversarial-cycle correction): the server's WS ingress
|
|
3441
|
+
// dual-writes accepted `data:submit` frames onto the consume pipe
|
|
3442
|
+
// ONLY when the live channel is composed with a
|
|
3443
|
+
// `pendingEventConsumer` — which `createGguiServer` threads for
|
|
3444
|
+
// DEFAULT-handlers compositions only (a custom `handlers` list
|
|
3445
|
+
// drains its own pipe; bridging the channel onto an instance
|
|
3446
|
+
// nobody drains would buffer gestures into the void — see the
|
|
3447
|
+
// hoist note in server.ts). The hosted pod IS a custom-handlers
|
|
3448
|
+
// caller: on it, a WS gesture lands on the retained ledger only
|
|
3449
|
+
// and never reaches the agent. Any WS action fallback (ggui#599's
|
|
3450
|
+
// open design leg) therefore requires the server/pod bridge
|
|
3451
|
+
// FIRST — it is a capability gap on that composition today. The WS pipe otherwise carries
|
|
3452
|
+
// streamSpec subscriptions (inbound `ggui_emit` fanout +
|
|
3453
|
+
// `props_update` + `render` + `data` + `drain_ack` +
|
|
3454
|
+
// `channel_payload`). `routeDispatch` is the shared named-export
|
|
3455
|
+
// helper (production + tests exercise the same code path);
|
|
3456
|
+
// threading it here keeps LIVE-mode on the canonical dispatch
|
|
3457
|
+
// pipeline.
|
|
3323
3458
|
// Single mounted render — populated by `applyRender` on the first
|
|
3324
3459
|
// render frame. The wire config + data channel handler read it
|
|
3325
3460
|
// through `currentRender`-returning thunks so they always see the
|
|
@@ -3461,13 +3596,21 @@ async function bootProduction(opts) {
|
|
|
3461
3596
|
!('createdAt' in render)
|
|
3462
3597
|
? { codeModuleUrl: meta.codeModuleUrl }
|
|
3463
3598
|
: {}),
|
|
3464
|
-
|
|
3465
|
-
//
|
|
3466
|
-
//
|
|
3467
|
-
//
|
|
3468
|
-
//
|
|
3599
|
+
// Base-theme ladder (ggui#589 ask 3 — see `resolveMountThemeId`):
|
|
3600
|
+
// stamped themeId > the slice theme object's NAME (a
|
|
3601
|
+
// registered name binds the full brand base; unregistered
|
|
3602
|
+
// names fall back to the default ladder, byte-identical to
|
|
3603
|
+
// the no-themeId path).
|
|
3469
3604
|
...(() => {
|
|
3470
|
-
const
|
|
3605
|
+
const themeId = resolveMountThemeId(meta);
|
|
3606
|
+
return themeId !== undefined ? { themeId } : {};
|
|
3607
|
+
})(),
|
|
3608
|
+
// Mode ladder (ggui#551 + #589 — see `resolveMountThemeMode`):
|
|
3609
|
+
// stamped `themeMode` > the slice theme OBJECT's `mode` >
|
|
3610
|
+
// host-announced theme. Absent everywhere means "no opinion",
|
|
3611
|
+
// not "light".
|
|
3612
|
+
...(() => {
|
|
3613
|
+
const themeMode = resolveMountThemeMode(meta);
|
|
3471
3614
|
return themeMode !== undefined ? { themeMode } : {};
|
|
3472
3615
|
})(),
|
|
3473
3616
|
// Per-app theme overlay (St3 M2.2). Threaded straight from the
|
|
@@ -3477,6 +3620,15 @@ async function bootProduction(opts) {
|
|
|
3477
3620
|
// `color-scheme` at `:root`. THEME IS COMPONENT-ONLY — system
|
|
3478
3621
|
// cards theme via the SystemCardHost/ThemeProvider path.
|
|
3479
3622
|
...(meta.theme !== undefined ? { appTheme: meta.theme } : {}),
|
|
3623
|
+
// Host palette: always threaded when the host announced one —
|
|
3624
|
+
// unlike mode there is no either/or gate here, because the
|
|
3625
|
+
// precedence is CSS document order inside the scoped block
|
|
3626
|
+
// (base < hostPalette < appTheme, ggui#572/#573: slice wins,
|
|
3627
|
+
// host fallback), not a value-level ?? fallback.
|
|
3628
|
+
...(() => {
|
|
3629
|
+
const hostPalette = hostAnnouncedPalette();
|
|
3630
|
+
return hostPalette !== undefined ? { hostPalette } : {};
|
|
3631
|
+
})(),
|
|
3480
3632
|
...(wrapOuter !== undefined ? { wrapOuter } : {}),
|
|
3481
3633
|
};
|
|
3482
3634
|
};
|
|
@@ -3500,17 +3652,17 @@ async function bootProduction(opts) {
|
|
|
3500
3652
|
}
|
|
3501
3653
|
await renderHandle.update(buildOpts(render));
|
|
3502
3654
|
};
|
|
3503
|
-
// Publish `applyRender` module-level
|
|
3504
|
-
//
|
|
3505
|
-
// (
|
|
3506
|
-
//
|
|
3507
|
-
//
|
|
3508
|
-
//
|
|
3509
|
-
//
|
|
3510
|
-
//
|
|
3511
|
-
//
|
|
3655
|
+
// Publish `applyRender` module-level. The persistent tool-result
|
|
3656
|
+
// listener is ALREADY registered — `bootSequence` installs it
|
|
3657
|
+
// pre-connect (ggui#586) — and reads this slot dynamically, so
|
|
3658
|
+
// publishing here is what arms it. On a host-re-emitted
|
|
3659
|
+
// tool-result (claude.ai re-broadcast; the
|
|
3660
|
+
// Anthropic-SDK-strips-_meta refetch), the listener re-mounts
|
|
3661
|
+
// through THIS `applyRender` — no fresh boot, no second WS. The
|
|
3662
|
+
// no-WS live-re-render channel; for WS hosts the render-frame
|
|
3663
|
+
// handler already covers re-render, so it's a redundant-safe
|
|
3664
|
+
// fallback (guarded by the sessionId pin + liveTrio dedupe).
|
|
3512
3665
|
activeApplyRender = applyRender;
|
|
3513
|
-
installPostMountListener();
|
|
3514
3666
|
// Validator context — A2UI default for `_ggui:preview`; no
|
|
3515
3667
|
// bootstrap-supplied overrides today (the
|
|
3516
3668
|
// `extraReservedValidators` injection slot is reserved for a
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { ObservabilityEmitter } from './observability.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stacking contract between the two body-appended runtime layers
|
|
4
|
+
* (ggui#603): the feedback chrome must sit strictly ABOVE the action
|
|
5
|
+
* toast. The persistent `action_required` toast parks
|
|
6
|
+
* `pointer-events:auto` at fixed bottom-center — the same region this
|
|
7
|
+
* chrome occupies — and is re-armed per gesture; without this order an
|
|
8
|
+
* armed toast eats every tap aimed at the feedback controls. The toast
|
|
9
|
+
* remains dismissable on its own uncovered area (it accepts the click
|
|
10
|
+
* anywhere on itself), so partial occlusion costs it nothing.
|
|
11
|
+
*
|
|
12
|
+
* Single source: `runtime.ts`'s toast reads {@link ACTION_TOAST_Z_INDEX};
|
|
13
|
+
* this module applies {@link UI_FEEDBACK_CHROME_Z_INDEX} to the chrome
|
|
14
|
+
* container. The test suite pins the relationship, not the numbers.
|
|
15
|
+
*/
|
|
16
|
+
export declare const UI_FEEDBACK_CHROME_Z_INDEX = 2147483647;
|
|
17
|
+
export declare const ACTION_TOAST_Z_INDEX = 2147483646;
|
|
2
18
|
export interface UiFeedbackChromeOptions {
|
|
3
19
|
/**
|
|
4
20
|
* Observability sink the card emits `ui-feedback` events into.
|
|
@@ -36,6 +36,22 @@
|
|
|
36
36
|
* gate passes.
|
|
37
37
|
*/
|
|
38
38
|
import { createElement } from 'react';
|
|
39
|
+
/**
|
|
40
|
+
* Stacking contract between the two body-appended runtime layers
|
|
41
|
+
* (ggui#603): the feedback chrome must sit strictly ABOVE the action
|
|
42
|
+
* toast. The persistent `action_required` toast parks
|
|
43
|
+
* `pointer-events:auto` at fixed bottom-center — the same region this
|
|
44
|
+
* chrome occupies — and is re-armed per gesture; without this order an
|
|
45
|
+
* armed toast eats every tap aimed at the feedback controls. The toast
|
|
46
|
+
* remains dismissable on its own uncovered area (it accepts the click
|
|
47
|
+
* anywhere on itself), so partial occlusion costs it nothing.
|
|
48
|
+
*
|
|
49
|
+
* Single source: `runtime.ts`'s toast reads {@link ACTION_TOAST_Z_INDEX};
|
|
50
|
+
* this module applies {@link UI_FEEDBACK_CHROME_Z_INDEX} to the chrome
|
|
51
|
+
* container. The test suite pins the relationship, not the numbers.
|
|
52
|
+
*/
|
|
53
|
+
export const UI_FEEDBACK_CHROME_Z_INDEX = 2147483647;
|
|
54
|
+
export const ACTION_TOAST_Z_INDEX = 2147483646;
|
|
39
55
|
/**
|
|
40
56
|
* Mount the in-iframe UI-feedback affordance, or do nothing.
|
|
41
57
|
*
|
|
@@ -69,6 +85,10 @@ export async function mountUiFeedbackChrome(doc, opts) {
|
|
|
69
85
|
doc.querySelector('[data-ggui-ui-feedback-chrome]')?.remove();
|
|
70
86
|
const container = doc.createElement('div');
|
|
71
87
|
container.setAttribute('data-ggui-ui-feedback-chrome', '');
|
|
88
|
+
// Win the stacking race against the action toast (ggui#603 — see
|
|
89
|
+
// the constants' docblock above).
|
|
90
|
+
container.style.position = 'relative';
|
|
91
|
+
container.style.zIndex = String(UI_FEEDBACK_CHROME_Z_INDEX);
|
|
72
92
|
doc.body.appendChild(container);
|
|
73
93
|
const root = reactDomClient.createRoot(container);
|
|
74
94
|
root.render(createElement(cardMod.UiFeedbackCard, {
|
package/dist/wire-config.d.ts
CHANGED
|
@@ -95,9 +95,13 @@ export interface BuildRootWireConfigOptions {
|
|
|
95
95
|
*
|
|
96
96
|
* The default WS-frame send reaches the agent only on servers whose
|
|
97
97
|
* live channel bridges WS `data:submit` actions onto the same
|
|
98
|
-
* pending-events pipe
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
98
|
+
* pending-events pipe. First-party `createGguiServer` bridges it
|
|
99
|
+
* ONLY when composed with DEFAULT handlers (`opts.handlers`
|
|
100
|
+
* undefined); a custom `handlers` list drains its own pipe, so the
|
|
101
|
+
* channel deliberately does not bridge — the hosted pod is such a
|
|
102
|
+
* caller, and on it a WS gesture lands on the retained ledger only.
|
|
103
|
+
* See `GguiSessionChannelOptions.pendingEventConsumer`. The default
|
|
104
|
+
* send remains the seam tests and direct-WS callers exercise.
|
|
101
105
|
*
|
|
102
106
|
* Called AFTER outbound validation passes. Receives the validated
|
|
103
107
|
* {@link ActionEnvelope}; caller extracts payload.action +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/iframe-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"react": "^19.0.0",
|
|
37
37
|
"react-dom": "^19.0.0",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@ggui-ai/design": "0.
|
|
40
|
-
"@ggui-ai/
|
|
41
|
-
"@ggui-ai/
|
|
42
|
-
"@ggui-ai/
|
|
43
|
-
"@ggui-ai/
|
|
44
|
-
"@ggui-ai/wire": "0.
|
|
39
|
+
"@ggui-ai/design": "0.12.0",
|
|
40
|
+
"@ggui-ai/live-channel": "0.12.0",
|
|
41
|
+
"@ggui-ai/gadgets": "0.12.0",
|
|
42
|
+
"@ggui-ai/preview-a2ui": "0.12.0",
|
|
43
|
+
"@ggui-ai/protocol": "0.12.0",
|
|
44
|
+
"@ggui-ai/wire": "0.12.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.27.0"
|