@ggui-ai/iframe-runtime 0.1.0-rc.1 → 0.2.0-alpha.1

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 (83) hide show
  1. package/README.md +7 -6
  2. package/dist/channel-transport.d.ts +15 -15
  3. package/dist/channel-transport.js +32 -35
  4. package/dist/channels/channel-payload.d.ts +2 -2
  5. package/dist/channels/channel-payload.js +1 -1
  6. package/dist/channels/data.d.ts +14 -19
  7. package/dist/channels/data.js +13 -31
  8. package/dist/channels/drain-ack.d.ts +2 -2
  9. package/dist/channels/drain-ack.js +1 -1
  10. package/dist/channels/feedback.d.ts +1 -1
  11. package/dist/channels/index.d.ts +1 -1
  12. package/dist/channels/index.js +1 -1
  13. package/dist/channels/props-update.d.ts +33 -37
  14. package/dist/channels/props-update.js +30 -106
  15. package/dist/channels/render-frame.d.ts +52 -0
  16. package/dist/channels/render-frame.js +53 -0
  17. package/dist/channels/system.d.ts +1 -1
  18. package/dist/compiled-validators.d.ts +15 -0
  19. package/dist/compiled-validators.js +31 -0
  20. package/dist/context-observer.d.ts +68 -8
  21. package/dist/context-observer.js +25 -45
  22. package/dist/events-polling.d.ts +95 -0
  23. package/dist/events-polling.js +143 -0
  24. package/dist/gadget-loader.d.ts +1 -1
  25. package/dist/globals.d.ts +4 -4
  26. package/dist/host-context-emitter.d.ts +84 -20
  27. package/dist/host-context-emitter.js +159 -29
  28. package/dist/iframe-runtime.js +98 -59
  29. package/dist/index.d.ts +18 -13
  30. package/dist/index.js +1 -1
  31. package/dist/lifecycle.d.ts +2 -2
  32. package/dist/lifecycle.js +5 -5
  33. package/dist/mcp-app-iframe-host.d.ts +9 -9
  34. package/dist/mcp-app-iframe-host.js +16 -17
  35. package/dist/meta-parse.d.ts +44 -0
  36. package/dist/meta-parse.js +390 -0
  37. package/dist/observability.d.ts +6 -6
  38. package/dist/protocol-error.d.ts +7 -7
  39. package/dist/provisional-renderer.d.ts +1 -1
  40. package/dist/provisional-renderer.js +1 -1
  41. package/dist/react-renderer.d.ts +10 -11
  42. package/dist/react-renderer.js +8 -8
  43. package/dist/registry-subscribe.d.ts +27 -9
  44. package/dist/registry-subscribe.js +32 -27
  45. package/dist/render-item.d.ts +83 -0
  46. package/dist/{stack-item-renderer.js → render-item.js} +106 -106
  47. package/dist/runtime.d.ts +183 -131
  48. package/dist/runtime.js +1264 -1330
  49. package/dist/status-dom.d.ts +52 -47
  50. package/dist/status-dom.js +51 -34
  51. package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
  52. package/dist/system-cards/NoCredentialsCard.js +1 -1
  53. package/dist/system-cards/ProtocolProbeCard.js +2 -2
  54. package/dist/system-cards/index.d.ts +4 -4
  55. package/dist/system-cards/index.js +4 -4
  56. package/dist/types.d.ts +70 -34
  57. package/dist/types.js +1 -1
  58. package/dist/validation.d.ts +2 -2
  59. package/dist/validation.js +2 -2
  60. package/dist/wire-config.d.ts +54 -82
  61. package/dist/wire-config.js +61 -93
  62. package/package.json +9 -8
  63. package/dist/bootstrap.d.ts +0 -72
  64. package/dist/bootstrap.js +0 -500
  65. package/dist/canvas/animator/animator-host.d.ts +0 -63
  66. package/dist/canvas/animator/animator-host.js +0 -105
  67. package/dist/canvas/animator/animator-pill.d.ts +0 -42
  68. package/dist/canvas/animator/animator-pill.js +0 -119
  69. package/dist/canvas/animator/state-machine.d.ts +0 -106
  70. package/dist/canvas/animator/state-machine.js +0 -150
  71. package/dist/canvas/canvas-shell.d.ts +0 -77
  72. package/dist/canvas/canvas-shell.js +0 -117
  73. package/dist/canvas/display-mode-policy.d.ts +0 -104
  74. package/dist/canvas/display-mode-policy.js +0 -98
  75. package/dist/canvas/mount.d.ts +0 -150
  76. package/dist/canvas/mount.js +0 -202
  77. package/dist/canvas/nav-stack.d.ts +0 -133
  78. package/dist/canvas/nav-stack.js +0 -172
  79. package/dist/channels/push.d.ts +0 -47
  80. package/dist/channels/push.js +0 -54
  81. package/dist/stack-item-renderer.d.ts +0 -103
  82. package/dist/stack.d.ts +0 -90
  83. package/dist/stack.js +0 -118
@@ -1,27 +1,32 @@
1
1
  /**
2
- * Stack-item dispatcher — routes each SessionStackEntry to the
2
+ * Render dispatcher — routes a single {@link Render} to the
3
3
  * appropriate renderer based on its discriminator.
4
4
  *
5
+ * Post-render-identity-collapse (2026-05-27): the iframe-runtime mounts
6
+ * EXACTLY ONE render per iframe, in a single container. This module
7
+ * exposes one entry point — {@link mountRender} — that returns a
8
+ * {@link RenderItemHandle} the runtime uses to apply subsequent
9
+ * patches (props_update) or unmount on teardown. The earlier
10
+ * `StackRenderer` / `StackRenderContext` orchestrator (a stack-of-N
11
+ * map keyed by item id) was retired along with `StackModel`.
12
+ *
5
13
  * The dispatch tree mirrors `@ggui-ai/react::DynamicComponent` +
6
- * `McpAppsStackItemRenderer`:
14
+ * `GguiRender`'s MCP-Apps branch:
7
15
  *
8
16
  * - `type === 'mcpApps'` → `mountMcpAppIframe` (vanilla iframe
9
17
  * host; the recursive foreign-MCP-App path).
10
18
  * - `componentCode` empty OR absent → `mountProvisional` (A2UI
11
19
  * preview fed by the `_ggui:preview` reserved channel).
12
- * - otherwise → `mountReactRoot` with a per-item `WireConfig`
13
- * built via the renderer's `buildScopedConfig(item)` closure.
14
- * The mounted component is wrapped in
20
+ * - otherwise → `mountReactRoot` with a per-render `WireConfig`
21
+ * built by the caller. The mounted component is wrapped in
15
22
  * `<GguiWireProvider config={scopedConfig}>` through the
16
23
  * `renderWrapper` seam.
17
24
  *
18
- * Per-item lifecycle:
25
+ * Per-render lifecycle:
19
26
  *
20
- * Each stack entry owns a SINGLE `StackItemHandle` for its visible
21
- * lifetime. When the entry gets replaced in-place (push matched by
22
- * id), the dispatcher transitions the existing handle rather than
23
- * unmount + remount — props-only changes skip the React eval; kind
24
- * changes unmount the current and mount the replacement.
27
+ * The handle's `update()` either transitions in-place (props-only,
28
+ * same kind) or tears down + remounts (kind changed — provisional
29
+ * → react when componentCode lands).
25
30
  *
26
31
  * StreamBus integration:
27
32
  *
@@ -40,16 +45,17 @@ import { mountMcpAppIframe, } from './mcp-app-iframe-host.js';
40
45
  // =============================================================================
41
46
  // Kind detection
42
47
  // =============================================================================
43
- function detectKind(stackItem) {
44
- if (stackItem.type === 'mcpApps')
48
+ function detectKind(render) {
49
+ if (render.type === 'mcpApps')
45
50
  return 'mcpApps';
46
- // System items don't route through this stack-item dispatcher — the
47
- // self-contained boot path renders them via `SystemCardHost` directly.
48
- // If one accidentally lands here, fall through to provisional so the
49
- // user at least sees the A2UI placeholder rather than a crash.
50
- if (stackItem.type === 'system')
51
- return 'provisional';
52
- const code = stackItem.componentCode;
51
+ // System renders mount via the built-in `SystemCardHost` registry
52
+ // (the `'system'` branch in `mountByKind`). This is the single mount
53
+ // surface for server-emitted `kind` cards post boot-consolidation —
54
+ // the old self-contained path that hand-rolled `SystemCardHost` is
55
+ // gone, so this MUST route to `'system'`, not the A2UI placeholder.
56
+ if (render.type === 'system')
57
+ return 'system';
58
+ const code = render.componentCode;
53
59
  if (typeof code === 'string' && code.trim().length > 0)
54
60
  return 'react';
55
61
  return 'provisional';
@@ -57,13 +63,18 @@ function detectKind(stackItem) {
57
63
  // =============================================================================
58
64
  // Mount
59
65
  // =============================================================================
60
- export async function renderStackItem(container, opts) {
66
+ export async function mountRender(container, opts) {
61
67
  let currentOpts = opts;
62
68
  let currentKind = 'none';
63
69
  let reactMount = null;
64
70
  let provisionalMount = null;
65
71
  let mcpMount = null;
66
72
  let previewUnsubscribe = null;
73
+ // System-card mount (type:'system'). The render-only `SystemCardHost`
74
+ // tree has no wire/context wrap; we hold the react-dom Root for
75
+ // teardown + a re-render closure for same-kind prop updates.
76
+ let systemRoot = null;
77
+ let systemRerender = null;
67
78
  function teardown() {
68
79
  if (reactMount !== null) {
69
80
  reactMount.unmount();
@@ -77,6 +88,11 @@ export async function renderStackItem(container, opts) {
77
88
  mcpMount.unmount();
78
89
  mcpMount = null;
79
90
  }
91
+ if (systemRoot !== null) {
92
+ systemRoot.unmount();
93
+ systemRoot = null;
94
+ systemRerender = null;
95
+ }
80
96
  if (previewUnsubscribe !== null) {
81
97
  previewUnsubscribe();
82
98
  previewUnsubscribe = null;
@@ -85,11 +101,11 @@ export async function renderStackItem(container, opts) {
85
101
  }
86
102
  async function mountByKind(kind) {
87
103
  if (kind === 'mcpApps') {
88
- // SessionStackEntry narrows to McpAppsStackItem when type='mcpApps'.
89
- const item = currentOpts.stackItem;
104
+ // Render narrows to McpAppsRender when type='mcpApps'.
105
+ const render = currentOpts.render;
90
106
  mcpMount = mountMcpAppIframe(container, {
91
- stackItem: item,
92
- sessionId: currentOpts.sessionId,
107
+ render,
108
+ renderId: currentOpts.renderId,
93
109
  ...(currentOpts.mcpAppsServerBaseUrl !== undefined
94
110
  ? { serverBaseUrl: currentOpts.mcpAppsServerBaseUrl }
95
111
  : {}),
@@ -97,6 +113,39 @@ export async function renderStackItem(container, opts) {
97
113
  currentKind = 'mcpApps';
98
114
  return;
99
115
  }
116
+ if (kind === 'system') {
117
+ // System card — server-emitted `kind` mapped to a built-in
118
+ // `.tsx` via `SystemCardHost`. Render-only: no `__ggui__` shim,
119
+ // no GguiWireProvider, no contextSpec wrap (system cards don't
120
+ // dispatch wire actions or read contexts). The registry handles
121
+ // unknown kinds via a typed fallback. Dynamic-import keeps the
122
+ // system-cards module out of the base graph (boot.test.ts +
123
+ // non-system mounts never pull it).
124
+ const render = currentOpts.render;
125
+ if (render.type !== 'system')
126
+ return;
127
+ const [reactDomClient, systemCardsMod] = await Promise.all([
128
+ import('react-dom/client'),
129
+ import('./system-cards/index.js'),
130
+ ]);
131
+ const root = reactDomClient.createRoot(container);
132
+ const renderCard = (r) => {
133
+ if (r.type !== 'system')
134
+ return;
135
+ root.render(createElement(systemCardsMod.SystemCardHost, {
136
+ kind: r.kind,
137
+ props: r.props ?? {},
138
+ ...(currentOpts.themeId !== undefined
139
+ ? { themeId: currentOpts.themeId }
140
+ : {}),
141
+ }));
142
+ };
143
+ renderCard(render);
144
+ systemRoot = root;
145
+ systemRerender = renderCard;
146
+ currentKind = 'system';
147
+ return;
148
+ }
100
149
  if (kind === 'provisional') {
101
150
  provisionalMount = mountProvisional(container);
102
151
  previewUnsubscribe = currentOpts.streamBus.subscribe('_ggui:preview', (env) => {
@@ -107,21 +156,21 @@ export async function renderStackItem(container, opts) {
107
156
  }
108
157
  // kind === 'react' — detectKind guarantees a generated component
109
158
  // variant (no mcpApps, no system) by this point.
110
- const item = currentOpts.stackItem;
111
- if (item.type === 'mcpApps' || item.type === 'system')
159
+ const render = currentOpts.render;
160
+ if (render.type === 'mcpApps' || render.type === 'system')
112
161
  return;
113
- // GG.8.2 — gadget package names from the item's descriptor sidecar
162
+ // GG.8.2 — gadget package names from the render's descriptor sidecar
114
163
  // so the rewriter resolves each direct gadget import to its
115
164
  // per-package shim. STDLIB `@ggui-ai/gadgets` is always rewritten.
116
- const gadgetPackages = 'gadgetDescriptors' in item && item.gadgetDescriptors !== undefined
117
- ? item.gadgetDescriptors.map((d) => d.package)
118
- : undefined;
165
+ const gadgetPackages = 'gadgetDescriptors' in render && render.gadgetDescriptors !== undefined
166
+ ? render.gadgetDescriptors.map((d) => d.package)
167
+ : currentOpts.gadgetPackages;
119
168
  reactMount = await mountReactRoot(container, {
120
- stackItem: {
121
- ...(item.id !== undefined ? { id: item.id } : {}),
122
- componentCode: item.componentCode ?? '',
123
- ...(item.props !== undefined
124
- ? { props: item.props }
169
+ render: {
170
+ ...(render.id !== undefined ? { id: render.id } : {}),
171
+ componentCode: render.componentCode ?? '',
172
+ ...(render.props !== undefined
173
+ ? { props: render.props }
125
174
  : {}),
126
175
  },
127
176
  ...(gadgetPackages !== undefined ? { gadgetPackages } : {}),
@@ -150,7 +199,7 @@ export async function renderStackItem(container, opts) {
150
199
  * wrapOuter(<GguiWireProvider><userComponent/></GguiWireProvider>)
151
200
  *
152
201
  * `wrapOuter` is supplied by `bootProduction` to install the
153
- * `<ContextStateHost>` provider tree around the per-item React
202
+ * `<ContextStateHost>` provider tree around the per-render React
154
203
  * mount. When absent, the wire provider is the only wrapper.
155
204
  */
156
205
  function wrapInScopedProvider(mountedComponent) {
@@ -162,7 +211,7 @@ export async function renderStackItem(container, opts) {
162
211
  : wireWrapped;
163
212
  }
164
213
  // Initial mount.
165
- const initialKind = detectKind(opts.stackItem);
214
+ const initialKind = detectKind(opts.render);
166
215
  await mountByKind(initialKind);
167
216
  return {
168
217
  get kind() {
@@ -171,7 +220,7 @@ export async function renderStackItem(container, opts) {
171
220
  async update(next) {
172
221
  const prevKind = currentKind;
173
222
  currentOpts = next;
174
- const nextKind = detectKind(next.stackItem);
223
+ const nextKind = detectKind(next.render);
175
224
  if (nextKind !== prevKind) {
176
225
  // Kind changed — tear down old + mount new.
177
226
  teardown();
@@ -180,15 +229,15 @@ export async function renderStackItem(container, opts) {
180
229
  }
181
230
  // Same-kind update — route to the inner renderer's update.
182
231
  if (nextKind === 'react' && reactMount !== null) {
183
- const item = next.stackItem;
184
- if (item.type === 'mcpApps' || item.type === 'system')
232
+ const render = next.render;
233
+ if (render.type === 'mcpApps' || render.type === 'system')
185
234
  return;
186
235
  await reactMount.update({
187
- stackItem: {
188
- ...(item.id !== undefined ? { id: item.id } : {}),
189
- componentCode: item.componentCode ?? '',
190
- ...(item.props !== undefined
191
- ? { props: item.props }
236
+ render: {
237
+ ...(render.id !== undefined ? { id: render.id } : {}),
238
+ componentCode: render.componentCode ?? '',
239
+ ...(render.props !== undefined
240
+ ? { props: render.props }
192
241
  : {}),
193
242
  },
194
243
  ...(next.themeId !== undefined ? { themeId: next.themeId } : {}),
@@ -200,6 +249,17 @@ export async function renderStackItem(container, opts) {
200
249
  : {}),
201
250
  renderWrapper: wrapInScopedProvider,
202
251
  });
252
+ return;
253
+ }
254
+ // Same-kind system update — re-render the card with new props
255
+ // through the retained root (no teardown). System cards are
256
+ // pure functions of (kind, props); a props delta just re-renders.
257
+ if (nextKind === 'system' && systemRerender !== null) {
258
+ const render = next.render;
259
+ if (render.type !== 'system')
260
+ return;
261
+ systemRerender(render);
262
+ return;
203
263
  }
204
264
  // provisional / mcpApps same-kind: no in-place update surface.
205
265
  // Provisional keeps accumulating envelopes; mcpApps iframe
@@ -208,63 +268,3 @@ export async function renderStackItem(container, opts) {
208
268
  unmount: teardown,
209
269
  };
210
270
  }
211
- /**
212
- * Minimal stack-level orchestrator — keyed by stackItem.id, mounts
213
- * new items, updates existing ones, unmounts removed ones.
214
- *
215
- * The production renderer tree uses this; tests can drive the
216
- * individual `renderStackItem` entry point directly.
217
- */
218
- export class StackRenderer {
219
- ctx;
220
- handles = new Map();
221
- constructor(ctx) {
222
- this.ctx = ctx;
223
- }
224
- async applyStack(stack) {
225
- const nextIds = new Set(stack.map((s) => s.id));
226
- // Unmount removed items.
227
- for (const [id, handle] of this.handles) {
228
- if (!nextIds.has(id)) {
229
- handle.unmount();
230
- this.handles.delete(id);
231
- }
232
- }
233
- // Mount or update each item in stack order.
234
- for (const item of stack) {
235
- const container = this.ctx.containerFor(item.id);
236
- const scopedWireConfig = this.ctx.getScopedWireConfig(item);
237
- const wrapOuter = this.ctx.getOuterWrapper?.(item);
238
- const opts = {
239
- stackItem: item,
240
- scopedWireConfig,
241
- streamBus: this.ctx.streamBus,
242
- sessionId: this.ctx.sessionId,
243
- ...(this.ctx.mcpAppsServerBaseUrl !== undefined
244
- ? { mcpAppsServerBaseUrl: this.ctx.mcpAppsServerBaseUrl }
245
- : {}),
246
- ...(this.ctx.themeId !== undefined ? { themeId: this.ctx.themeId } : {}),
247
- ...(this.ctx.themeMode !== undefined ? { themeMode: this.ctx.themeMode } : {}),
248
- ...(this.ctx.onError !== undefined ? { onError: this.ctx.onError } : {}),
249
- ...(this.ctx.onRequestRepair !== undefined
250
- ? { onRequestRepair: this.ctx.onRequestRepair }
251
- : {}),
252
- ...(wrapOuter !== undefined ? { wrapOuter } : {}),
253
- };
254
- const existing = this.handles.get(item.id);
255
- if (existing === undefined) {
256
- const handle = await renderStackItem(container, opts);
257
- this.handles.set(item.id, handle);
258
- }
259
- else {
260
- await existing.update(opts);
261
- }
262
- }
263
- }
264
- unmountAll() {
265
- for (const handle of this.handles.values()) {
266
- handle.unmount();
267
- }
268
- this.handles.clear();
269
- }
270
- }