@ggui-ai/iframe-runtime 0.11.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.
@@ -77,8 +77,13 @@ export function mountMcpAppIframe(container, opts) {
77
77
  iframe.style.maxWidth = dims.maxWidth !== undefined ? `${dims.maxWidth}px` : '100%';
78
78
  if (dims.maxHeight !== undefined)
79
79
  iframe.style.maxHeight = `${dims.maxHeight}px`;
80
- iframe.style.border = '1px solid #e5e5e5';
81
- iframe.style.borderRadius = '8px';
80
+ // Round-6 doctrine (ggui#589 @6e15724a1, kit C1 grade): the helper
81
+ // paints CONTAINMENT ONLY — silhouette (border/radius) belongs to
82
+ // the embedding host's own chrome, tokens to the theme. The former
83
+ // 1px #e5e5e5 / radius-8 here was the same ungoverned-chrome class
84
+ // the RN helper shipped, surfaced by the host-helper catalog's
85
+ // first-party run (ggui#600 OPEN FINDING, closed here).
86
+ iframe.style.border = 'none';
82
87
  iframe.style.display = 'block';
83
88
  container.appendChild(iframe);
84
89
  // Cache contentWindow — on detach, `iframe.contentWindow` is null
@@ -81,6 +81,28 @@ export interface ReactRootMountOptions {
81
81
  readonly appTheme?: {
82
82
  readonly mode: 'light' | 'dark';
83
83
  readonly cssVariables: Record<string, string>;
84
+ /**
85
+ * The REGISTERED base ladder, delivered over the wire (ggui#598-C
86
+ * runtime theme registration — see protocol's `appThemeSchema`).
87
+ * BOTH modes' resolved variable sets ride the envelope; when
88
+ * present, the mode-selected set REPLACES the compiled ladder
89
+ * block (`getScopedThemeCss`/`getScopedCssTokens`) at the base
90
+ * position of the #573 precedence above — delivery changes WHERE
91
+ * the ladder's bytes come from, never WHICH layer wins. Mode
92
+ * selection reuses the mount's already-resolved `themeMode` (one
93
+ * projection — no second mode resolution here), and a mode switch
94
+ * re-paints from the retained other set with no network activity.
95
+ * Values are injection-safe by the wire schema — never
96
+ * re-validated here; serialization is key-sorted for byte-stable
97
+ * output. `documentHash` is the registration's identity (joins a
98
+ * painted ladder to its registration record); the renderer
99
+ * carries it but exposes no observability surface for it today.
100
+ */
101
+ readonly base?: {
102
+ readonly documentHash: string;
103
+ readonly light: Readonly<Record<string, string>>;
104
+ readonly dark: Readonly<Record<string, string>>;
105
+ };
84
106
  };
85
107
  /**
86
108
  * Host-announced palette, already translated onto `--ggui-*` tokens
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import React, { Component, createElement, Fragment, } from 'react';
33
33
  import { createRoot } from 'react-dom/client';
34
- import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
34
+ import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, assembleDeliveredThemeCss, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
35
35
  import { hoistImports, loadModule, loadModuleInline, probeUrlModuleLoad, } from '@ggui-ai/design/module-loader';
36
36
  /**
37
37
  * Document-lifetime verdict: URL-scheme module loading (`blob:` main
@@ -314,9 +314,25 @@ export async function mountReactRoot(container, opts) {
314
314
  }
315
315
  }
316
316
  function renderTree(opts) {
317
- let themeCss = opts.themeId
318
- ? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
319
- : getScopedCssTokens(scopeClass, opts.themeMode);
317
+ // Delivered base ladder (ggui#598-C): when the slice theme carries
318
+ // `base`, the mode-selected set REPLACES the compiled ladder block
319
+ // at the base position — same precedence slot, different byte
320
+ // source. Mode selection reuses the already-resolved `themeMode`
321
+ // (one projection; absent means light — the same default
322
+ // `getScopedThemeCss` applies). Both sets stay retained on the
323
+ // options, so a mode-switch re-render picks the other ladder from
324
+ // memory — never the network.
325
+ const deliveredBase = opts.appTheme?.base;
326
+ let themeCss = deliveredBase !== undefined
327
+ ? // Design-package assembler (ggui#598-C, injection-review flag 1):
328
+ // the delivered ladder gets the SAME scaffolding the compiled
329
+ // ladder gets — base inherits, derived gradient/effect tokens
330
+ // (color-mix fallback split), box-sizing, font-inherit — from
331
+ // ONE shared builder, so the two paths cannot drift.
332
+ assembleDeliveredThemeCss(scopeClass, opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light)
333
+ : opts.themeId
334
+ ? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
335
+ : getScopedCssTokens(scopeClass, opts.themeMode);
320
336
  if (opts.hostPalette) {
321
337
  // Host palette = the fallback layer (ggui#572, #573 ruling):
322
338
  // after the base scoped block so it can repaint at all (the base
@@ -51,10 +51,19 @@ export interface RenderItemOptions {
51
51
  * so the renderer injects the `--ggui-*` overrides + `color-scheme` at
52
52
  * `:root`. Sourced from the bootstrap's `_meta["ai.ggui/render"].theme`.
53
53
  * A structural subset of protocol's `AppTheme` (`name` is display-only).
54
+ * `base` is the delivered registered ladder (ggui#598-C) — forwarded
55
+ * verbatim; the renderer injects the mode-selected set in place of
56
+ * the compiled ladder block (see
57
+ * {@link ReactRootMountOptions.appTheme}).
54
58
  */
55
59
  readonly appTheme?: {
56
60
  readonly mode: 'light' | 'dark';
57
61
  readonly cssVariables: Record<string, string>;
62
+ readonly base?: {
63
+ readonly documentHash: string;
64
+ readonly light: Readonly<Record<string, string>>;
65
+ readonly dark: Readonly<Record<string, string>>;
66
+ };
58
67
  };
59
68
  /**
60
69
  * Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
package/dist/runtime.js CHANGED
@@ -28,6 +28,7 @@
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';
@@ -48,7 +49,7 @@ import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
48
49
  import { mountRender, } from './render-item.js';
49
50
  import { fromBootstrapFailure, } from './protocol-error.js';
50
51
  import { postObservabilityToParent, } from './observability.js';
51
- import { mountUiFeedbackChrome } from './ui-feedback-chrome.js';
52
+ import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome } from './ui-feedback-chrome.js';
52
53
  import { hostCanReceiveMessages, hostCanRelayToolCalls, hostCapabilitiesCaptured, setHostCapabilities, } from './host-capabilities.js';
53
54
  import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
54
55
  import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
@@ -213,7 +214,16 @@ export function hostAnnouncedThemeMode() {
213
214
  * `buildOpts` inside `bootSequence`.
214
215
  */
215
216
  export function resolveMountThemeMode(meta) {
216
- return meta.themeMode ?? meta.theme?.mode ?? hostAnnouncedThemeMode();
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
+ });
217
227
  }
218
228
  /**
219
229
  * Resolve the mount's base-theme id from the slice (ggui#589 ask 3):
@@ -235,7 +245,12 @@ export function resolveMountThemeMode(meta) {
235
245
  * `buildOpts` inside `bootSequence`.
236
246
  */
237
247
  export function resolveMountThemeId(meta) {
238
- return meta.themeId ?? meta.theme?.name;
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
+ });
239
254
  }
240
255
  /**
241
256
  * The host's announced palette, read from the connected App's
@@ -1630,9 +1645,11 @@ export function emitAudit(args) {
1630
1645
  * Returns `'success'` when the handler said `ok:true`. Returns
1631
1646
  * `'fallback'` for any other outcome — pipe missing
1632
1647
  * (`PIPE_NOT_FOUND`), envelope rejected (`INVALID_ACTION_KIND`),
1633
- * relay error, no host relay wired, or any unexpected shape. The
1634
- * iframe-runtime then falls through to `ui/message` so the gesture
1635
- * still reaches the agent on its next turn.
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).
1636
1653
  */
1637
1654
  /**
1638
1655
  * Unwrap the payload record of a relayed submit-action tool result.
@@ -1698,6 +1715,16 @@ function isRelayShapedFailure(resp) {
1698
1715
  * consumer" — the doorbell rings. Shares {@link submitActionPayload}
1699
1716
  * with {@link classifySubmitActionResponse} so both reads agree on
1700
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.
1701
1728
  */
1702
1729
  function extractConsumerPresent(resp) {
1703
1730
  const inner = submitActionPayload(resp);
@@ -1908,7 +1935,7 @@ function showActionToast(text, kind, onDismiss) {
1908
1935
  'bottom:16px',
1909
1936
  'left:50%',
1910
1937
  'transform:translateX(-50%)',
1911
- 'z-index:2147483647',
1938
+ `z-index:${ACTION_TOAST_Z_INDEX}`,
1912
1939
  'pointer-events:auto',
1913
1940
  'max-width:90%',
1914
1941
  'padding:8px 14px',
@@ -3404,20 +3431,30 @@ async function bootProduction(opts) {
3404
3431
  }
3405
3432
  },
3406
3433
  };
3407
- // Spec-canonical outbound dispatch. The WS pipe is for streamSpec
3408
- // subscriptions ONLY (inbound `ggui_emit` fanout + `props_update` +
3409
- // `render` + `data` + `drain_ack` + `channel_payload`).
3410
- // Outbound user actions go through the MCP-Apps host relay per
3411
- // spec §401: postMessage `tools/call:ggui_runtime_submit_action`
3412
- // to the parent → `AppRenderer.onCallTool` → sample agent's
3413
- // `/relay/tools-call` → ggui MCP server →
3414
- // `createGguiSubmitActionHandler.append` → `pendingEventConsumer`
3415
- // → `ggui_consume` wakes the agent. The server's WS
3416
- // `handleInboundAction` writes to the render ledger only — no
3417
- // downstream consumer — so the WS action path silently drops
3418
- // clicks. `routeDispatch` is the shared named-export helper
3419
- // (production + tests exercise the same code path); threading it
3420
- // here keeps LIVE-mode on the canonical dispatch pipeline.
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.
3421
3458
  // Single mounted render — populated by `applyRender` on the first
3422
3459
  // render frame. The wire config + data channel handler read it
3423
3460
  // through `currentRender`-returning thunks so they always see the
@@ -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, {
@@ -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 (first-party `createGguiServer` does — see
99
- * `GguiSessionChannelOptions.pendingEventConsumer`); it remains the
100
- * seam tests and direct-WS callers exercise.
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.11.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/preview-a2ui": "0.11.0",
40
- "@ggui-ai/live-channel": "0.11.0",
41
- "@ggui-ai/gadgets": "0.11.0",
42
- "@ggui-ai/design": "0.11.0",
43
- "@ggui-ai/protocol": "0.11.0",
44
- "@ggui-ai/wire": "0.11.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"