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

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
package/README.md CHANGED
@@ -4,7 +4,7 @@ The iframe-local runtime for [ggui](https://ggui.ai).
4
4
 
5
5
  A self-contained ESM bundle that boots inside an MCP Apps host iframe,
6
6
  runs the protocol-version handshake, opens the live-channel WebSocket,
7
- and renders the ggui session stack. It bundles React, ReactDOM, and the
7
+ and mounts the rendered UI. It bundles React, ReactDOM, and the
8
8
  ggui design system inside the artifact so generated component code has a
9
9
  single seam to read from.
10
10
 
@@ -16,17 +16,18 @@ exists for host apps, the `<McpAppIframe>` wrapper, and tests.
16
16
 
17
17
  For host-side code that needs to validate or react to runtime state:
18
18
 
19
- - `parseBootstrap` (and `parseBootstrapFrom*` variants) —
20
- parse and validate a session bootstrap before spawning the iframe.
19
+ - `parseMetaFromGlobal` / `parseMetaFromToolResult` —
20
+ parse and validate a render bootstrap before spawning the iframe.
21
21
  - `ProtocolError` and the `from*` constructors — the canonical typed
22
22
  union for every failure the renderer classifies outward.
23
23
  - `ObservabilityEvent` / `postObservabilityToParent` — the renderer →
24
24
  host telemetry seam.
25
25
  - `LifecycleEmitter` / `postLifecycleToParent` — lifecycle events.
26
26
 
27
- Internal runtime types (`RendererWebSocketManager`, `StackModel`, the
28
- boot sequence) are deliberately not exported — they are an internal
29
- implementation contract, not a public API.
27
+ Internal runtime types (`bootSequence`, `connectViaRegistry`, the
28
+ per-channel handler factories, and the single-item render mount) are
29
+ deliberately not exported — they are an internal implementation
30
+ contract, not a public API.
30
31
 
31
32
  ## License
32
33
 
@@ -19,7 +19,7 @@
19
19
  * for any tool the agent has access to, including third-party
20
20
  * MCP servers the local pod can't subscribe-for.
21
21
  *
22
- * The dispatch decision is per-channel + per-render (each new push
22
+ * The dispatch decision is per-channel + per-render (each new render
23
23
  * brings a potentially-new contract; we re-run the routing). The
24
24
  * StreamBus + `useStream(channel)` API stay unchanged — the router
25
25
  * adapts payloads from both transports into the same envelope shape
@@ -36,7 +36,7 @@
36
36
  * on the {@link RendererWebSocketManager}. The router only watches
37
37
  * status transitions; it doesn't own the reconnect schedule.
38
38
  *
39
- * **Idempotence** — re-subscribing the same `(stackItemId, channelName)`
39
+ * **Idempotence** — re-subscribing the same `(renderId, channelName)`
40
40
  * pair is server-side idempotent (the server's `ChannelSubscriptionState`
41
41
  * replaces in place). Clients can re-send on reconnect without bookkeeping
42
42
  * a "did we already subscribe?" gate.
@@ -83,9 +83,9 @@ export type ToolsCallInvoker = (args: {
83
83
  * Options for {@link createChannelTransportRouter}.
84
84
  */
85
85
  export interface ChannelTransportRouterOptions {
86
- /** Session id the router scopes channel_subscribe frames against. */
87
- readonly sessionId: string;
88
- /** App (tenant) id paired with sessionId on the subscribe frame. */
86
+ /** Render id the router scopes channel_subscribe frames against. */
87
+ readonly renderId: string;
88
+ /** App (tenant) id paired with renderId on the subscribe frame. */
89
89
  readonly appId: string;
90
90
  /**
91
91
  * Allowlist of `source.tool` names this server can subscribe-for
@@ -121,17 +121,17 @@ export interface ChannelTransportRouterOptions {
121
121
  */
122
122
  export type ChannelTransportEvent = {
123
123
  readonly kind: 'channel-transport-picked';
124
- readonly stackItemId: string;
124
+ readonly renderId: string;
125
125
  readonly channelName: string;
126
126
  readonly transport: 'ws' | 'poll';
127
127
  } | {
128
128
  readonly kind: 'channel-transport-fallback';
129
- readonly stackItemId: string;
129
+ readonly renderId: string;
130
130
  readonly channelName: string;
131
131
  readonly reason: 'ws-disconnect' | 'channel-not-local';
132
132
  } | {
133
133
  readonly kind: 'channel-transport-resubscribed';
134
- readonly stackItemId: string;
134
+ readonly renderId: string;
135
135
  readonly channelName: string;
136
136
  };
137
137
  /**
@@ -139,16 +139,16 @@ export type ChannelTransportEvent = {
139
139
  */
140
140
  export interface ChannelTransportRouter {
141
141
  /**
142
- * Apply a new stack item's `streamSpec`. Idempotent against
143
- * re-applying the same shape (no churn). Channels added/removed
144
- * across renders fire transport pick / teardown accordingly.
142
+ * Apply a new render's `streamSpec`. Idempotent against re-applying
143
+ * the same shape (no churn). Channels added/removed across renders
144
+ * fire transport pick / teardown accordingly.
145
145
  *
146
146
  * The legacy `data` frame path on `streamSpec[ch]` entries
147
147
  * WITHOUT `source.tool` is unaffected — the router only manages
148
148
  * the source-fed subset.
149
149
  */
150
- readonly applyStackItem: (item: {
151
- readonly stackItemId: string;
150
+ readonly applyRender: (render: {
151
+ readonly renderId: string;
152
152
  readonly streamSpec?: StreamSpec;
153
153
  }) => void;
154
154
  /**
@@ -168,14 +168,14 @@ export interface ChannelTransportRouter {
168
168
  readonly onWsStatusChange: (status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting') => void;
169
169
  /**
170
170
  * Tear down every active subscription + timer. Called on
171
- * `triadWiring.teardown`.
171
+ * `renderer.teardown`.
172
172
  */
173
173
  readonly dispose: () => void;
174
174
  }
175
175
  /**
176
176
  * Factory.
177
177
  *
178
- * Per-channel transport routing — owns the `(stackItemId, channelName)`
178
+ * Per-channel transport routing — owns the `(renderId, channelName)`
179
179
  * registry, the WS-vs-poll decision per channel, and the cross-state
180
180
  * transitions (disconnect → poll fallback, reconnect → re-subscribe).
181
181
  */
@@ -10,13 +10,13 @@ export const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10_000;
10
10
  /**
11
11
  * Factory.
12
12
  *
13
- * Per-channel transport routing — owns the `(stackItemId, channelName)`
13
+ * Per-channel transport routing — owns the `(renderId, channelName)`
14
14
  * registry, the WS-vs-poll decision per channel, and the cross-state
15
15
  * transitions (disconnect → poll fallback, reconnect → re-subscribe).
16
16
  */
17
17
  export function createChannelTransportRouter(opts) {
18
18
  /**
19
- * Registry keyed by `${stackItemId}:${channelName}`. The composite
19
+ * Registry keyed by `${renderId}:${channelName}`. The composite
20
20
  * key matches the server-side `channelSubs` map shape exactly, so
21
21
  * test snapshots line up.
22
22
  */
@@ -27,15 +27,15 @@ export function createChannelTransportRouter(opts) {
27
27
  * WS lifecycle flag. We start in `'connected'` because the router
28
28
  * is created AFTER the subscribe ack — the runtime's bootSequence
29
29
  * resolves `subscribeFn(...)` before threading the manager into
30
- * triadWiring's `attachManager` (where the router is constructed).
30
+ * renderer's `attachManager` (where the router is constructed).
31
31
  * Status transitions update this; the router uses it to decide
32
32
  * whether to start WS subscribes or skip straight to polling.
33
33
  */
34
34
  let wsStatus = 'connected';
35
35
  let disposed = false;
36
36
  const observe = opts.onObserve ?? (() => { });
37
- function keyOf(stackItemId, channelName) {
38
- return `${stackItemId}:${channelName}`;
37
+ function keyOf(renderId, channelName) {
38
+ return `${renderId}:${channelName}`;
39
39
  }
40
40
  function stopPolling(state) {
41
41
  if (state.pollTimer !== null) {
@@ -45,7 +45,7 @@ export function createChannelTransportRouter(opts) {
45
45
  }
46
46
  function emitToBus(state, payload, complete) {
47
47
  const envelope = {
48
- sessionId: opts.sessionId,
48
+ renderId: state.renderId,
49
49
  channel: state.channelName,
50
50
  mode: state.mode,
51
51
  payload,
@@ -90,16 +90,15 @@ export function createChannelTransportRouter(opts) {
90
90
  }
91
91
  /**
92
92
  * Send a `channel_subscribe` WS frame. Server-side bookkeeping is
93
- * idempotent on `(sessionId, stackItemId, channelName)`, so re-sends
94
- * on reconnect are safe.
93
+ * idempotent on `(renderId, channelName)`, so re-sends on reconnect
94
+ * are safe.
95
95
  */
96
96
  function sendSubscribe(state) {
97
97
  opts.send({
98
98
  type: 'channel_subscribe',
99
99
  payload: {
100
- sessionId: opts.sessionId,
100
+ renderId: state.renderId,
101
101
  appId: opts.appId,
102
- stackItemId: state.stackItemId,
103
102
  channelName: state.channelName,
104
103
  ...(state.args !== undefined ? { args: { ...state.args } } : {}),
105
104
  },
@@ -115,9 +114,8 @@ export function createChannelTransportRouter(opts) {
115
114
  opts.send({
116
115
  type: 'channel_unsubscribe',
117
116
  payload: {
118
- sessionId: opts.sessionId,
117
+ renderId: state.renderId,
119
118
  appId: opts.appId,
120
- stackItemId: state.stackItemId,
121
119
  channelName: state.channelName,
122
120
  },
123
121
  });
@@ -139,7 +137,7 @@ export function createChannelTransportRouter(opts) {
139
137
  sendSubscribe(state);
140
138
  observe({
141
139
  kind: 'channel-transport-picked',
142
- stackItemId: state.stackItemId,
140
+ renderId: state.renderId,
143
141
  channelName: state.channelName,
144
142
  transport: 'ws',
145
143
  });
@@ -150,7 +148,7 @@ export function createChannelTransportRouter(opts) {
150
148
  startPolling(state);
151
149
  observe({
152
150
  kind: 'channel-transport-picked',
153
- stackItemId: state.stackItemId,
151
+ renderId: state.renderId,
154
152
  channelName: state.channelName,
155
153
  transport: 'poll',
156
154
  });
@@ -159,17 +157,17 @@ export function createChannelTransportRouter(opts) {
159
157
  startPolling(state);
160
158
  observe({
161
159
  kind: 'channel-transport-picked',
162
- stackItemId: state.stackItemId,
160
+ renderId: state.renderId,
163
161
  channelName: state.channelName,
164
162
  transport: 'poll',
165
163
  });
166
164
  }
167
165
  return {
168
- applyStackItem: (item) => {
166
+ applyRender: (render) => {
169
167
  if (disposed)
170
168
  return;
171
169
  const seenKeys = new Set();
172
- const spec = item.streamSpec ?? {};
170
+ const spec = render.streamSpec ?? {};
173
171
  for (const [channelName, entry] of Object.entries(spec)) {
174
172
  const source = entry.source;
175
173
  // Channels without a `source.tool` declaration stay on the
@@ -186,18 +184,18 @@ export function createChannelTransportRouter(opts) {
186
184
  const channelArgs = source.args !== undefined && source.args !== null
187
185
  ? source.args
188
186
  : undefined;
189
- const k = keyOf(item.stackItemId, channelName);
187
+ const k = keyOf(render.renderId, channelName);
190
188
  seenKeys.add(k);
191
189
  const existing = channels.get(k);
192
190
  if (existing !== undefined) {
193
- // Same (stackItemId, channelName) — leave the transport
191
+ // Same (renderId, channelName) — leave the transport
194
192
  // bookkeeping alone. Spec changes that flip preferWs
195
- // mid-session are out of 1c scope (would require
193
+ // mid-render are out of 1c scope (would require
196
194
  // re-handshake).
197
195
  continue;
198
196
  }
199
197
  const state = {
200
- stackItemId: item.stackItemId,
198
+ renderId: render.renderId,
201
199
  channelName,
202
200
  toolName,
203
201
  ...(channelArgs !== undefined ? { args: channelArgs } : {}),
@@ -210,13 +208,12 @@ export function createChannelTransportRouter(opts) {
210
208
  channels.set(k, state);
211
209
  activate(state);
212
210
  }
213
- // Tear down any channel for THIS stack item that's no longer in
214
- // the spec. Channels for OTHER stack items stay — they belong
215
- // to other items that haven't been re-applied yet. Stack-pop /
216
- // close-session paths call `dispose()` for the wholesale
217
- // teardown.
211
+ // Tear down any channel for THIS render that's no longer in the
212
+ // spec. Channels for OTHER renders stay — they belong to other
213
+ // mounts that haven't been re-applied yet. Close-render paths
214
+ // call `dispose()` for the wholesale teardown.
218
215
  for (const [k, state] of channels) {
219
- if (state.stackItemId === item.stackItemId &&
216
+ if (state.renderId === render.renderId &&
220
217
  !seenKeys.has(k)) {
221
218
  stopPolling(state);
222
219
  sendUnsubscribe(state);
@@ -229,7 +226,7 @@ export function createChannelTransportRouter(opts) {
229
226
  return false;
230
227
  if (msg.type === 'channel_payload') {
231
228
  const p = msg.payload;
232
- const k = keyOf(p.stackItemId, p.channelName);
229
+ const k = keyOf(p.renderId, p.channelName);
233
230
  const state = channels.get(k);
234
231
  if (state === undefined)
235
232
  return false;
@@ -247,9 +244,9 @@ export function createChannelTransportRouter(opts) {
247
244
  if (msg.type === 'channel_error') {
248
245
  const p = msg.payload;
249
246
  // Locate by `channelName` only — the error payload doesn't
250
- // carry `stackItemId`. We match by channel-name across active
247
+ // carry `renderId`. We match by channel-name across active
251
248
  // items and apply the permanent-fallback flag to the matching
252
- // entries. Same channel name across stack items is rare; the
249
+ // entries. Same channel name across renders is rare; the
253
250
  // policy is conservative (fall back ALL matches rather than
254
251
  // miss one).
255
252
  for (const state of channels.values()) {
@@ -257,12 +254,12 @@ export function createChannelTransportRouter(opts) {
257
254
  continue;
258
255
  if (p.code === 'CHANNEL_NOT_LOCAL' ||
259
256
  p.code === 'CHANNEL_UNKNOWN' ||
260
- p.code === 'STACK_ITEM_NOT_FOUND') {
257
+ p.code === 'RENDER_NOT_FOUND') {
261
258
  state.permanentPollFallback = true;
262
259
  startPolling(state);
263
260
  observe({
264
261
  kind: 'channel-transport-fallback',
265
- stackItemId: state.stackItemId,
262
+ renderId: state.renderId,
266
263
  channelName: state.channelName,
267
264
  reason: 'channel-not-local',
268
265
  });
@@ -301,7 +298,7 @@ export function createChannelTransportRouter(opts) {
301
298
  startPolling(state);
302
299
  observe({
303
300
  kind: 'channel-transport-fallback',
304
- stackItemId: state.stackItemId,
301
+ renderId: state.renderId,
305
302
  channelName: state.channelName,
306
303
  reason: 'ws-disconnect',
307
304
  });
@@ -313,7 +310,7 @@ export function createChannelTransportRouter(opts) {
313
310
  (prevStatus === 'disconnected' || prevStatus === 'reconnecting')) {
314
311
  // Reconnected. Re-send `channel_subscribe` for every
315
312
  // WS-preferring channel. Server is idempotent on the (session,
316
- // stackItem, channel) triple, so duplicates are safe. We
313
+ // render, channel) tuple, so duplicates are safe. We
317
314
  // intentionally LEAVE the polling fallback running until the
318
315
  // first `channel_payload` lands — bridges the gap where the
319
316
  // server's first post-reconnect poll cycle hasn't fired yet.
@@ -325,7 +322,7 @@ export function createChannelTransportRouter(opts) {
325
322
  sendSubscribe(state);
326
323
  observe({
327
324
  kind: 'channel-transport-resubscribed',
328
- stackItemId: state.stackItemId,
325
+ renderId: state.renderId,
329
326
  channelName: state.channelName,
330
327
  });
331
328
  }
@@ -7,10 +7,10 @@
7
7
  * passthrough — the router parses the frame contents against its
8
8
  * subscription map and dispatches to the matching consumer.
9
9
  *
10
- * Factored out of `handleTriadMessage` as part of the B3a handler
10
+ * Factored out of `handleRendererMessage` as part of the B3a handler
11
11
  * extraction.
12
12
  */
13
- import type { ChannelHandler } from '@ggui-ai/channel-client';
13
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
14
14
  import type { ChannelErrorPayload, ChannelPayloadFrame } from '@ggui-ai/protocol';
15
15
  import type { ChannelTransportRouter } from '../channel-transport.js';
16
16
  export interface ChannelRouterHandlerDeps {
@@ -7,7 +7,7 @@
7
7
  * passthrough — the router parses the frame contents against its
8
8
  * subscription map and dispatches to the matching consumer.
9
9
  *
10
- * Factored out of `handleTriadMessage` as part of the B3a handler
10
+ * Factored out of `handleRendererMessage` as part of the B3a handler
11
11
  * extraction.
12
12
  */
13
13
  export function createChannelPayloadHandler(deps) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `data` channel handler — validates inbound stream-envelope payloads
3
- * against the active stack item's `streamSpec`, then fans the validated
3
+ * against the active render's `streamSpec`, then fans the validated
4
4
  * envelope out via `streamBus`. Invalid payloads drop silently
5
5
  * (matching `GguiSession.handleServerMessage`'s surfacing policy —
6
6
  * the violation is already logged by the validator).
@@ -11,18 +11,24 @@
11
11
  * in `runtime.ts` as part of the B3b cleanup — the handler is now the
12
12
  * sole dispatch surface for `data` frames.
13
13
  *
14
- * Factored out of `handleTriadMessage` as part of the B3a handler
15
- * extraction; expanded in B3b to absorb the observability-axis
16
- * emission previously fired in parallel from `bootSequence`.
14
+ * Post-stack-removal (2026-05-27): the active render is read through
15
+ * the caller-supplied `getCurrentRender` thunk instead of via a
16
+ * `StackModel.snapshot()` walk — the iframe holds exactly one mounted
17
+ * render, so the lookup is direct.
17
18
  */
18
- import type { ChannelHandler } from '@ggui-ai/channel-client';
19
- import { type CanvasLifecyclePayload, type StreamEnvelope } from '@ggui-ai/protocol';
19
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
20
+ import { type Render, type StreamEnvelope } from '@ggui-ai/protocol';
21
+ import type { RenderSeedInput } from '../types.js';
20
22
  import type { ObservabilityEmitter } from '../observability.js';
21
- import type { StackModel } from '../stack.js';
22
23
  import { type RendererValidatorContext } from '../validation.js';
23
24
  import type { StreamBus } from '../wire-config.js';
24
25
  export interface DataHandlerDeps {
25
- readonly stackModel: StackModel;
26
+ /**
27
+ * Read the currently-mounted render. Returns `null` when no render
28
+ * has been mounted yet — data frames received pre-mount have no
29
+ * streamSpec to validate against and silently drop.
30
+ */
31
+ readonly getCurrentRender: () => Render | RenderSeedInput | null;
26
32
  readonly streamBus: StreamBus;
27
33
  readonly validatorCtx: RendererValidatorContext;
28
34
  /**
@@ -33,16 +39,5 @@ export interface DataHandlerDeps {
33
39
  * bound).
34
40
  */
35
41
  readonly onObserve?: ObservabilityEmitter;
36
- /**
37
- * Optional canvas-mode lifecycle sink. When present, every envelope
38
- * on the reserved `_ggui:lifecycle` channel fires this callback
39
- * with the parsed payload. Production wires it to mount.tsx's
40
- * AnimatorEventBus so the animator pill state machine advances.
41
- * Non-canvas iframes leave it absent and the envelopes drop
42
- * silently — `validateInboundStreamPayload` rejects lifecycle
43
- * payloads when no reserved validator is registered for them
44
- * client-side, so they never reach `streamBus.emit` either.
45
- */
46
- readonly onCanvasLifecycle?: (payload: CanvasLifecyclePayload) => void;
47
42
  }
48
43
  export declare function createDataHandler(deps: DataHandlerDeps): ChannelHandler<StreamEnvelope>;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `data` channel handler — validates inbound stream-envelope payloads
3
- * against the active stack item's `streamSpec`, then fans the validated
3
+ * against the active render's `streamSpec`, then fans the validated
4
4
  * envelope out via `streamBus`. Invalid payloads drop silently
5
5
  * (matching `GguiSession.handleServerMessage`'s surfacing policy —
6
6
  * the violation is already logged by the validator).
@@ -11,11 +11,12 @@
11
11
  * in `runtime.ts` as part of the B3b cleanup — the handler is now the
12
12
  * sole dispatch surface for `data` frames.
13
13
  *
14
- * Factored out of `handleTriadMessage` as part of the B3a handler
15
- * extraction; expanded in B3b to absorb the observability-axis
16
- * emission previously fired in parallel from `bootSequence`.
14
+ * Post-stack-removal (2026-05-27): the active render is read through
15
+ * the caller-supplied `getCurrentRender` thunk instead of via a
16
+ * `StackModel.snapshot()` walk — the iframe holds exactly one mounted
17
+ * render, so the lookup is direct.
17
18
  */
18
- import { CONTRACT_ERROR_CHANNEL, LIFECYCLE_CHANNEL, } from '@ggui-ai/protocol';
19
+ import { CONTRACT_ERROR_CHANNEL, } from '@ggui-ai/protocol';
19
20
  import { validateInboundStreamPayload, } from '../validation.js';
20
21
  export function createDataHandler(deps) {
21
22
  return {
@@ -36,32 +37,13 @@ export function createDataHandler(deps) {
36
37
  deps.onObserve !== undefined) {
37
38
  emitContractErrorFromDataFrame(envelope, deps.onObserve);
38
39
  }
39
- // Canvas-mode lifecycle
40
- // envelopes — fires BEFORE validation for the same reason as
41
- // the contract-error path: the wire shape is server-owned, and
42
- // any reachable envelope is a server-emitted signal. The
43
- // payload's `kind` discriminator narrows downstream; the
44
- // animator host treats unknown kinds as no-ops. Invalid shapes
45
- // silently drop here (typeof check) — defensive against future
46
- // wire shape drift.
47
- if (envelope.channel === LIFECYCLE_CHANNEL &&
48
- deps.onCanvasLifecycle !== undefined) {
49
- const payload = envelope.payload;
50
- if (payload !== null &&
51
- typeof payload === 'object' &&
52
- !Array.isArray(payload) &&
53
- typeof payload.kind === 'string') {
54
- deps.onCanvasLifecycle(payload);
55
- }
56
- }
57
- // Active stack item carries the streamSpec — mirrors
58
- // `GguiSession.handleServerMessage`. Top of stack only.
59
- const snapshot = deps.stackModel.snapshot();
60
- const activeItem = snapshot[snapshot.length - 1];
61
- const streamSpec = activeItem !== undefined &&
62
- activeItem.type !== 'mcpApps' &&
63
- activeItem.type !== 'system'
64
- ? activeItem.streamSpec
40
+ // Active render carries the streamSpec — mirrors
41
+ // `GguiSession.handleServerMessage`.
42
+ const activeRender = deps.getCurrentRender();
43
+ const streamSpec = activeRender !== null &&
44
+ activeRender.type !== 'mcpApps' &&
45
+ activeRender.type !== 'system'
46
+ ? activeRender.streamSpec
65
47
  : undefined;
66
48
  const result = validateInboundStreamPayload(streamSpec, envelope.channel, envelope.payload, deps.validatorCtx);
67
49
  if (!result.valid)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * `drain_ack` channel handler — factored out of `runtime.ts` into
3
- * the `@ggui-ai/channel-client` layer. The `onMessage` body fans the
3
+ * the `@ggui-ai/live-channel` layer. The `onMessage` body fans the
4
4
  * payload out to the module-scoped `subscribeDrainAck` listener
5
5
  * registry so per-action state machines stay decoupled from how the
6
6
  * frame arrives on the wire.
7
7
  */
8
- import type { ChannelHandler } from '@ggui-ai/channel-client';
8
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
9
9
  import type { DrainAckPayload } from '@ggui-ai/protocol';
10
10
  export interface DrainAckHandlerDeps {
11
11
  readonly dispatch: (payload: DrainAckPayload) => void;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `drain_ack` channel handler — factored out of `runtime.ts` into
3
- * the `@ggui-ai/channel-client` layer. The `onMessage` body fans the
3
+ * the `@ggui-ai/live-channel` layer. The `onMessage` body fans the
4
4
  * payload out to the module-scoped `subscribeDrainAck` listener
5
5
  * registry so per-action state machines stay decoupled from how the
6
6
  * frame arrives on the wire.
@@ -9,6 +9,6 @@
9
9
  * "no handler for type 'feedback'" warnings — the silence is
10
10
  * intentional, the handler documents that.
11
11
  */
12
- import type { ChannelHandler } from '@ggui-ai/channel-client';
12
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
13
13
  import type { JsonObject } from '@ggui-ai/protocol';
14
14
  export declare function createFeedbackHandler(): ChannelHandler<JsonObject>;
@@ -1,6 +1,6 @@
1
1
  export { createPropsUpdateHandler, type PropsUpdateHandlerDeps, } from './props-update.js';
2
2
  export { createDrainAckHandler, type DrainAckHandlerDeps, } from './drain-ack.js';
3
- export { createPushHandler, type PushHandlerDeps } from './push.js';
3
+ export { createRenderHandler, type RenderHandlerDeps } from './render-frame.js';
4
4
  export { createDataHandler, type DataHandlerDeps } from './data.js';
5
5
  export { createFeedbackHandler } from './feedback.js';
6
6
  export { createChannelPayloadHandler, createChannelErrorHandler, type ChannelRouterHandlerDeps, } from './channel-payload.js';
@@ -1,6 +1,6 @@
1
1
  export { createPropsUpdateHandler, } from './props-update.js';
2
2
  export { createDrainAckHandler, } from './drain-ack.js';
3
- export { createPushHandler } from './push.js';
3
+ export { createRenderHandler } from './render-frame.js';
4
4
  export { createDataHandler } from './data.js';
5
5
  export { createFeedbackHandler } from './feedback.js';
6
6
  export { createChannelPayloadHandler, createChannelErrorHandler, } from './channel-payload.js';
@@ -1,53 +1,49 @@
1
1
  /**
2
- * `props_update` channel handler — factored out of `runtime.ts` into
3
- * the `@ggui-ai/channel-client` layer.
2
+ * `props_update` channel handler — patches the currently-mounted
3
+ * render's props in place via the renderer's `applyRender`
4
+ * callback.
4
5
  *
5
- * Receives a `{stackItemId, props}` payload from the live channel, validates
6
- * the new props against the stack item's cached `propsSpec`, patches
7
- * the in-model entry, and re-applies the renderer so React picks up
8
- * the change.
6
+ * Post-stack-removal (2026-05-27): each iframe holds exactly one
7
+ * mounted render. The handler reads the current render via
8
+ * `getCurrentRender()`, validates the inbound props against its
9
+ * cached `propsSpec`, and re-applies the patched render through
10
+ * `applyRender` — the same callback the render handler uses, so the
11
+ * React update surface is unified.
9
12
  *
10
13
  * Skips when:
11
- * - `stackItemId` is empty or not a string.
14
+ * - `renderId` is empty or not a string.
12
15
  * - `props` is null / not an object (defensive — server can't emit
13
16
  * this shape, but the dispatcher routes the frame on type alone).
14
- * - No matching stack item exists (server raced ahead of our pop).
15
- * - The matched item is `mcpApps` / `system` (no `propsSpec`; server
16
- * should never emit `props_update` for these).
17
+ * - No render is currently mounted (`getCurrentRender` returns null).
18
+ * - The current render's id doesn't match `payload.renderId` —
19
+ * the server may have raced ahead of an in-flight render swap.
20
+ * - The current render is `mcpApps` / `system` (no `propsSpec`;
21
+ * server should never emit `props_update` for these).
17
22
  * - The new props fail validation against the cached spec.
18
23
  *
19
- * An OPTIONAL polling descriptor provides a fallback transport —
20
- * when `polling` is supplied at factory time, `PollingTransport`
21
- * polls `/api/bootstrap/<shortCode>` and synthesizes a frame
22
- * whenever `propsJson` diffs from the last seen hash. WS hosts are
23
- * unaffected (the `polling` descriptor is inert under `WSTransport`).
24
+ * R6 (2026-05-26) retired the per-handler polling descriptor. Polling
25
+ * is now registry-level — the iframe-runtime composes the
26
+ * `/api/renders/:id/state?wsToken=<token>` URL and a snapshot-parsing
27
+ * function once at bind time (see `runtime.ts`); a single fetch per
28
+ * tick projects the slice envelope into per-handler frames the
29
+ * `PollingTransport` dispatches.
24
30
  */
25
- import type { ChannelHandler } from '@ggui-ai/channel-client';
26
- import type { PropsUpdatePayload } from '@ggui-ai/protocol';
27
- import type { StackRenderer } from '../stack-item-renderer.js';
28
- import type { StackModel } from '../stack.js';
31
+ import type { ChannelHandler } from '@ggui-ai/live-channel';
32
+ import type { PropsUpdatePayload, Render } from '@ggui-ai/protocol';
33
+ import type { RenderSeedInput } from '../types.js';
29
34
  export interface PropsUpdateHandlerDeps {
30
- readonly stackModel: StackModel;
31
- readonly getStackRenderer: () => StackRenderer;
32
35
  /**
33
- * Optional polling fallback URL. When the registry's chosen transport
34
- * is `PollingTransport` (e.g. bootstrap missing wsUrl, or
35
- * `FailoverHandle` swapped WS → polling after a hard failure), this
36
- * URL is fetched on each tick. The response body is expected to carry
37
- * `propsJson` + `stackItemId` (the same shape `/api/bootstrap/<code>`
38
- * returns); the handler diffs `propsJson` between ticks and synthesizes
39
- * a `props_update` frame whenever it changes. WS hosts ignore this
40
- * descriptor (polling is inert under `WSTransport`).
41
- *
42
- * Producer: server stamps `bootstrap.pollingUrl` on the bootstrap
43
- * envelope. Consumer: iframe-runtime threads it here at handler
44
- * registration time. Absent → no polling fallback (WS-only mode).
36
+ * Read the currently-mounted render. Returns `null` when no
37
+ * render has been mounted yet (the first render frame hasn't landed)
38
+ * or after teardown. The handler short-circuits on `null` —
39
+ * `props_update` before the first render frame has no React tree to
40
+ * patch.
45
41
  */
46
- readonly pollingUrl?: string;
42
+ readonly getCurrentRender: () => Render | RenderSeedInput | null;
47
43
  /**
48
- * Polling cadence in milliseconds. Defaults to 2000ms per the B5
49
- * plan (well inside the 10s drain-claim budget; 5 ticks of safety).
44
+ * Re-apply the patched render to the single mount slot. Shared with
45
+ * the render-frame handler so React updates flow through one path.
50
46
  */
51
- readonly pollingIntervalMs?: number;
47
+ readonly applyRender: (render: Render | RenderSeedInput) => Promise<void>;
52
48
  }
53
49
  export declare function createPropsUpdateHandler(deps: PropsUpdateHandlerDeps): ChannelHandler<PropsUpdatePayload>;