@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
@@ -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
+ }
@@ -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
- * `GguiBootstrapMeta.gadgets[*]` entry but typed locally so the loader
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 first stack
39
- * item renders. Race-free: Node/browser event loop is single-threaded
40
- * and no other code runs between `installGlobalRegistry()` and the
41
- * first stack-item render invocation.
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 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 { 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 `sessionId` is undefined behavior; the
64
- * iframe-runtime is one-session-per-mount by construction. Tests
65
- * exercising multi-session flows MUST call `detach()` between seeds.
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 sessionId: string;
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 on the
74
- * iframe `window`. Safe to call multiple times — duplicate listeners
75
- * are deduped via `state.listener !== null` guard.
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(targetWindow?: Window): void;
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(targetWindow?: Window): void;
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;