@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.
- 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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-level events-polling composition for the iframe-runtime (R7).
|
|
3
|
+
*
|
|
4
|
+
* The R7 cursor-replay model — paired transport of the
|
|
5
|
+
* `/api/renders/:renderId/events?sinceSequence=N&limit=100` HTTP endpoint
|
|
6
|
+
* + the WS subscribe `sinceSequence` cursor. Both transports replay
|
|
7
|
+
* from the same RenderEvent ledger; the polling client uses HTTP, the
|
|
8
|
+
* live client uses WS, and they SHARE the cursor model. Switching
|
|
9
|
+
* transports does not lose events.
|
|
10
|
+
*
|
|
11
|
+
* Semantics (vs. R6 snapshot-polling):
|
|
12
|
+
* - **URL** — `/api/renders/<renderId>/events?wsToken=<token>` with
|
|
13
|
+
* `sinceSequence` and `limit` added per tick.
|
|
14
|
+
* - **Interval** — fixed 2000ms (mirrors R6's default).
|
|
15
|
+
* - **parseSnapshot** — reads the `EventsResponse` envelope, dispatches
|
|
16
|
+
* each event by `event.type` to the registered ChannelHandler (e.g.
|
|
17
|
+
* `render` → render handler; `props_update` → props_update handler),
|
|
18
|
+
* and advances the cursor to `lastSequence`.
|
|
19
|
+
* - **REPLAY_HORIZON_PASSED** — when the server returns 410 the
|
|
20
|
+
* parser can't fold; the consumer must re-mount from a fresh
|
|
21
|
+
* `/state` snapshot. Today the polling transport's `parseSnapshot`
|
|
22
|
+
* only sees the body, so we emit a synthetic `error` frame for the
|
|
23
|
+
* registry to surface upward.
|
|
24
|
+
*
|
|
25
|
+
* # Anthropic first-mount race fix
|
|
26
|
+
*
|
|
27
|
+
* Iframes mounted inside the Anthropic SDK lose `__GGUI_META__` because
|
|
28
|
+
* the SDK strips `_meta` from `tools/call` responses before forwarding
|
|
29
|
+
* the structured-content shell to the iframe. R5's documented gap was
|
|
30
|
+
* that the first push's payload never reached the iframe.
|
|
31
|
+
*
|
|
32
|
+
* R7's principled fix: the same /events endpoint that powers polling
|
|
33
|
+
* ALSO serves the cold-mount path. An iframe booting without inline
|
|
34
|
+
* meta calls `/events?sinceSequence=0&limit=1` to fetch the first
|
|
35
|
+
* render event from the ledger; the wsToken comes from the iframe's
|
|
36
|
+
* URL query string (the server stamps it on the resource URI when
|
|
37
|
+
* minting the render tool result, preserved across the Anthropic SDK
|
|
38
|
+
* strip). One unified cursor model handles cold-mount, polling
|
|
39
|
+
* fallback, and live updates.
|
|
40
|
+
*/
|
|
41
|
+
import type { RegistryPollingOptions } from '@ggui-ai/live-channel';
|
|
42
|
+
export interface BuildEventsPollingOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Base URL the polling tick reads from. The composer appends
|
|
45
|
+
* `&sinceSequence=<cursor>&limit=<limit>` per tick. Typically the
|
|
46
|
+
* `/api/renders/<renderId>/events?wsToken=<token>` URL the iframe
|
|
47
|
+
* derived from the render slice. Must already include a `?` or `&`
|
|
48
|
+
* separator-ready terminator; we add the cursor params with
|
|
49
|
+
* `&` if the URL contains `?`, else `?`.
|
|
50
|
+
*/
|
|
51
|
+
readonly baseUrl: string;
|
|
52
|
+
/**
|
|
53
|
+
* Optional cursor seed — initial value of `sinceSequence` on the
|
|
54
|
+
* first tick. Typically threaded from `render.lastSequence` so the
|
|
55
|
+
* cold-mount-after-WS-fail path picks up where the snapshot left
|
|
56
|
+
* off. Defaults to `0` (replay everything still retained).
|
|
57
|
+
*/
|
|
58
|
+
readonly initialSinceSequence?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Optional poll cadence override. Defaults to 2000ms.
|
|
61
|
+
*/
|
|
62
|
+
readonly intervalMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Optional per-page event cap. Defaults to 100 (matches server
|
|
65
|
+
* default). The polling transport handles pagination by re-polling
|
|
66
|
+
* on the next tick when `hasMore` is true — we don't loop within a
|
|
67
|
+
* single tick to keep latency bounded.
|
|
68
|
+
*/
|
|
69
|
+
readonly limit?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a {@link RegistryPollingOptions} descriptor that reads
|
|
73
|
+
* `/api/renders/:renderId/events?sinceSequence=N&limit=M` and dispatches
|
|
74
|
+
* each `RenderEvent` by `event.type` to the registry's matching
|
|
75
|
+
* channel handler. Cursor advances per-tick to the server's
|
|
76
|
+
* `lastSequence`.
|
|
77
|
+
*
|
|
78
|
+
* # Per-tick URL composition
|
|
79
|
+
*
|
|
80
|
+
* The transport calls `polling.url` verbatim. Because the cursor
|
|
81
|
+
* changes per tick we can't fix a single URL at build time; instead we
|
|
82
|
+
* implement a single-tick read by mutating a closure-scoped cursor
|
|
83
|
+
* and recomputing the URL inside `parseSnapshot`. But `parseSnapshot`
|
|
84
|
+
* gets the BODY, not the URL — so this design uses a layered approach:
|
|
85
|
+
* the URL on the descriptor IS the cursor-aware URL composed inside a
|
|
86
|
+
* proxy fetch wrapper layer above. For now, this implementation
|
|
87
|
+
* returns the cursor-aware URL via a synchronous re-composition each
|
|
88
|
+
* tick (the transport calls fetch ONCE per tick).
|
|
89
|
+
*
|
|
90
|
+
* Implementation note: `RegistryPollingOptions.url` is a static string
|
|
91
|
+
* on the type, but we need a per-tick re-computation. We achieve this
|
|
92
|
+
* via a `Proxy` getter on the returned object — each access of `url`
|
|
93
|
+
* pulls the live cursor and composes the URL anew.
|
|
94
|
+
*/
|
|
95
|
+
export declare function buildEventsPolling(opts: BuildEventsPollingOptions): RegistryPollingOptions;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const DEFAULT_EVENTS_POLL_INTERVAL_MS = 2000;
|
|
2
|
+
const DEFAULT_EVENTS_PAGE_LIMIT = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Compose the per-tick URL with `&sinceSequence=N&limit=M` appended.
|
|
5
|
+
* The base URL may or may not already carry a query string; we pick
|
|
6
|
+
* the right separator.
|
|
7
|
+
*/
|
|
8
|
+
function composeTickUrl(baseUrl, sinceSequence, limit) {
|
|
9
|
+
const sep = baseUrl.includes('?') ? '&' : '?';
|
|
10
|
+
return `${baseUrl}${sep}sinceSequence=${sinceSequence}&limit=${limit}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for the EventsResponse envelope shape. Defends against
|
|
14
|
+
* server bugs / proxy interference that could feed the parser
|
|
15
|
+
* non-conforming JSON.
|
|
16
|
+
*/
|
|
17
|
+
function isEventsResponse(body) {
|
|
18
|
+
if (body === null || typeof body !== 'object')
|
|
19
|
+
return false;
|
|
20
|
+
const obj = body;
|
|
21
|
+
if (!Array.isArray(obj['events']))
|
|
22
|
+
return false;
|
|
23
|
+
if (typeof obj['lastSequence'] !== 'number')
|
|
24
|
+
return false;
|
|
25
|
+
if (typeof obj['hasMore'] !== 'boolean')
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type guard for one RenderEvent in the events array.
|
|
31
|
+
*/
|
|
32
|
+
function isRenderEvent(value) {
|
|
33
|
+
if (value === null || typeof value !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
const obj = value;
|
|
36
|
+
if (typeof obj['seq'] !== 'number')
|
|
37
|
+
return false;
|
|
38
|
+
if (typeof obj['timestamp'] !== 'string')
|
|
39
|
+
return false;
|
|
40
|
+
if (typeof obj['type'] !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
// `data` is intentionally unconstrained — typed at the
|
|
43
|
+
// consumer-side handler that dispatches on `type`.
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a {@link RegistryPollingOptions} descriptor that reads
|
|
48
|
+
* `/api/renders/:renderId/events?sinceSequence=N&limit=M` and dispatches
|
|
49
|
+
* each `RenderEvent` by `event.type` to the registry's matching
|
|
50
|
+
* channel handler. Cursor advances per-tick to the server's
|
|
51
|
+
* `lastSequence`.
|
|
52
|
+
*
|
|
53
|
+
* # Per-tick URL composition
|
|
54
|
+
*
|
|
55
|
+
* The transport calls `polling.url` verbatim. Because the cursor
|
|
56
|
+
* changes per tick we can't fix a single URL at build time; instead we
|
|
57
|
+
* implement a single-tick read by mutating a closure-scoped cursor
|
|
58
|
+
* and recomputing the URL inside `parseSnapshot`. But `parseSnapshot`
|
|
59
|
+
* gets the BODY, not the URL — so this design uses a layered approach:
|
|
60
|
+
* the URL on the descriptor IS the cursor-aware URL composed inside a
|
|
61
|
+
* proxy fetch wrapper layer above. For now, this implementation
|
|
62
|
+
* returns the cursor-aware URL via a synchronous re-composition each
|
|
63
|
+
* tick (the transport calls fetch ONCE per tick).
|
|
64
|
+
*
|
|
65
|
+
* Implementation note: `RegistryPollingOptions.url` is a static string
|
|
66
|
+
* on the type, but we need a per-tick re-computation. We achieve this
|
|
67
|
+
* via a `Proxy` getter on the returned object — each access of `url`
|
|
68
|
+
* pulls the live cursor and composes the URL anew.
|
|
69
|
+
*/
|
|
70
|
+
export function buildEventsPolling(opts) {
|
|
71
|
+
let cursor = opts.initialSinceSequence ?? 0;
|
|
72
|
+
const limit = opts.limit ?? DEFAULT_EVENTS_PAGE_LIMIT;
|
|
73
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_EVENTS_POLL_INTERVAL_MS;
|
|
74
|
+
// Each `url` access recomputes from the current cursor. The
|
|
75
|
+
// PollingTransport's tick calls `polling.url` once per fetch.
|
|
76
|
+
const descriptor = Object.create(null);
|
|
77
|
+
Object.defineProperty(descriptor, 'url', {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
get: () => composeTickUrl(opts.baseUrl, cursor, limit),
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(descriptor, 'intervalMs', {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
value: intervalMs,
|
|
84
|
+
});
|
|
85
|
+
Object.defineProperty(descriptor, 'parseSnapshot', {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
value: (body) => {
|
|
88
|
+
// 410 case manifests as a body that doesn't match the
|
|
89
|
+
// EventsResponse shape (`{reason: 'REPLAY_HORIZON_PASSED',
|
|
90
|
+
// currentSequence}`). Surface a synthetic `error` frame so the
|
|
91
|
+
// registry's error handler can fire the re-mount signal.
|
|
92
|
+
if (body !== null &&
|
|
93
|
+
typeof body === 'object' &&
|
|
94
|
+
body.reason === 'REPLAY_HORIZON_PASSED') {
|
|
95
|
+
const cs = body.currentSequence;
|
|
96
|
+
const currentSequence = typeof cs === 'number' ? cs : 0;
|
|
97
|
+
// Reset cursor to the server's high-water mark; next tick
|
|
98
|
+
// starts fresh from there. Consumers handle the re-mount via
|
|
99
|
+
// the error channel.
|
|
100
|
+
cursor = currentSequence;
|
|
101
|
+
const errorFrame = {
|
|
102
|
+
type: 'error',
|
|
103
|
+
payload: {
|
|
104
|
+
code: 'REPLAY_HORIZON_PASSED',
|
|
105
|
+
message: `events polling cursor outside replayable range; reset to ${currentSequence}`,
|
|
106
|
+
details: { currentSequence },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
return { error: errorFrame };
|
|
110
|
+
}
|
|
111
|
+
if (!isEventsResponse(body))
|
|
112
|
+
return null;
|
|
113
|
+
// Advance cursor even on empty pages — the server's high-water
|
|
114
|
+
// mark moves with /state reads too.
|
|
115
|
+
cursor = body.lastSequence;
|
|
116
|
+
if (body.events.length === 0) {
|
|
117
|
+
// Nothing to dispatch; the empty object signals "snapshot
|
|
118
|
+
// parsed but no handlers matched today's keys" (distinct from
|
|
119
|
+
// `null` = no change).
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
// Group events by type. The registry-level polling transport
|
|
123
|
+
// calls one handler per type; if multiple events share a type
|
|
124
|
+
// we'd lose deliveries. Today's wire frame types
|
|
125
|
+
// (render/props_update) are typically distinct per tick at the
|
|
126
|
+
// expected 2s cadence, but the protocol allows multiple of the
|
|
127
|
+
// same type. Honest workaround: dispatch the LAST event of each
|
|
128
|
+
// type and rely on the consumer's idempotency. Future R8: extend
|
|
129
|
+
// the dispatch shape to a list per type.
|
|
130
|
+
const frames = {};
|
|
131
|
+
for (const event of body.events) {
|
|
132
|
+
if (!isRenderEvent(event))
|
|
133
|
+
continue;
|
|
134
|
+
frames[event.type] = {
|
|
135
|
+
type: event.type,
|
|
136
|
+
payload: event.data,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return frames;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return descriptor;
|
|
143
|
+
}
|
package/dist/gadget-loader.d.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
import type { GadgetPackageRegistry, ModuleNamespace } from './globals.js';
|
|
39
39
|
/**
|
|
40
40
|
* Minimal shape consumed by the loader — mirrors the protocol's
|
|
41
|
-
* `
|
|
41
|
+
* `McpAppAiGguiRenderMeta.gadgets[*]` entry but typed locally so the loader
|
|
42
42
|
* is testable without dragging the protocol package into unit setups.
|
|
43
43
|
*/
|
|
44
44
|
export interface GadgetRegistration {
|
package/dist/globals.d.ts
CHANGED
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
* means the registry MUST be populated BEFORE any `loadModule()` call
|
|
36
36
|
* evaluates generated code. The boot orchestration in
|
|
37
37
|
* `runtime.ts::bootSequence` enforces this by calling
|
|
38
|
-
* `installGlobalRegistry()` BEFORE the subscribe ack's
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* `installGlobalRegistry()` BEFORE the subscribe ack's render mounts.
|
|
39
|
+
* Race-free: Node/browser event loop is single-threaded and no other
|
|
40
|
+
* code runs between `installGlobalRegistry()` and the first render
|
|
41
|
+
* invocation.
|
|
42
42
|
*/
|
|
43
43
|
import type { Context } from 'react';
|
|
44
44
|
/**
|
|
@@ -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 { type App } from '@modelcontextprotocol/ext-apps';
|
|
40
56
|
import { type HostContextObservedPayload, type HostContextProjection } from '@ggui-ai/protocol';
|
|
41
57
|
/**
|
|
42
58
|
* Function the emitter calls to ship a `host_context_observed` envelope.
|
|
@@ -60,28 +76,56 @@ export type HostContextSendFn = (msg: {
|
|
|
60
76
|
* the same projection) suppresses the duplicate emission via the
|
|
61
77
|
* equality check.
|
|
62
78
|
*
|
|
63
|
-
* Re-seeding with a DIFFERENT `
|
|
64
|
-
* iframe-runtime is one-
|
|
65
|
-
* exercising multi-
|
|
79
|
+
* Re-seeding with a DIFFERENT `renderId` is undefined behavior; the
|
|
80
|
+
* iframe-runtime is one-render-per-mount by construction. Tests
|
|
81
|
+
* exercising multi-render flows MUST call `detach()` between seeds.
|
|
66
82
|
*/
|
|
67
83
|
export declare function seed(args: {
|
|
68
|
-
readonly
|
|
84
|
+
readonly renderId: string;
|
|
69
85
|
readonly send: HostContextSendFn;
|
|
70
86
|
readonly initial: HostContextProjection;
|
|
71
87
|
}): void;
|
|
88
|
+
export interface AttachListenerOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Bind via the spec-canonical `App.addEventListener('hostcontextchanged')`
|
|
91
|
+
* event surface. Production wires this — App's onEventDispatch
|
|
92
|
+
* pre-merges the params into its internal `_hostContext` before
|
|
93
|
+
* our handler runs, so `app.getHostContext()` is always fresh by
|
|
94
|
+
* the time we project + WS-echo.
|
|
95
|
+
*
|
|
96
|
+
* When omitted, falls back to a raw `window.addEventListener('message')`
|
|
97
|
+
* listener (the pre-1.19b.3 behavior). Tests rely on this — they
|
|
98
|
+
* dispatch `MessageEvent`s directly instead of constructing a
|
|
99
|
+
* connected App.
|
|
100
|
+
*/
|
|
101
|
+
readonly app?: App;
|
|
102
|
+
/**
|
|
103
|
+
* Target window for the legacy raw-listener fallback. Ignored when
|
|
104
|
+
* {@link AttachListenerOptions.app} is set. Defaults to `window`.
|
|
105
|
+
*/
|
|
106
|
+
readonly targetWindow?: Window;
|
|
107
|
+
}
|
|
72
108
|
/**
|
|
73
|
-
* Install the `host-context-changed` notification listener
|
|
74
|
-
*
|
|
75
|
-
*
|
|
109
|
+
* Install the `host-context-changed` notification listener. When the
|
|
110
|
+
* {@link AttachListenerOptions.app} handle is supplied, uses App's
|
|
111
|
+
* spec-canonical event surface; otherwise falls back to a raw
|
|
112
|
+
* `window.message` listener (tests use this).
|
|
113
|
+
*
|
|
114
|
+
* Safe to call multiple times — duplicate listeners are deduped via
|
|
115
|
+
* `state.listener !== null` guard.
|
|
76
116
|
*
|
|
77
117
|
* Detaches when `detach()` is called or when the module is torn down.
|
|
78
118
|
*/
|
|
79
|
-
export declare function attachListener(
|
|
119
|
+
export declare function attachListener(optsOrWindow?: AttachListenerOptions | Window): void;
|
|
80
120
|
/**
|
|
81
121
|
* Tear down: remove the listener, clear module state. Called from
|
|
82
122
|
* unit tests between scenarios and from the future host-switch flow.
|
|
123
|
+
*
|
|
124
|
+
* The `targetWindow` parameter is no longer used (the listener handle
|
|
125
|
+
* carries its own cleanup closure); preserved as an optional arg for
|
|
126
|
+
* back-compat with existing test code.
|
|
83
127
|
*/
|
|
84
|
-
export declare function detach(
|
|
128
|
+
export declare function detach(_targetWindow?: Window): void;
|
|
85
129
|
/**
|
|
86
130
|
* Test-only read of the current held projection. Production code
|
|
87
131
|
* MUST NOT depend on this; the emitter is fire-and-forget by design.
|
|
@@ -89,3 +133,23 @@ export declare function detach(targetWindow?: Window): void;
|
|
|
89
133
|
*/
|
|
90
134
|
export declare function _peekCurrent(): HostContextProjection | undefined;
|
|
91
135
|
export declare function subscribeLocal(listener: (projection: HostContextProjection) => void): () => void;
|
|
136
|
+
/**
|
|
137
|
+
* Apply the host's `theme`, `styles.variables`, `styles.css.fonts`, and
|
|
138
|
+
* `safeAreaInsets` to the iframe DOM via the canonical
|
|
139
|
+
* `@modelcontextprotocol/ext-apps` helpers. Defensive in two ways:
|
|
140
|
+
*
|
|
141
|
+
* 1. Accepts `unknown` so callers (the listener; `bootSequence`'s
|
|
142
|
+
* initial-application path) don't have to gate-check before calling.
|
|
143
|
+
* 2. Per-field optionality is respected — every helper is called only
|
|
144
|
+
* when the corresponding field is present + structurally valid.
|
|
145
|
+
* Malformed input drops the field silently rather than throwing
|
|
146
|
+
* and breaking the boot.
|
|
147
|
+
*
|
|
148
|
+
* Fire-and-forget: a helper throwing (e.g. when running outside a
|
|
149
|
+
* browser DOM context, or under jsdom limits) is caught + swallowed so
|
|
150
|
+
* the WS-echo path stays alive. Theme application is a UX nicety,
|
|
151
|
+
* never load-bearing for agent visibility.
|
|
152
|
+
*
|
|
153
|
+
* @public
|
|
154
|
+
*/
|
|
155
|
+
export declare function applyHostContextStyling(raw: unknown): void;
|