@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.
- package/README.md +7 -6
- package/dist/channel-transport.d.ts +15 -15
- package/dist/channel-transport.js +32 -35
- package/dist/channels/channel-payload.d.ts +2 -2
- package/dist/channels/channel-payload.js +1 -1
- package/dist/channels/data.d.ts +14 -19
- package/dist/channels/data.js +13 -31
- package/dist/channels/drain-ack.d.ts +2 -2
- package/dist/channels/drain-ack.js +1 -1
- package/dist/channels/feedback.d.ts +1 -1
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/index.js +1 -1
- package/dist/channels/props-update.d.ts +33 -37
- package/dist/channels/props-update.js +30 -106
- package/dist/channels/render-frame.d.ts +52 -0
- package/dist/channels/render-frame.js +53 -0
- package/dist/channels/system.d.ts +1 -1
- package/dist/compiled-validators.d.ts +15 -0
- package/dist/compiled-validators.js +31 -0
- package/dist/context-observer.d.ts +68 -8
- package/dist/context-observer.js +25 -45
- package/dist/events-polling.d.ts +95 -0
- package/dist/events-polling.js +143 -0
- package/dist/gadget-loader.d.ts +1 -1
- package/dist/globals.d.ts +4 -4
- package/dist/host-context-emitter.d.ts +84 -20
- package/dist/host-context-emitter.js +159 -29
- package/dist/iframe-runtime.js +98 -59
- package/dist/index.d.ts +18 -13
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +5 -5
- package/dist/mcp-app-iframe-host.d.ts +9 -9
- package/dist/mcp-app-iframe-host.js +16 -17
- package/dist/meta-parse.d.ts +44 -0
- package/dist/meta-parse.js +390 -0
- package/dist/observability.d.ts +6 -6
- package/dist/protocol-error.d.ts +7 -7
- package/dist/provisional-renderer.d.ts +1 -1
- package/dist/provisional-renderer.js +1 -1
- package/dist/react-renderer.d.ts +10 -11
- package/dist/react-renderer.js +8 -8
- package/dist/registry-subscribe.d.ts +27 -9
- package/dist/registry-subscribe.js +32 -27
- package/dist/render-item.d.ts +83 -0
- package/dist/{stack-item-renderer.js → render-item.js} +106 -106
- package/dist/runtime.d.ts +183 -131
- package/dist/runtime.js +1264 -1330
- package/dist/status-dom.d.ts +52 -47
- package/dist/status-dom.js +51 -34
- package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
- package/dist/system-cards/NoCredentialsCard.js +1 -1
- package/dist/system-cards/ProtocolProbeCard.js +2 -2
- package/dist/system-cards/index.d.ts +4 -4
- package/dist/system-cards/index.js +4 -4
- package/dist/types.d.ts +70 -34
- package/dist/types.js +1 -1
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +2 -2
- package/dist/wire-config.d.ts +54 -82
- package/dist/wire-config.js +61 -93
- package/package.json +9 -8
- package/dist/bootstrap.d.ts +0 -72
- package/dist/bootstrap.js +0 -500
- package/dist/canvas/animator/animator-host.d.ts +0 -63
- package/dist/canvas/animator/animator-host.js +0 -105
- package/dist/canvas/animator/animator-pill.d.ts +0 -42
- package/dist/canvas/animator/animator-pill.js +0 -119
- package/dist/canvas/animator/state-machine.d.ts +0 -106
- package/dist/canvas/animator/state-machine.js +0 -150
- package/dist/canvas/canvas-shell.d.ts +0 -77
- package/dist/canvas/canvas-shell.js +0 -117
- package/dist/canvas/display-mode-policy.d.ts +0 -104
- package/dist/canvas/display-mode-policy.js +0 -98
- package/dist/canvas/mount.d.ts +0 -150
- package/dist/canvas/mount.js +0 -202
- package/dist/canvas/nav-stack.d.ts +0 -133
- package/dist/canvas/nav-stack.js +0 -172
- package/dist/channels/push.d.ts +0 -47
- package/dist/channels/push.js +0 -54
- package/dist/stack-item-renderer.d.ts +0 -103
- package/dist/stack.d.ts +0 -90
- 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
|
|
8
|
-
*
|
|
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
|
-
* `
|
|
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,
|
|
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 `
|
|
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
|
|
27
|
-
*
|
|
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 ~
|
|
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)
|
|
36
|
-
* isolation without the WS transport /
|
|
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 `
|
|
52
|
-
* iframe-runtime is one-
|
|
53
|
-
* exercising multi-
|
|
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
|
-
|
|
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
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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(
|
|
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(
|
|
149
|
+
export function detach(_targetWindow = window) {
|
|
101
150
|
if (state !== null && state.listener !== null) {
|
|
102
|
-
|
|
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.
|
|
154
|
-
*
|
|
264
|
+
* everything else is ignored. Delegates to {@link handleHostContextChangedParams}
|
|
265
|
+
* once the envelope shape is validated.
|
|
155
266
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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
|
-
|
|
319
|
+
renderId: state.renderId,
|
|
190
320
|
hostContext: state.current,
|
|
191
321
|
},
|
|
192
322
|
});
|