@ggui-ai/iframe-runtime 0.2.0-alpha.4 → 0.4.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/channel-transport.d.ts +20 -30
  2. package/dist/channel-transport.js +24 -24
  3. package/dist/channels/data.d.ts +5 -20
  4. package/dist/channels/data.js +4 -52
  5. package/dist/channels/index.d.ts +0 -1
  6. package/dist/channels/index.js +0 -1
  7. package/dist/channels/props-update.d.ts +9 -9
  8. package/dist/channels/props-update.js +9 -9
  9. package/dist/channels/render-frame.d.ts +5 -5
  10. package/dist/channels/render-frame.js +5 -5
  11. package/dist/compiled-validators.d.ts +2 -2
  12. package/dist/compiled-validators.js +1 -1
  13. package/dist/context-observer.d.ts +4 -4
  14. package/dist/context-observer.js +3 -3
  15. package/dist/events-polling.d.ts +7 -7
  16. package/dist/events-polling.js +5 -5
  17. package/dist/globals.d.ts +1 -1
  18. package/dist/host-context-emitter.d.ts +4 -4
  19. package/dist/host-context-emitter.js +9 -9
  20. package/dist/iframe-runtime.js +68 -78
  21. package/dist/iframe-runtime.meta.json +1 -0
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +1 -1
  24. package/dist/lifecycle.d.ts +3 -3
  25. package/dist/lifecycle.js +29 -6
  26. package/dist/mcp-app-iframe-host.d.ts +8 -8
  27. package/dist/mcp-app-iframe-host.js +4 -4
  28. package/dist/meta-parse.d.ts +3 -4
  29. package/dist/meta-parse.js +17 -44
  30. package/dist/observability.d.ts +14 -36
  31. package/dist/observability.js +2 -1
  32. package/dist/protocol-error.d.ts +38 -48
  33. package/dist/protocol-error.js +5 -23
  34. package/dist/provisional-renderer.js +3 -3
  35. package/dist/react-renderer.d.ts +16 -6
  36. package/dist/react-renderer.js +22 -40
  37. package/dist/registry-subscribe.d.ts +7 -7
  38. package/dist/registry-subscribe.js +28 -14
  39. package/dist/render-item.d.ts +22 -15
  40. package/dist/render-item.js +7 -14
  41. package/dist/runtime.d.ts +79 -105
  42. package/dist/runtime.js +219 -269
  43. package/dist/status-dom.d.ts +12 -12
  44. package/dist/status-dom.js +11 -11
  45. package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
  46. package/dist/system-cards/NoCredentialsCard.js +1 -1
  47. package/dist/system-cards/ProtocolProbeCard.d.ts +13 -17
  48. package/dist/system-cards/ProtocolProbeCard.js +21 -25
  49. package/dist/system-cards/index.d.ts +5 -5
  50. package/dist/system-cards/index.js +5 -5
  51. package/dist/types.d.ts +29 -34
  52. package/dist/validation.d.ts +4 -5
  53. package/dist/validation.js +5 -6
  54. package/dist/wire-config.d.ts +43 -106
  55. package/dist/wire-config.js +29 -195
  56. package/package.json +14 -12
  57. package/dist/channels/feedback.d.ts +0 -14
  58. package/dist/channels/feedback.js +0 -19
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Default emitter — operator-visible `console.warn` with a grep-
3
- * friendly tag. Identical posture to `GguiSession.surfaceContractViolation`'s
3
+ * friendly tag. Identical posture to `GguiRender.surfaceContractViolation`'s
4
4
  * console.warn fallback. Callers override by injecting their own.
5
5
  */
6
6
  export function defaultProtocolErrorEmitter(err) {
@@ -17,18 +17,10 @@ export function defaultProtocolErrorEmitter(err) {
17
17
  * outbound action, or when an inbound stream / props payload fails the
18
18
  * local validator) onto a `ProtocolError`.
19
19
  *
20
- * Client-side violations do NOT ride the server-emitted
21
- * `_ggui:contract-error` envelope — that shape (`ContractErrorPayload`)
22
- * is tool-failure-scoped and authored by the session-channel router.
23
- * A client-side violation is a DIFFERENT category: nothing reached the
24
- * tool. We surface it as `{kind: 'protocol'; code:
20
+ * A client-side violation never left the renderer — nothing reached
21
+ * the server. We surface it as `{kind: 'protocol'; code:
25
22
  * 'CLIENT_CONTRACT_VIOLATION'}` with structured `details` so hosts can
26
23
  * render the same summary that `console.warn` would have produced.
27
- *
28
- * Server-emitted `ContractErrorPayload` landing on
29
- * `_ggui:contract-error` rides the separate `{kind: 'contract'}`
30
- * variant — that path activates when the renderer observes the
31
- * reserved channel.
32
24
  */
33
25
  export function fromClientContractViolation(err) {
34
26
  return {
@@ -41,16 +33,6 @@ export function fromClientContractViolation(err) {
41
33
  },
42
34
  };
43
35
  }
44
- /**
45
- * Wrap a server-emitted `ContractErrorPayload` (received over the
46
- * reserved `_ggui:contract-error` channel) as the 'contract' variant.
47
- * Trivial wrapper — exists so call sites produce the union via a
48
- * factory rather than an object literal (keeps emission uniform and
49
- * makes grep for creation sites exact).
50
- */
51
- export function fromContractErrorPayload(payload) {
52
- return { kind: 'contract', payload };
53
- }
54
36
  /**
55
37
  * Build a `ProtocolError` of kind 'version' from the existing
56
38
  * `UpgradeRequiredError` class. Matches the `liftUpgradeRequiredFromError`
@@ -104,8 +86,8 @@ export function fromTransportFailure(code, retryable, message) {
104
86
  };
105
87
  }
106
88
  /**
107
- * Build a `ProtocolError` of kind 'auth' — session / token rejection
108
- * pre-handshake.
89
+ * Build a `ProtocolError` of kind 'auth' — session / ws-token rejection
90
+ * pre-ack (see {@link AuthFailureCode}).
109
91
  */
110
92
  export function fromAuthFailure(code, message) {
111
93
  return {
@@ -36,9 +36,9 @@ import { createRoot } from 'react-dom/client';
36
36
  * Spec-canonical A2UI preview channel name (from
37
37
  * `@ggui-ai/protocol::PREVIEW_CHANNEL`). Inlined here so this
38
38
  * module's runtime imports stay off the protocol root barrel —
39
- * importing the constant from `@ggui-ai/protocol` pulls in
40
- * `openrouter-models` + the entire zod validation graph via the
41
- * root re-export chain, adding ~250 KB gz to the renderer bundle.
39
+ * importing the constant from `@ggui-ai/protocol` pulls in the
40
+ * entire zod validation graph via the root re-export chain,
41
+ * adding hundreds of KB gz to the renderer bundle.
42
42
  * The value is a stable string literal (`isKnownReservedChannel`
43
43
  * and the spec both reference `_ggui:preview` directly); re-declaring
44
44
  * it here is shape-preserving and covered by the structural-lock
@@ -22,9 +22,8 @@
22
22
  *
23
23
  * Error handling — ErrorBoundary with auto-retry (AUTO_RETRY_LIMIT +
24
24
  * AUTO_RETRY_DELAY) matches the `ReactComponentRenderer` contract.
25
- * On terminal error, the callback `onRequestRepair` fires; the
26
- * render dispatcher (`render-item.ts`) wires this to the
27
- * `ggui:request-repair` live-channel envelope it dispatches.
25
+ * On terminal error, `onError` fires and the boundary paints the
26
+ * terminal error UI.
28
27
  *
29
28
  * No JSX in this file — the mount-root code uses
30
29
  * `React.createElement` + `React.Fragment` directly so the renderer's
@@ -33,7 +32,7 @@
33
32
  import React, { type ReactNode } from 'react';
34
33
  export interface ReactRootMountOptions {
35
34
  /**
36
- * Render whose componentCode + props drive the mount. Only
35
+ * GguiSession whose componentCode + props drive the mount. Only
37
36
  * `componentCode` + `props` are read here; the caller is
38
37
  * responsible for wrapping this with a `<GguiWireProvider>` at the
39
38
  * higher-level dispatcher (see `render-item.ts`).
@@ -50,9 +49,20 @@ export interface ReactRootMountOptions {
50
49
  * registered theme paints when the operator selected it.
51
50
  */
52
51
  readonly themeMode?: 'light' | 'dark';
52
+ /**
53
+ * Per-app theme overlay applied at the iframe's `:root` AFTER the base
54
+ * token block, so the operator-supplied `--ggui-*` values win the
55
+ * cascade (a partial set — unset vars keep their token defaults). A
56
+ * structural subset of protocol's `AppTheme` (`name` is display-only,
57
+ * not needed at render). `mode` drives `color-scheme` so native
58
+ * controls / scrollbars / canvas pick up dark vs light.
59
+ */
60
+ readonly appTheme?: {
61
+ readonly mode: 'light' | 'dark';
62
+ readonly cssVariables: Record<string, string>;
63
+ };
53
64
  readonly cssOverrides?: string;
54
65
  readonly onError?: (error: Error) => void;
55
- readonly onRequestRepair?: (error: Error) => void;
56
66
  /**
57
67
  * Children injected BETWEEN the mount DOM (scope + CSS) and the
58
68
  * evaluated component element. The caller's render dispatcher
@@ -64,7 +74,7 @@ export interface ReactRootMountOptions {
64
74
  readonly renderWrapper?: (mountedComponent: ReactNode) => ReactNode;
65
75
  /**
66
76
  * Bare npm package names of the operator-registered 3rd-party gadget
67
- * packages reachable in this session (GG.8.2). Forwarded to
77
+ * packages reachable in this render (GG.8.2). Forwarded to
68
78
  * `rewriteImports` so each direct gadget import resolves to its
69
79
  * per-package data-URL shim. Sourced from the bootstrap's `gadgets`
70
80
  * channel; `@ggui-ai/gadgets` (STDLIB) is always rewritten and need
@@ -22,9 +22,8 @@
22
22
  *
23
23
  * Error handling — ErrorBoundary with auto-retry (AUTO_RETRY_LIMIT +
24
24
  * AUTO_RETRY_DELAY) matches the `ReactComponentRenderer` contract.
25
- * On terminal error, the callback `onRequestRepair` fires; the
26
- * render dispatcher (`render-item.ts`) wires this to the
27
- * `ggui:request-repair` live-channel envelope it dispatches.
25
+ * On terminal error, `onError` fires and the boundary paints the
26
+ * terminal error UI.
28
27
  *
29
28
  * No JSX in this file — the mount-root code uses
30
29
  * `React.createElement` + `React.Fragment` directly so the renderer's
@@ -59,7 +58,6 @@ class RcrErrorBoundary extends Component {
59
58
  error: null,
60
59
  catchCount: 0,
61
60
  autoRetrying: false,
62
- repairRequested: false,
63
61
  };
64
62
  retryTimer = null;
65
63
  static getDerivedStateFromError(error) {
@@ -76,20 +74,16 @@ class RcrErrorBoundary extends Component {
76
74
  return;
77
75
  }
78
76
  this.props.onError?.(error);
79
- if (this.props.onRequestRepair && !this.state.repairRequested) {
80
- this.setState({ repairRequested: true });
81
- this.props.onRequestRepair(error);
82
- }
83
77
  }
84
78
  componentWillUnmount() {
85
79
  if (this.retryTimer !== null)
86
80
  clearTimeout(this.retryTimer);
87
81
  }
88
82
  render() {
89
- const { error, autoRetrying, repairRequested, catchCount } = this.state;
83
+ const { error, autoRetrying, catchCount } = this.state;
90
84
  if (error === null)
91
85
  return this.props.children;
92
- // The three fallback UIs match the host-SDK error boundary
86
+ // The two fallback UIs match the host-SDK error boundary
93
87
  // verbatim (same inline styles so operator-visible DOM is
94
88
  // identical between the host-SDK and iframe-renderer paths).
95
89
  const FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
@@ -117,30 +111,6 @@ class RcrErrorBoundary extends Component {
117
111
  },
118
112
  }), 'Retrying...', createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
119
113
  }
120
- if (repairRequested) {
121
- return createElement('div', {
122
- style: {
123
- display: 'flex',
124
- flexDirection: 'column',
125
- alignItems: 'center',
126
- justifyContent: 'center',
127
- padding: '32px 24px',
128
- minHeight: 120,
129
- gap: 10,
130
- textAlign: 'center',
131
- fontFamily: FONT,
132
- },
133
- }, createElement('div', {
134
- style: {
135
- width: 16,
136
- height: 16,
137
- border: '2px solid rgba(139,92,246,0.2)',
138
- borderTopColor: 'rgba(139,92,246,0.7)',
139
- borderRadius: '50%',
140
- animation: 'ggui-err-spin 0.8s linear infinite',
141
- },
142
- }), createElement('div', { style: { fontSize: 13, color: 'rgba(255,255,255,0.5)' } }, 'Repairing component...'), createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
143
- }
144
114
  return createElement('div', {
145
115
  style: {
146
116
  display: 'flex',
@@ -165,7 +135,7 @@ class RcrErrorBoundary extends Component {
165
135
  fontSize: 22,
166
136
  },
167
137
  }, '!'), createElement('div', { style: { fontSize: 14, fontWeight: 600, color: 'rgba(255,255,255,0.8)' } }, 'Something went wrong'), createElement('div', { style: { fontSize: 12, color: 'rgba(255,255,255,0.4)', maxWidth: 280, lineHeight: 1.4 } }, 'This component encountered an error. Try asking for a different view.'), createElement('button', {
168
- onClick: () => this.setState({ error: null, catchCount: 0, repairRequested: false }),
138
+ onClick: () => this.setState({ error: null, catchCount: 0 }),
169
139
  style: {
170
140
  marginTop: 4,
171
141
  padding: '8px 20px',
@@ -241,9 +211,22 @@ export async function mountReactRoot(container, opts) {
241
211
  // `ThemeProvider` so a Studio/Portal context with both runtime + RP
242
212
  // active doesn't double-stack.
243
213
  if (typeof document !== 'undefined') {
244
- const rootCss = opts.themeId
214
+ let rootCss = opts.themeId
245
215
  ? getThemeCss(opts.themeId, opts.themeMode)
246
216
  : getCssTokens(opts.themeMode);
217
+ if (opts.appTheme) {
218
+ const decls = Object.entries(opts.appTheme.cssVariables)
219
+ .map(([k, v]) => `${k}: ${v};`)
220
+ .join('');
221
+ // Appended AFTER the base block → the per-app overlay wins; it is a
222
+ // partial set (unset vars keep their token defaults). CSS custom
223
+ // properties at :root inherit into the scoped React tree, so this
224
+ // alone is sufficient for the component to resolve `var(--ggui-*)`.
225
+ // Values were validated injection-safe upstream (write-side
226
+ // `appThemeSchema` + the wire parser re-validates), so the
227
+ // string-join here is safe — do NOT re-sanitize.
228
+ rootCss += `:root{color-scheme:${opts.appTheme.mode};${decls}}`;
229
+ }
247
230
  const styleId = 'ggui-theme-vars';
248
231
  let styleEl = document.getElementById(styleId);
249
232
  if (!styleEl) {
@@ -267,7 +250,6 @@ export async function mountReactRoot(container, opts) {
267
250
  root.render(createElement('div', { className: scopeClass }, createElement('style', null, `${themeCss}${opts.cssOverrides ?? ''}`), createElement(RcrErrorBoundary, {
268
251
  key: currentCode?.length ?? 0,
269
252
  ...(opts.onError ? { onError: opts.onError } : {}),
270
- ...(opts.onRequestRepair ? { onRequestRepair: opts.onRequestRepair } : {}),
271
253
  }, createElement(Fragment, null, wrapped))));
272
254
  }
273
255
  async function initialEvaluate() {
@@ -286,9 +268,9 @@ export async function mountReactRoot(container, opts) {
286
268
  catch (err) {
287
269
  const e = err instanceof Error ? err : new Error(String(err));
288
270
  // Fail-loud on eval errors. The OPTIONAL `onError` callback is
289
- // not enough on its own — bootSelfContained doesn't wire one,
290
- // so without this log a self-contained mount with a broken
291
- // component renders a blank iframe and emits nothing.
271
+ // not enough on its own — not every mount path wires one, so
272
+ // without this log a mount with a broken component renders a
273
+ // blank iframe and emits nothing.
292
274
  // eslint-disable-next-line no-console -- operator-visible failure signal
293
275
  console.error('[ggui] mountReactRoot: component evaluation failed —', e);
294
276
  currentComponent = null;
@@ -1,4 +1,4 @@
1
- import type { AckPayload, ContractErrorPayload } from '@ggui-ai/protocol';
1
+ import type { AckPayload } from '@ggui-ai/protocol';
2
2
  import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
3
3
  import type { McpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
4
4
  import type { ChannelRegistry, RegistryPollingOptions, WsTransportHandle } from '@ggui-ai/live-channel';
@@ -8,7 +8,7 @@ import type { ObservabilityEmitter } from './observability.js';
8
8
  * Result of a successful handshake — the live typed transport handle
9
9
  * plus the resolved `AckPayload` from the first ack frame. The caller
10
10
  * attaches `handle` to the renderer's outbound send surface (via the
11
- * BufferedManagerShim flush) and applies `ack.render` to the mounted
11
+ * BufferedManagerShim flush) and applies `ack.session` to the mounted
12
12
  * render slot.
13
13
  */
14
14
  export interface RegistrySubscribeHandle {
@@ -21,8 +21,8 @@ export interface RegistrySubscribeHandle {
21
21
  */
22
22
  export interface ConnectViaRegistryOptions {
23
23
  /**
24
- * Render slice (`McpAppAiGguiRenderMeta`) — the live-channel
25
- * credentials this subscribe path actually needs: `renderId`,
24
+ * GguiSession slice (`McpAppAiGguiRenderMeta`) — the live-channel
25
+ * credentials this subscribe path actually needs: `sessionId`,
26
26
  * `appId`, `wsUrl`, `wsToken`. The rest of the slice (codeUrl /
27
27
  * propsJson / contextSlots / etc.) is load-bearing only for the
28
28
  * renderer / mount layer.
@@ -72,7 +72,7 @@ export interface ConnectViaRegistryOptions {
72
72
  * during a WS dropout window flows back into the iframe on
73
73
  * reconnect.
74
74
  *
75
- * Idempotent at the runtime layer: `applyRender(ack.render)` patches
75
+ * Idempotent at the runtime layer: `applyRender(ack.session)` patches
76
76
  * in place. Channels re-subscribed by
77
77
  * `ChannelTransportRouter.onWsStatusChange('connected')` upstream
78
78
  * of this callback, so the per-channel transport state is already
@@ -100,7 +100,7 @@ export interface ConnectViaRegistryOptions {
100
100
  * error.
101
101
  *
102
102
  * Once the promise settles, subsequent server frames flow through the
103
- * registered channel handlers (props_update, drain_ack, push, data,
103
+ * registered channel handlers (props_update, drain_ack, render, data,
104
104
  * etc.) — there is no longer a separate `onMessage` callback fanning
105
105
  * frames across multiple handler chains.
106
106
  */
@@ -117,4 +117,4 @@ export type ConnectFn = typeof connectViaRegistry;
117
117
  * Type re-export for callers that want to thread the handle type
118
118
  * through their own seams (e.g. `bootSequence`'s shim attach).
119
119
  */
120
- export type { WsTransportHandle, ContractErrorPayload };
120
+ export type { WsTransportHandle };
@@ -6,9 +6,9 @@
6
6
  * Replaces the pre-B3b `subscribe()` helper + the `RendererWebSocketManager`
7
7
  * lifecycle class. The two retired modules paired a hand-rolled WS
8
8
  * manager with a separate handshake helper; B3b unifies both behind
9
- * the `@ggui-ai/live-channel` library so every WS frame type (push,
9
+ * the `@ggui-ai/live-channel` library so every WS frame type (render,
10
10
  * data, props_update, drain_ack, channel_payload, channel_error,
11
- * system, feedback) flows through one registry-owned transport.
11
+ * system) flows through one registry-owned transport.
12
12
  *
13
13
  * Composition:
14
14
  *
@@ -92,19 +92,33 @@ function normalizeObservedVersion(value) {
92
92
  return value;
93
93
  return value.join(', ');
94
94
  }
95
+ /**
96
+ * Auth-class pre-ack codes — the §12.2.3 vocabulary first-party servers
97
+ * actually emit on the pre-ack error frame (ws-token verification +
98
+ * upgrade-auth failures). Replaced the phantom `AUTH_REJECTED` /
99
+ * `TOKEN_EXPIRED` arms in draft-2026-06-12 — a real ws-token expiry
100
+ * previously fell into the generic `'protocol'` bucket.
101
+ */
102
+ const PRE_ACK_AUTH_CODES = new Set([
103
+ 'SESSION_NOT_FOUND',
104
+ 'BOOTSTRAP_EXPIRED',
105
+ 'BOOTSTRAP_INVALID',
106
+ 'BOOTSTRAP_SESSION_MISMATCH',
107
+ 'BOOTSTRAP_APP_MISMATCH',
108
+ 'UNAUTHENTICATED',
109
+ ]);
110
+ function isAuthFailureCode(code) {
111
+ return PRE_ACK_AUTH_CODES.has(code);
112
+ }
95
113
  /**
96
114
  * Map a pre-ack `error` frame onto a non-UPGRADE_REQUIRED ProtocolError.
97
- * `RENDER_NOT_FOUND` / `AUTH_REJECTED` → `'auth'`; every other code
98
- * → `'protocol'` with the extensibly-closed tail carrying the wire code
99
- * verbatim.
115
+ * {@link PRE_ACK_AUTH_CODES} → `'auth'`; every other code → `'protocol'`
116
+ * with the extensibly-closed tail carrying the wire code verbatim.
100
117
  */
101
118
  function classifyPreAckError(payload) {
102
- if (payload.code === 'RENDER_NOT_FOUND' || payload.code === 'AUTH_REJECTED') {
119
+ if (isAuthFailureCode(payload.code)) {
103
120
  return fromAuthFailure(payload.code, payload.message);
104
121
  }
105
- if (payload.code === 'TOKEN_EXPIRED') {
106
- return fromAuthFailure('TOKEN_EXPIRED', payload.message);
107
- }
108
122
  return {
109
123
  kind: 'protocol',
110
124
  code: payload.code,
@@ -121,7 +135,7 @@ function classifyPreAckError(payload) {
121
135
  * error.
122
136
  *
123
137
  * Once the promise settles, subsequent server frames flow through the
124
- * registered channel handlers (props_update, drain_ack, push, data,
138
+ * registered channel handlers (props_update, drain_ack, render, data,
125
139
  * etc.) — there is no longer a separate `onMessage` callback fanning
126
140
  * frames across multiple handler chains.
127
141
  */
@@ -153,9 +167,9 @@ export function connectViaRegistry(opts) {
153
167
  onMessage: (payload) => {
154
168
  const ack = payload;
155
169
  // Post-handshake ack — the WS reconnected and the server
156
- // re-sent the subscribe ack with a fresh `stack` snapshot.
170
+ // re-sent the subscribe ack with a fresh `render` snapshot.
157
171
  // Fan it onto the caller's resubscribe callback so the
158
- // runtime can reapply the authoritative stack (a push or
172
+ // runtime can reapply the authoritative render (a render or
159
173
  // update that landed during the WS dropout window now
160
174
  // restores). Skip the version handshake / settle bookkeeping
161
175
  // — that already fired on the original ack.
@@ -270,7 +284,7 @@ export function connectViaRegistry(opts) {
270
284
  const composedUrl = composeWsUrl(wsUrl, wsToken);
271
285
  // `bind()` selects the transport (WS for wsUrl+wsToken sessions;
272
286
  // the live-channel narrows on its `bootstrap` shape) and starts
273
- // it. We override `session.wsUrl` with the composed URL so the
287
+ // it. We override `bootstrap.wsUrl` with the composed URL so the
274
288
  // registry's WS transport gets the wsToken-threaded variant. The
275
289
  // live-channel's `bootstrap` arg is its internal name; the field
276
290
  // on the wire is `wsToken`.
@@ -279,7 +293,7 @@ export function connectViaRegistry(opts) {
279
293
  bootstrap: {
280
294
  wsUrl: composedUrl,
281
295
  wsToken,
282
- renderId: opts.meta.renderId,
296
+ sessionId: opts.meta.sessionId,
283
297
  appId: opts.meta.appId,
284
298
  },
285
299
  onStatusChange: mappedStatusCallback,
@@ -1,36 +1,34 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { Render } from '@ggui-ai/protocol';
3
- import type { RenderSeedInput } from './types.js';
2
+ import type { GguiSession } from '@ggui-ai/protocol';
3
+ import type { GguiSessionSeedInput } from './types.js';
4
4
  import { type WireConfig } from '@ggui-ai/wire';
5
5
  import type { StreamBus } from './wire-config.js';
6
6
  export interface RenderItemOptions {
7
7
  /** The render to mount. */
8
- readonly render: Render | RenderSeedInput;
9
- /** Wire config for this render — caller pre-scopes via the runtime's
10
- * `buildScopedConfig(render)` (see
11
- * `wire-config.ts::RootWireConfigBundle`). `null` ⇒ the component
12
- * mounts without a wire provider (standalone — matches today's
13
- * DynamicComponent fallback when no GguiSession parent is present). */
8
+ readonly render: GguiSession | GguiSessionSeedInput;
9
+ /** Wire config for this render — caller builds it via the runtime's
10
+ * `buildRootWireConfig(...)` (see `wire-config.ts`). `null` ⇒ the
11
+ * component mounts without a wire provider (standalone — matches
12
+ * today's DynamicComponent fallback when no GguiRender parent is
13
+ * present). */
14
14
  readonly scopedWireConfig: WireConfig | null;
15
15
  /** Shared stream bus — provisional renderer subscribes to
16
16
  * `_ggui:preview` for this render. */
17
17
  readonly streamBus: StreamBus;
18
- /** Render id — used by the mcp-apps iframe host for proxy URLs. */
19
- readonly renderId: string;
18
+ /** GguiSession id — used by the mcp-apps iframe host for proxy URLs. */
19
+ readonly sessionId: string;
20
20
  /**
21
21
  * 3rd-party gadget package names whose direct imports the rewriter
22
22
  * must resolve to per-package shims. FALLBACK source used only when
23
23
  * the render carries no `gadgetDescriptors` sidecar — i.e. the static
24
- * seed mount ({@link RenderSeedInput}, which has no descriptors). The
24
+ * seed mount ({@link GguiSessionSeedInput}, which has no descriptors). The
25
25
  * caller threads the bootstrap's `meta.gadgets` package names here so a
26
26
  * generated component importing a non-STDLIB gadget on a no-WS host
27
27
  * (claude.ai / ChatGPT) still resolves its shim on first paint. A
28
- * WS-delivered full `Render` carries `gadgetDescriptors` and ignores
28
+ * WS-delivered full `GguiSession` carries `gadgetDescriptors` and ignores
29
29
  * this. STDLIB `@ggui-ai/gadgets` is always rewritten regardless.
30
30
  */
31
31
  readonly gadgetPackages?: readonly string[];
32
- /** ggui-server base URL (for mcp-apps proxy). Empty = same-origin. */
33
- readonly mcpAppsServerBaseUrl?: string;
34
32
  /** Theme id passed to ReactComponentRenderer for scoped CSS. */
35
33
  readonly themeId?: string;
36
34
  /**
@@ -39,9 +37,18 @@ export interface RenderItemOptions {
39
37
  * design registry's `getTheme(id, mode?)` default.
40
38
  */
41
39
  readonly themeMode?: 'light' | 'dark';
40
+ /**
41
+ * Per-app theme overlay forwarded to {@link ReactRootMountOptions.appTheme}
42
+ * so the renderer injects the `--ggui-*` overrides + `color-scheme` at
43
+ * `:root`. Sourced from the bootstrap's `_meta["ai.ggui/render"].theme`.
44
+ * A structural subset of protocol's `AppTheme` (`name` is display-only).
45
+ */
46
+ readonly appTheme?: {
47
+ readonly mode: 'light' | 'dark';
48
+ readonly cssVariables: Record<string, string>;
49
+ };
42
50
  readonly cssOverrides?: string;
43
51
  readonly onError?: (err: Error) => void;
44
- readonly onRequestRepair?: (err: Error) => void;
45
52
  /**
46
53
  * Optional outer wrapper applied AROUND the GguiWireProvider — used
47
54
  * by `bootProduction` to install the `<ContextStateHost>` provider
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Render dispatcher — routes a single {@link Render} to the
2
+ * GguiSession dispatcher — routes a single {@link GguiSession} to the
3
3
  * appropriate renderer based on its discriminator.
4
4
  *
5
5
  * Post-render-identity-collapse (2026-05-27): the iframe-runtime mounts
@@ -101,21 +101,18 @@ export async function mountRender(container, opts) {
101
101
  }
102
102
  async function mountByKind(kind) {
103
103
  if (kind === 'mcpApps') {
104
- // Render narrows to McpAppsRender when type='mcpApps'.
104
+ // GguiSession narrows to McpAppsGguiSession when type='mcpApps'.
105
105
  const render = currentOpts.render;
106
106
  mcpMount = mountMcpAppIframe(container, {
107
107
  render,
108
- renderId: currentOpts.renderId,
109
- ...(currentOpts.mcpAppsServerBaseUrl !== undefined
110
- ? { serverBaseUrl: currentOpts.mcpAppsServerBaseUrl }
111
- : {}),
108
+ sessionId: currentOpts.sessionId,
112
109
  });
113
110
  currentKind = 'mcpApps';
114
111
  return;
115
112
  }
116
113
  if (kind === 'system') {
117
114
  // System card — server-emitted `kind` mapped to a built-in
118
- // `.tsx` via `SystemCardHost`. Render-only: no `__ggui__` shim,
115
+ // `.tsx` via `SystemCardHost`. GguiSession-only: no `__ggui__` shim,
119
116
  // no GguiWireProvider, no contextSpec wrap (system cards don't
120
117
  // dispatch wire actions or read contexts). The registry handles
121
118
  // unknown kinds via a typed fallback. Dynamic-import keeps the
@@ -176,13 +173,11 @@ export async function mountRender(container, opts) {
176
173
  ...(gadgetPackages !== undefined ? { gadgetPackages } : {}),
177
174
  ...(currentOpts.themeId !== undefined ? { themeId: currentOpts.themeId } : {}),
178
175
  ...(currentOpts.themeMode !== undefined ? { themeMode: currentOpts.themeMode } : {}),
176
+ ...(currentOpts.appTheme !== undefined ? { appTheme: currentOpts.appTheme } : {}),
179
177
  ...(currentOpts.cssOverrides !== undefined
180
178
  ? { cssOverrides: currentOpts.cssOverrides }
181
179
  : {}),
182
180
  ...(currentOpts.onError !== undefined ? { onError: currentOpts.onError } : {}),
183
- ...(currentOpts.onRequestRepair !== undefined
184
- ? { onRequestRepair: currentOpts.onRequestRepair }
185
- : {}),
186
181
  renderWrapper: wrapInScopedProvider,
187
182
  });
188
183
  currentKind = 'react';
@@ -191,7 +186,7 @@ export async function mountRender(container, opts) {
191
186
  * Wrap the mounted component in `<GguiWireProvider
192
187
  * config={scopedConfig}>` when the caller supplied one. Without a
193
188
  * scoped config, the component renders standalone — matches
194
- * today's DynamicComponent fallback when no GguiSession parent is
189
+ * today's DynamicComponent fallback when no GguiRender parent is
195
190
  * present.
196
191
  *
197
192
  * Composition order (outer → inner):
@@ -242,11 +237,9 @@ export async function mountRender(container, opts) {
242
237
  },
243
238
  ...(next.themeId !== undefined ? { themeId: next.themeId } : {}),
244
239
  ...(next.themeMode !== undefined ? { themeMode: next.themeMode } : {}),
240
+ ...(next.appTheme !== undefined ? { appTheme: next.appTheme } : {}),
245
241
  ...(next.cssOverrides !== undefined ? { cssOverrides: next.cssOverrides } : {}),
246
242
  ...(next.onError !== undefined ? { onError: next.onError } : {}),
247
- ...(next.onRequestRepair !== undefined
248
- ? { onRequestRepair: next.onRequestRepair }
249
- : {}),
250
243
  renderWrapper: wrapInScopedProvider,
251
244
  });
252
245
  return;