@ggui-ai/iframe-runtime 0.1.0-rc.3 → 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,42 +1,58 @@
1
1
  /**
2
- * Host-context emitter.
2
+ * Host-context emitter + DOM application.
3
3
  *
4
4
  * Responsibilities:
5
5
  *
6
6
  * 1. Hold the latest `HostContextProjection` the runtime has captured.
7
- * Seeded from the iframe's `ui/initialize` response (Reading-A
8
- * path, via `parseBootstrapFromUiInitialize`); updated on every
7
+ * Seeded from the App's `ui/initialize` capture
8
+ * (`app.getHostContext()`); updated on every
9
9
  * `ui/notifications/host-context-changed` postMessage from the host.
10
10
  * 2. Echo each fresh value as a `type: 'host_context_observed'`
11
11
  * WebSocket envelope so the server can persist it on
12
- * `SessionRecord.hostContext` for agent visibility.
12
+ * `RenderRecord.hostContext` for agent visibility.
13
13
  * 3. Suppress no-op re-emissions (via deep-equality on the projection
14
14
  * shape) — avoids server-side write traffic when a notification
15
15
  * arrives but no projection-visible field actually changed (e.g.,
16
16
  * the host's `theme` updated but ggui doesn't project theme).
17
+ * 4. Apply host theme + styles + fonts to the iframe DOM via the
18
+ * spec-canonical {@link applyDocumentTheme} /
19
+ * {@link applyHostStyleVariables} / {@link applyHostFonts} helpers
20
+ * from `@modelcontextprotocol/ext-apps`. Fires on EVERY raw
21
+ * `McpUiHostContext` the runtime observes (initial + change
22
+ * notifications). The application path is INDEPENDENT of the
23
+ * WS-echo path — projection drops `theme` / `styles` (they live in
24
+ * ggui's own theming pipeline), but the DOM still needs them to
25
+ * render the host's native primitives consistently.
17
26
  *
18
27
  * Lifecycle:
19
28
  *
20
- * - Initial seed: `seed(projection, sessionId)` called from
29
+ * - Initial seed: `seed(projection, renderId)` called from
21
30
  * `bootProduction` right after the WS transport is attached and we
22
- * know the initial projection from `parseBootstrap.hostContext`.
31
+ * know the initial projection from `projectHostContext(app.getHostContext())`.
32
+ * {@link applyHostContextStyling} fires separately from
33
+ * `bootSequence` against the RAW `app.getHostContext()` so the
34
+ * spec-canonical helpers see the full `McpUiHostContext`
35
+ * (projection drops fields they need).
23
36
  * - Live updates: `attachListener()` installs a `window`-scoped
24
37
  * `message` listener for the spec-defined notification method
25
38
  * `'ui/notifications/host-context-changed'`. The notification's
26
- * `params` carry a partial `McpUiHostContext`; we merge into the
27
- * held projection and emit if changed.
39
+ * `params` carry a partial `McpUiHostContext`; we apply the raw
40
+ * fields to the DOM, merge the projection into the held value,
41
+ * and WS-echo if the projection changed.
28
42
  * - Teardown: `detach()` removes the listener and clears state.
29
43
  * Used by tests + future host-switch flows.
30
44
  *
31
45
  * Why a dedicated module:
32
46
  *
33
- * - `runtime.ts` is already ~3100 lines; adding another postMessage
47
+ * - `runtime.ts` is already ~3500 lines; adding another postMessage
34
48
  * listener + state slot bloats it.
35
- * - The emission policy (seed + change-merge + dedupe) is testable in
36
- * isolation without the WS transport / bootstrap parse / DOM mount.
49
+ * - The emission policy (seed + change-merge + dedupe) + DOM-apply
50
+ * policy is testable in isolation without the WS transport /
51
+ * slice-meta parse / DOM mount.
37
52
  * - Future widening (echo more fields, debounce burst changes, etc.)
38
53
  * happens in one place.
39
54
  */
55
+ import { applyDocumentTheme, applyHostFonts, applyHostStyleVariables, } from '@modelcontextprotocol/ext-apps';
40
56
  import { hostContextProjectionsEqual, projectHostContext, } from '@ggui-ai/protocol';
41
57
  let state = null;
42
58
  // =============================================================================
@@ -48,15 +64,15 @@ let state = null;
48
64
  * the same projection) suppresses the duplicate emission via the
49
65
  * equality check.
50
66
  *
51
- * Re-seeding with a DIFFERENT `sessionId` is undefined behavior; the
52
- * iframe-runtime is one-session-per-mount by construction. Tests
53
- * exercising multi-session flows MUST call `detach()` between seeds.
67
+ * Re-seeding with a DIFFERENT `renderId` is undefined behavior; the
68
+ * iframe-runtime is one-render-per-mount by construction. Tests
69
+ * exercising multi-render flows MUST call `detach()` between seeds.
54
70
  */
55
71
  export function seed(args) {
56
72
  // First seed — record state + emit initial.
57
73
  if (state === null) {
58
74
  state = {
59
- sessionId: args.sessionId,
75
+ renderId: args.renderId,
60
76
  send: args.send,
61
77
  current: args.initial,
62
78
  listener: null,
@@ -72,13 +88,17 @@ export function seed(args) {
72
88
  }
73
89
  }
74
90
  /**
75
- * Install the `host-context-changed` notification listener on the
76
- * iframe `window`. Safe to call multiple times — duplicate listeners
77
- * are deduped via `state.listener !== null` guard.
91
+ * Install the `host-context-changed` notification listener. When the
92
+ * {@link AttachListenerOptions.app} handle is supplied, uses App's
93
+ * spec-canonical event surface; otherwise falls back to a raw
94
+ * `window.message` listener (tests use this).
95
+ *
96
+ * Safe to call multiple times — duplicate listeners are deduped via
97
+ * `state.listener !== null` guard.
78
98
  *
79
99
  * Detaches when `detach()` is called or when the module is torn down.
80
100
  */
81
- export function attachListener(targetWindow = window) {
101
+ export function attachListener(optsOrWindow = window) {
82
102
  if (state === null) {
83
103
  // Not yet seeded — listener can't usefully fire (no send seam).
84
104
  // Caller should `seed()` first; this defensive bail keeps the
@@ -87,19 +107,48 @@ export function attachListener(targetWindow = window) {
87
107
  }
88
108
  if (state.listener !== null)
89
109
  return;
110
+ // Back-compat: callers that pass a bare `Window` (the pre-1.19b.3
111
+ // signature, still used by tests) take the raw-listener path.
112
+ const isWindowArg = optsOrWindow !== null
113
+ && typeof optsOrWindow === 'object'
114
+ && 'addEventListener' in optsOrWindow
115
+ && 'removeEventListener' in optsOrWindow
116
+ && !('app' in optsOrWindow);
117
+ const opts = isWindowArg
118
+ ? { targetWindow: optsOrWindow }
119
+ : optsOrWindow;
120
+ if (opts.app !== undefined) {
121
+ const handler = (params) => {
122
+ handleHostContextChangedParams(params);
123
+ };
124
+ opts.app.addEventListener('hostcontextchanged', handler);
125
+ state.listener = {
126
+ kind: 'app',
127
+ off: () => opts.app.removeEventListener('hostcontextchanged', handler),
128
+ };
129
+ return;
130
+ }
131
+ const targetWindow = opts.targetWindow ?? window;
90
132
  const listener = (ev) => {
91
133
  handleHostContextChangedMessage(ev.data);
92
134
  };
93
- state.listener = listener;
94
135
  targetWindow.addEventListener('message', listener);
136
+ state.listener = {
137
+ kind: 'window',
138
+ off: () => targetWindow.removeEventListener('message', listener),
139
+ };
95
140
  }
96
141
  /**
97
142
  * Tear down: remove the listener, clear module state. Called from
98
143
  * unit tests between scenarios and from the future host-switch flow.
144
+ *
145
+ * The `targetWindow` parameter is no longer used (the listener handle
146
+ * carries its own cleanup closure); preserved as an optional arg for
147
+ * back-compat with existing test code.
99
148
  */
100
- export function detach(targetWindow = window) {
149
+ export function detach(_targetWindow = window) {
101
150
  if (state !== null && state.listener !== null) {
102
- targetWindow.removeEventListener('message', state.listener);
151
+ state.listener.off();
103
152
  }
104
153
  state = null;
105
154
  // Reset the local subscriber set too — module-level state is the
@@ -142,6 +191,68 @@ export function subscribeLocal(listener) {
142
191
  };
143
192
  }
144
193
  // =============================================================================
194
+ // DOM application — spec-canonical theme / styles / fonts.
195
+ // =============================================================================
196
+ /**
197
+ * Apply the host's `theme`, `styles.variables`, `styles.css.fonts`, and
198
+ * `safeAreaInsets` to the iframe DOM via the canonical
199
+ * `@modelcontextprotocol/ext-apps` helpers. Defensive in two ways:
200
+ *
201
+ * 1. Accepts `unknown` so callers (the listener; `bootSequence`'s
202
+ * initial-application path) don't have to gate-check before calling.
203
+ * 2. Per-field optionality is respected — every helper is called only
204
+ * when the corresponding field is present + structurally valid.
205
+ * Malformed input drops the field silently rather than throwing
206
+ * and breaking the boot.
207
+ *
208
+ * Fire-and-forget: a helper throwing (e.g. when running outside a
209
+ * browser DOM context, or under jsdom limits) is caught + swallowed so
210
+ * the WS-echo path stays alive. Theme application is a UX nicety,
211
+ * never load-bearing for agent visibility.
212
+ *
213
+ * @public
214
+ */
215
+ export function applyHostContextStyling(raw) {
216
+ if (!isPlainObject(raw))
217
+ return;
218
+ const ctx = raw;
219
+ try {
220
+ if (ctx.theme === 'light' || ctx.theme === 'dark') {
221
+ applyDocumentTheme(ctx.theme);
222
+ }
223
+ }
224
+ catch {
225
+ // jsdom may not implement every documentElement API the helper
226
+ // touches; swallow + carry on with the remaining fields.
227
+ }
228
+ try {
229
+ const variables = ctx.styles?.variables;
230
+ if (variables !== undefined && variables !== null) {
231
+ applyHostStyleVariables(variables);
232
+ }
233
+ }
234
+ catch {
235
+ // Same posture — best-effort. A subscriber throwing must not stop
236
+ // the WS-echo half of the listener.
237
+ }
238
+ try {
239
+ const fontsCss = ctx.styles?.css?.fonts;
240
+ if (typeof fontsCss === 'string' && fontsCss.length > 0) {
241
+ applyHostFonts(fontsCss);
242
+ }
243
+ }
244
+ catch {
245
+ // applyHostFonts injects a <style> tag; jsdom may reject the
246
+ // operation under strict modes. Swallow.
247
+ }
248
+ // safeAreaInsets — the spec carries these for mobile chrome
249
+ // letterboxing. ext-apps doesn't ship a canonical helper (apps
250
+ // typically apply per-component); the system-card reference applies
251
+ // as root padding. ggui's iframe contains LLM-generated UI which has
252
+ // its own responsive concerns — skip safe-area for now; revisit when
253
+ // a concrete mobile use case appears.
254
+ }
255
+ // =============================================================================
145
256
  // Internals
146
257
  // =============================================================================
147
258
  function isPlainObject(value) {
@@ -150,15 +261,13 @@ function isPlainObject(value) {
150
261
  /**
151
262
  * Process an incoming postMessage payload. Filters to JSON-RPC
152
263
  * notifications with method `ui/notifications/host-context-changed`;
153
- * everything else is ignored. The notification's `params` is a partial
154
- * `McpUiHostContext` — we project and merge into the held value.
264
+ * everything else is ignored. Delegates to {@link handleHostContextChangedParams}
265
+ * once the envelope shape is validated.
155
266
  *
156
- * Exported via `attachListener` indirection; isolated here so it's
157
- * unit-testable without dispatching real `MessageEvent`s.
267
+ * Used by the legacy `window.message` listener path; App's event surface
268
+ * skips this filter (App's own dispatch has already matched the method).
158
269
  */
159
270
  function handleHostContextChangedMessage(raw) {
160
- if (state === null)
161
- return;
162
271
  if (!isPlainObject(raw))
163
272
  return;
164
273
  if (raw['method'] !== 'ui/notifications/host-context-changed')
@@ -166,6 +275,27 @@ function handleHostContextChangedMessage(raw) {
166
275
  const params = raw['params'];
167
276
  if (!isPlainObject(params))
168
277
  return;
278
+ handleHostContextChangedParams(params);
279
+ }
280
+ /**
281
+ * Apply a `ui/notifications/host-context-changed` params payload to
282
+ * the iframe DOM + projection state + WS-echo. Called by both delivery
283
+ * paths (raw window.message in tests / App's `hostcontextchanged`
284
+ * event in production) once they've extracted the partial-context
285
+ * payload.
286
+ *
287
+ * Exported (module-private) so the App listener can call directly
288
+ * without re-validating the JSON-RPC envelope.
289
+ */
290
+ function handleHostContextChangedParams(params) {
291
+ if (state === null)
292
+ return;
293
+ // Apply spec-canonical theme/styles/fonts to the iframe DOM BEFORE
294
+ // the WS-echo path. The DOM-apply path covers fields the projection
295
+ // drops (theme, styles) so the LLM-generated UI inside the iframe
296
+ // re-paints to match a host theme switch even when nothing
297
+ // projection-visible changed (e.g., dark/light toggle).
298
+ applyHostContextStyling(params);
169
299
  // Spec: the notification's params carry a partial McpUiHostContext.
170
300
  // Project it (defensive — malformed inputs drop to undefined fields)
171
301
  // and merge with the held projection so a partial update overlays
@@ -186,7 +316,7 @@ function emit() {
186
316
  state.send({
187
317
  type: 'host_context_observed',
188
318
  payload: {
189
- sessionId: state.sessionId,
319
+ renderId: state.renderId,
190
320
  hostContext: state.current,
191
321
  },
192
322
  });