@ggui-ai/iframe-runtime 0.9.0 → 0.11.0

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.
@@ -30,6 +30,19 @@ export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGgui
30
30
  * so the runtime can mount without any postMessage round-trip.
31
31
  */
32
32
  export declare function parseMetaFromGlobal(): McpAppAiGguiMetaParseResult;
33
+ /**
34
+ * The `ui://ggui/render/…` LOCATOR a tool result publishes when a server
35
+ * runs the read-plane-only posture (`withholdResultMeta`, guuey#209 C3 /
36
+ * ggui#537): the bootstrap material is absent, but the view's durable
37
+ * identity rides `structuredContent.resourceUri` (ggui-aware hosts) and
38
+ * the spec-canonical `_meta.ui.resourceUri` (MCP Apps hosts) — same
39
+ * value, either slot. The runtime resolves such a result through the
40
+ * read-plane door (`app.readServerResource` → the per-render
41
+ * self-contained shell → its inlined envelope). `null` for anything else
42
+ * — a result that carries the slice inline never needs this, and a
43
+ * result with neither is not a view.
44
+ */
45
+ export declare function extractLocatorFromToolResult(params: unknown): string | null;
33
46
  /**
34
47
  * Parse the render slice from a `ui/notifications/tool-result`
35
48
  * postMessage params payload. `params` is a `CallToolResult` per the
@@ -274,6 +274,11 @@ function projectMeta(meta, hasStaticContent) {
274
274
  : {}),
275
275
  ...(meta.codeUrl !== undefined ? { codeUrl: meta.codeUrl } : {}),
276
276
  ...(meta.codeHash !== undefined ? { codeHash: meta.codeHash } : {}),
277
+ // Strict-CSP module-variant twin (ggui#522 slice 2). The protocol
278
+ // parser already enforces the with-a-raw-carrier pairing.
279
+ ...(meta.codeModuleUrl !== undefined
280
+ ? { codeModuleUrl: meta.codeModuleUrl }
281
+ : {}),
277
282
  ...(meta.codeB64 !== undefined ? { codeB64: meta.codeB64 } : {}),
278
283
  ...(meta.kind !== undefined ? { kind: meta.kind } : {}),
279
284
  };
@@ -349,6 +354,29 @@ export function parseMetaFromGlobal() {
349
354
  }
350
355
  return validateMeta(parsed.meta);
351
356
  }
357
+ /**
358
+ * The `ui://ggui/render/…` LOCATOR a tool result publishes when a server
359
+ * runs the read-plane-only posture (`withholdResultMeta`, guuey#209 C3 /
360
+ * ggui#537): the bootstrap material is absent, but the view's durable
361
+ * identity rides `structuredContent.resourceUri` (ggui-aware hosts) and
362
+ * the spec-canonical `_meta.ui.resourceUri` (MCP Apps hosts) — same
363
+ * value, either slot. The runtime resolves such a result through the
364
+ * read-plane door (`app.readServerResource` → the per-render
365
+ * self-contained shell → its inlined envelope). `null` for anything else
366
+ * — a result that carries the slice inline never needs this, and a
367
+ * result with neither is not a view.
368
+ */
369
+ export function extractLocatorFromToolResult(params) {
370
+ if (!isPlainObject(params))
371
+ return null;
372
+ const sc = params['structuredContent'];
373
+ const fromStructured = isPlainObject(sc) && typeof sc['resourceUri'] === 'string' ? sc['resourceUri'] : null;
374
+ const meta = params['_meta'];
375
+ const ui = isPlainObject(meta) ? meta['ui'] : undefined;
376
+ const fromUi = isPlainObject(ui) && typeof ui['resourceUri'] === 'string' ? ui['resourceUri'] : null;
377
+ const uri = fromStructured ?? fromUi;
378
+ return uri !== null && uri.startsWith('ui://ggui/render/') ? uri : null;
379
+ }
352
380
  /**
353
381
  * Parse the render slice from a `ui/notifications/tool-result`
354
382
  * postMessage params payload. `params` is a `CallToolResult` per the
@@ -41,6 +41,18 @@ export interface ReactRootMountOptions {
41
41
  readonly id?: string;
42
42
  readonly componentCode: string;
43
43
  readonly props?: Record<string, unknown>;
44
+ /**
45
+ * Strict-CSP module variant of `componentCode` (ggui#522 slice 2):
46
+ * a server-side import-rewritten twin of the SAME bytes, directly
47
+ * `import()`able (its imports resolve to static shim assets on an
48
+ * allowed origin — no `data:`, no `blob:`, no eval). Tried FIRST;
49
+ * any failure falls through to the blob/inline ladder on the raw
50
+ * bytes. Callers MUST pass it only when it names exactly these
51
+ * `componentCode` bytes — a stale variant would paint the WRONG
52
+ * component, not fail (the runtime's dispatcher guards this by
53
+ * attaching it to the inline seed only).
54
+ */
55
+ readonly codeModuleUrl?: string;
44
56
  };
45
57
  readonly themeId?: string;
46
58
  /**
@@ -50,17 +62,37 @@ export interface ReactRootMountOptions {
50
62
  */
51
63
  readonly themeMode?: 'light' | 'dark';
52
64
  /**
53
- * Per-app theme overlay applied at the iframe's `:root` AFTER the base
54
- * token block, so the operator-supplied `--ggui-*` values win the
55
- * cascade (a partial set — unset vars keep their token defaults). A
56
- * structural subset of protocol's `AppTheme` (`name` is display-only,
57
- * not needed at render). `mode` drives `color-scheme` so native
58
- * controls / scrollbars / canvas pick up dark vs light.
65
+ * Per-app theme overlay — a partial set of `--ggui-*` values; unset
66
+ * vars keep their token defaults. Merged INTO the scoped in-body
67
+ * token block after the base block (and after `hostPalette`), which
68
+ * is the only placement that can recolor tree content: a `:root`-only
69
+ * append is cascade-dead for components — the scoped block sits later
70
+ * in document order and re-specifies every token (browser-verified,
71
+ * see the scoped-merge comment in `renderTree`). The `:root` append
72
+ * still happens, but for body chrome only. A structural subset of
73
+ * protocol's `AppTheme` (`name` is display-only, not needed at
74
+ * render). `mode` drives `color-scheme` so native controls /
75
+ * scrollbars / canvas pick up dark vs light.
76
+ *
77
+ * Precedence (ggui#573 ruling): base tokens < `hostPalette` <
78
+ * `appTheme` < `cssOverrides` — the slice's stamped theme always
79
+ * wins; the host palette is the fallback layer beneath it.
59
80
  */
60
81
  readonly appTheme?: {
61
82
  readonly mode: 'light' | 'dark';
62
83
  readonly cssVariables: Record<string, string>;
63
84
  };
85
+ /**
86
+ * Host-announced palette, already translated onto `--ggui-*` tokens
87
+ * (see `host-palette-bridge.ts` — the spec `--color-*` vocabulary
88
+ * never reaches this layer). Merged into the scoped token block
89
+ * after the base block and BEFORE `appTheme`, so it repaints only
90
+ * the slots no operator theme re-specifies (ggui#572; precedence
91
+ * per the ggui#573 ruling above). Values are sanitized by the
92
+ * bridge before they get here — host input never reaches this
93
+ * string-join raw.
94
+ */
95
+ readonly hostPalette?: Readonly<Record<string, string>>;
64
96
  readonly cssOverrides?: string;
65
97
  readonly onError?: (error: Error) => void;
66
98
  /**
@@ -59,6 +59,18 @@ function makeScopeClass() {
59
59
  scopeCounter += 1;
60
60
  return `ggui-rcr-${scopeCounter}`;
61
61
  }
62
+ /**
63
+ * Join a custom-property record into CSS declarations. Values were
64
+ * validated injection-safe upstream (appTheme: write-side
65
+ * `appThemeSchema` + the wire parser; hostPalette: the host-palette
66
+ * bridge's sanitizer), so the string-join is safe — do NOT
67
+ * re-sanitize here.
68
+ */
69
+ function toCssDecls(vars) {
70
+ return Object.entries(vars)
71
+ .map(([k, v]) => `${k}: ${v};`)
72
+ .join('');
73
+ }
62
74
  // =============================================================================
63
75
  // Error boundary — port of RCR's internal ErrorBoundary (L68–203).
64
76
  // =============================================================================
@@ -235,6 +247,30 @@ export async function mountReactRoot(container, opts) {
235
247
  ? { gadgetPackages: currentOpts.gadgetPackages }
236
248
  : {}),
237
249
  }));
250
+ // Asset-module path (ggui#522 slice 2) — FIRST when the mount
251
+ // carries the strict-CSP variant URL: a plain https module import,
252
+ // legal under `script-src` origins alone, no scheme sources, no
253
+ // eval. Under a strict host CSP it is the ONLY path that works;
254
+ // under a permissive one it costs a CDN round-trip on cold cache
255
+ // and nothing on warm (the URL is immutable). Any failure — CSP
256
+ // block, network fault, a 404 from a redeployed variant family —
257
+ // falls through to the raw-bytes ladder below. No document latch
258
+ // on failure: unlike the per-evaluation blob probe, this branch
259
+ // runs only when componentCode changes (rare — usually once per
260
+ // mount), so the retry cost is one fetch, and the failure classes
261
+ // (transient network vs permanent CSP) are indistinguishable in
262
+ // the import error anyway.
263
+ const moduleUrl = currentOpts.render.codeModuleUrl;
264
+ if (moduleUrl !== undefined) {
265
+ try {
266
+ const mod = (await import(
267
+ /* webpackIgnore: true */ /* @vite-ignore */ moduleUrl));
268
+ return pick(mod);
269
+ }
270
+ catch {
271
+ // Fall through — the raw bytes are the authoritative carrier.
272
+ }
273
+ }
238
274
  // Once URL-scheme module loading has been proven blocked in this
239
275
  // document, go straight to inline execution — the CSP verdict
240
276
  // cannot change within the document's lifetime.
@@ -278,9 +314,33 @@ export async function mountReactRoot(container, opts) {
278
314
  }
279
315
  }
280
316
  function renderTree(opts) {
281
- const themeCss = opts.themeId
317
+ let themeCss = opts.themeId
282
318
  ? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
283
319
  : getScopedCssTokens(scopeClass, opts.themeMode);
320
+ if (opts.hostPalette) {
321
+ // Host palette = the fallback layer (ggui#572, #573 ruling):
322
+ // after the base scoped block so it can repaint at all (the base
323
+ // re-specifies every token), BEFORE the appTheme overlay so the
324
+ // slice's stamped theme wins by document order. Keys + values
325
+ // arrive pre-sanitized from the host-palette bridge.
326
+ themeCss += `.${scopeClass}{${toCssDecls(opts.hostPalette)}}`;
327
+ }
328
+ if (opts.appTheme) {
329
+ const decls = toCssDecls(opts.appTheme.cssVariables);
330
+ // The overlay must be merged HERE, into the scoped style that
331
+ // mounts INSIDE the scope div — the `:root` append below is
332
+ // cascade-dead for tree content: the in-scope block sits later
333
+ // in document order than anything in `<head>`, so at equal
334
+ // specificity the base scoped tokens always win over a head-level
335
+ // override (browser-verified in rnd/gen-ui/beauty/experiments/001,
336
+ // the probe sequence that found this). Appended AFTER the base
337
+ // block + host palette and BEFORE `cssOverrides`, so: base <
338
+ // host palette < operator overlay < explicit overrides
339
+ // (ggui#573: slice wins, host fallback). Values validated
340
+ // injection-safe upstream (same guarantee the `:root` append
341
+ // cites).
342
+ themeCss += `.${scopeClass}{${decls}}`;
343
+ }
284
344
  // Also inject theme CSS at `:root` on `document.head`. The scoped
285
345
  // injection above isolates token resolution to the React tree, but
286
346
  // the embedding shell's body styles (font-family, color, background)
@@ -294,14 +354,21 @@ export async function mountReactRoot(container, opts) {
294
354
  let rootCss = opts.themeId
295
355
  ? getThemeCss(opts.themeId, opts.themeMode)
296
356
  : getCssTokens(opts.themeMode);
357
+ if (opts.hostPalette) {
358
+ // BODY-CHROME ONLY, same as the appTheme `:root` append below —
359
+ // and beneath it, mirroring the scoped-block order (#573:
360
+ // slice wins, host fallback). Keeps the embedding shell's
361
+ // body font/color/background coherent with the tree.
362
+ rootCss += `:root{${toCssDecls(opts.hostPalette)}}`;
363
+ }
297
364
  if (opts.appTheme) {
298
- const decls = Object.entries(opts.appTheme.cssVariables)
299
- .map(([k, v]) => `${k}: ${v};`)
300
- .join('');
301
- // Appended AFTER the base block → the per-app overlay wins; it is a
302
- // partial set (unset vars keep their token defaults). CSS custom
303
- // properties at :root inherit into the scoped React tree, so this
304
- // alone is sufficient for the component to resolve `var(--ggui-*)`.
365
+ const decls = toCssDecls(opts.appTheme.cssVariables);
366
+ // BODY-CHROME ONLY. This `:root` append themes the embedding
367
+ // shell's body styles (font/color/background outside the scope
368
+ // div). It does NOT reach tree content — the scoped in-body
369
+ // token block wins by document order (see the scoped merge in
370
+ // `renderTree`, and rnd/gen-ui/beauty/experiments/001 for the
371
+ // probe that falsified the old "inherits into the tree" claim).
305
372
  // Values were validated injection-safe upstream (write-side
306
373
  // `appThemeSchema` + the wire parser re-validates), so the
307
374
  // string-join here is safe — do NOT re-sanitize.
@@ -29,6 +29,15 @@ export interface RenderItemOptions {
29
29
  * this. STDLIB `@ggui-ai/gadgets` is always rewritten regardless.
30
30
  */
31
31
  readonly gadgetPackages?: readonly string[];
32
+ /**
33
+ * Strict-CSP module variant of the render's `componentCode`
34
+ * (ggui#522 slice 2), forwarded to
35
+ * {@link ReactRootMountOptions.render.codeModuleUrl}. The CALLER
36
+ * owns the bytes↔URL association: pass it only when it names
37
+ * exactly `render.componentCode`'s bytes (the runtime attaches it
38
+ * to the inline seed mount only — a WS-delivered render drops it).
39
+ */
40
+ readonly codeModuleUrl?: string;
32
41
  /** Theme id passed to ReactComponentRenderer for scoped CSS. */
33
42
  readonly themeId?: string;
34
43
  /**
@@ -47,6 +56,14 @@ export interface RenderItemOptions {
47
56
  readonly mode: 'light' | 'dark';
48
57
  readonly cssVariables: Record<string, string>;
49
58
  };
59
+ /**
60
+ * Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
61
+ * host-palette bridge. Forwarded to
62
+ * {@link ReactRootMountOptions.hostPalette}: the fallback layer
63
+ * beneath `appTheme` in the scoped token block (ggui#572, merge
64
+ * order per the ggui#573 ruling — slice wins, host fallback).
65
+ */
66
+ readonly hostPalette?: Readonly<Record<string, string>>;
50
67
  readonly cssOverrides?: string;
51
68
  readonly onError?: (err: Error) => void;
52
69
  /**
@@ -169,11 +169,17 @@ export async function mountRender(container, opts) {
169
169
  ...(render.props !== undefined
170
170
  ? { props: render.props }
171
171
  : {}),
172
+ ...(currentOpts.codeModuleUrl !== undefined
173
+ ? { codeModuleUrl: currentOpts.codeModuleUrl }
174
+ : {}),
172
175
  },
173
176
  ...(gadgetPackages !== undefined ? { gadgetPackages } : {}),
174
177
  ...(currentOpts.themeId !== undefined ? { themeId: currentOpts.themeId } : {}),
175
178
  ...(currentOpts.themeMode !== undefined ? { themeMode: currentOpts.themeMode } : {}),
176
179
  ...(currentOpts.appTheme !== undefined ? { appTheme: currentOpts.appTheme } : {}),
180
+ ...(currentOpts.hostPalette !== undefined
181
+ ? { hostPalette: currentOpts.hostPalette }
182
+ : {}),
177
183
  ...(currentOpts.cssOverrides !== undefined
178
184
  ? { cssOverrides: currentOpts.cssOverrides }
179
185
  : {}),
@@ -234,10 +240,14 @@ export async function mountRender(container, opts) {
234
240
  ...(render.props !== undefined
235
241
  ? { props: render.props }
236
242
  : {}),
243
+ ...(next.codeModuleUrl !== undefined
244
+ ? { codeModuleUrl: next.codeModuleUrl }
245
+ : {}),
237
246
  },
238
247
  ...(next.themeId !== undefined ? { themeId: next.themeId } : {}),
239
248
  ...(next.themeMode !== undefined ? { themeMode: next.themeMode } : {}),
240
249
  ...(next.appTheme !== undefined ? { appTheme: next.appTheme } : {}),
250
+ ...(next.hostPalette !== undefined ? { hostPalette: next.hostPalette } : {}),
241
251
  ...(next.cssOverrides !== undefined ? { cssOverrides: next.cssOverrides } : {}),
242
252
  ...(next.onError !== undefined ? { onError: next.onError } : {}),
243
253
  renderWrapper: wrapInScopedProvider,
package/dist/runtime.d.ts CHANGED
@@ -1,3 +1,30 @@
1
+ /**
2
+ * `@ggui-ai/iframe-runtime` iframe runtime entry.
3
+ *
4
+ * This is the file esbuild bundles into `dist/iframe-runtime.js`. The
5
+ * thin-shell HTML loads it via `<script type="module" src=".../iframe-runtime.js">`;
6
+ * on import the side-effects below take over: build a status DOM,
7
+ * postMessage `ui/initialize` to the parent, parse the bootstrap, open
8
+ * the WebSocket, run the version handshake, and mount the render
9
+ * placeholder.
10
+ *
11
+ * Boot sequence:
12
+ * - Boot from the `_meta["ai.ggui/render"]` slice received via
13
+ * `ui/initialize`.
14
+ * - Open the WebSocket, run the subscribe handshake.
15
+ * - Mount the render — either a structural placeholder or, when
16
+ * the renderer hooks are wired, a React mount of `componentCode`.
17
+ *
18
+ * The runtime advertises its build version via a post-`ui/initialize`
19
+ * notification (`ggui:renderer-ready`).
20
+ *
21
+ * Failure surfacing: every parse / handshake failure goes to two
22
+ * places — (1) the in-iframe status line (operator-visible) and
23
+ * (2) a parent-bound `postMessage({type:'ggui:bootstrap-failed', ...})`.
24
+ * The post-message envelope is consumed by the `<McpAppIframe>` host
25
+ * wrapper, which routes it to the host's `onError` callback.
26
+ */
27
+ import './zod-jitless.js';
1
28
  import type { DrainAckPayload, JsonValue, JsonObject, GguiSession } from '@ggui-ai/protocol';
2
29
  import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
3
30
  import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type McpAppRendererReadyMessage } from '@ggui-ai/protocol/integrations/mcp-apps';
@@ -47,6 +74,110 @@ export type ConnectAppResult = {
47
74
  * without invoking the full boot pipeline.
48
75
  */
49
76
  export declare function setCurrentApp(app: App): void;
77
+ /**
78
+ * The host's announced color mode, read from the connected App's
79
+ * pre-merged hostContext (`ui/initialize` result, kept current by the
80
+ * App across `host-context-changed`). `undefined` when no App is
81
+ * connected, the host sent no `theme`, or it sent something outside
82
+ * the spec's `'light' | 'dark'` literals (dropped, never guessed —
83
+ * the same tolerant posture as `projectHostContext`).
84
+ *
85
+ * This is the INPUT leg of host-theme adaptation
86
+ * (rnd/gen-ui/beauty/experiments/002-host-theme-adaptation.md, ggui#551):
87
+ * `hostContext.theme` alone reaches only `data-theme` +
88
+ * `color-scheme` via `applyDocumentTheme` — nothing in the token
89
+ * pipeline keys off it, so a dark host painted the light ladder
90
+ * (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
91
+ * `buildOpts` consumes this ONLY when the slice stamps no `themeMode`:
92
+ * every operator layer (live provider > per-render override > app
93
+ * default > server default) still wins by being stamped; the host
94
+ * fills the gap where no operator had an opinion.
95
+ *
96
+ * @internal — exported for unit tests (`setCurrentApp` injects the
97
+ * App); production caller is `buildOpts` inside `bootSequence`.
98
+ */
99
+ export declare function hostAnnouncedThemeMode(): 'light' | 'dark' | undefined;
100
+ /**
101
+ * Resolve the mount's base-stylesheet color mode from the slice, with
102
+ * the host as the final fallback (ggui#589, completing the ggui#551
103
+ * precedence ruling — "slice wins, host is the fallback, absent ≠
104
+ * light"):
105
+ *
106
+ * stamped `themeMode` > `theme.mode` (the slice theme OBJECT) >
107
+ * host-announced theme > undefined
108
+ *
109
+ * The middle leg is the ggui#589 fix: a render envelope carrying a
110
+ * per-app theme (`theme: {mode, cssVariables}`) but no top-level
111
+ * `themeMode` is still a slice-stamped mode opinion — before this,
112
+ * the base token ladder ignored it and painted the LIGHT token set
113
+ * under a dark overlay whenever the host (correctly, per the
114
+ * adapter-boundary doctrine in the native host helpers) announced no
115
+ * `hostContext.theme`: a light skeleton in a dark skin. The theme
116
+ * object's own `mode` already drove `color-scheme`; now it also
117
+ * selects the ladder.
118
+ *
119
+ * `undefined` still means "no opinion anywhere" — the renderer's
120
+ * default applies; never coerced to `'light'`.
121
+ *
122
+ * @internal — exported for unit tests; production caller is
123
+ * `buildOpts` inside `bootSequence`.
124
+ */
125
+ export declare function resolveMountThemeMode(meta: {
126
+ readonly themeMode?: 'light' | 'dark';
127
+ readonly theme?: {
128
+ readonly mode: 'light' | 'dark';
129
+ readonly cssVariables?: Record<string, string>;
130
+ };
131
+ }): 'light' | 'dark' | undefined;
132
+ /**
133
+ * Resolve the mount's base-theme id from the slice (ggui#589 ask 3):
134
+ *
135
+ * stamped `themeId` > `theme.name` (the slice theme OBJECT's name)
136
+ * > undefined (renderer default ladder)
137
+ *
138
+ * The name leg makes a slice theme whose `name` matches a REGISTERED
139
+ * theme id select that theme as the BASE token ladder — the overlay's
140
+ * cssVariables still apply above it, so the base carries the full
141
+ * brand ramp coverage a sparse overlay cannot (the "only the mapped
142
+ * vars carry brand" store-frame class). An unregistered name is
143
+ * harmless by construction: `getScopedThemeCss` falls back to the
144
+ * default theme, which is byte-identical to the no-themeId path
145
+ * (`getScopedCssTokens` ≡ `getScopedThemeCss(default)`), so a slice
146
+ * naming its theme "My Custom" renders exactly as before.
147
+ *
148
+ * @internal — exported for unit tests; production caller is
149
+ * `buildOpts` inside `bootSequence`.
150
+ */
151
+ export declare function resolveMountThemeId(meta: {
152
+ readonly themeId?: string;
153
+ readonly theme?: {
154
+ readonly name?: string;
155
+ readonly mode?: 'light' | 'dark';
156
+ readonly cssVariables?: Record<string, string>;
157
+ };
158
+ }): string | undefined;
159
+ /**
160
+ * The host's announced palette, read from the connected App's
161
+ * pre-merged hostContext and translated onto `--ggui-*` tokens by the
162
+ * host-palette bridge. `undefined` when no App is connected, the host
163
+ * sent no `styles.variables`, or nothing in it maps/survives
164
+ * sanitization — callers spread-skip the option, exactly like
165
+ * {@link hostAnnouncedThemeMode}.
166
+ *
167
+ * This is the palette input leg of host-theme adaptation (ggui#572,
168
+ * the color analog of #551's mode leg above): the spec `--color-*`
169
+ * variables land as inline custom properties on `<html>` via the
170
+ * canonical ext-apps helper, but nothing ggui renders consumes that
171
+ * vocabulary, and the scoped token block shadows root inheritance
172
+ * anyway — so without this mapping a host palette repaints nothing.
173
+ * `buildOpts` threads it onto the mount options; the renderer merges
174
+ * it as the fallback layer BENEATH the slice's own theme (ggui#573
175
+ * ruling: slice wins, host fallback).
176
+ *
177
+ * @internal — exported for unit tests (`setCurrentApp` injects the
178
+ * App); production caller is `buildOpts` inside `bootSequence`.
179
+ */
180
+ export declare function hostAnnouncedPalette(): Readonly<Record<string, string>> | undefined;
50
181
  /**
51
182
  * @internal — exported for unit tests to reset module state between
52
183
  * scenarios.
@@ -193,6 +324,17 @@ export interface BootSequenceOptions {
193
324
  * arrives, and `hostContext` is captured from `app.getHostContext()`.
194
325
  */
195
326
  readonly preResolvedMeta?: McpAppAiGguiRenderMeta;
327
+ /**
328
+ * Autostart-layer pre-resolution of the read-plane door (ggui#537):
329
+ * a tool result that arrived before `bootSequence` ran carried the
330
+ * view's LOCATOR (`ui://ggui/render/…`) but no bootstrap material —
331
+ * the read-plane-only posture. The runtime cannot read the resource
332
+ * before the App is connected, so the locator is threaded here and
333
+ * resolved via {@link resolveMetaViaReadDoor} right after the
334
+ * handshake, ahead of the Tier 2 tool-result wait. Ignored when
335
+ * `preResolvedMeta` is set.
336
+ */
337
+ readonly preResolvedLocator?: string;
196
338
  /**
197
339
  * How long to wait for the spec-canonical `ui/notifications/tool-result`
198
340
  * notification (Tier 2 of the resolver chain) before failing with
@@ -409,6 +551,29 @@ export declare function extractMetaFromToolResult(params: unknown): McpAppAiGgui
409
551
  * runtime reads it once at autostart.
410
552
  */
411
553
  export declare function readPendingToolResults(): McpAppAiGguiRenderMeta | null;
554
+ /**
555
+ * Sibling of {@link readPendingToolResults} for the read-plane-only
556
+ * posture (ggui#537): the NEWEST buffered tool result that carries a
557
+ * `ui://ggui/render/…` locator but no bootstrap material. Consulted only
558
+ * when the buffer yielded no inline slice; the autostart threads it to
559
+ * `bootSequence` as `preResolvedLocator`, resolved after the handshake.
560
+ */
561
+ export declare function readPendingToolResultLocator(): string | null;
562
+ /**
563
+ * The read-plane door (ggui#537). A server running the read-plane-only
564
+ * posture publishes only the view's identity on the tool result — the
565
+ * `ui://ggui/render/…` locator — and no bootstrap material; the
566
+ * per-render resource that locator names is the self-contained shell,
567
+ * whose document inlines the very envelope this runtime boots from.
568
+ * Ask the HOST to read it (`app.readServerResource` → `resources/read`,
569
+ * proxied by the host's own MCP client — no fetch from this sandbox, so
570
+ * a host CSP without `connect-src` to the server is not in the way),
571
+ * recover the envelope with the protocol's writer/reader pair
572
+ * ({@link readGguiShellEnvelope}), and validate it exactly as an inline
573
+ * slice would be. `null` on any miss (host cannot proxy reads, resource
574
+ * gone, no envelope) — the caller's other tiers still decide.
575
+ */
576
+ export declare function resolveMetaViaReadDoor(app: App, locator: string): Promise<McpAppAiGguiRenderMeta | null>;
412
577
  /**
413
578
  * Render a submit-action's `data` payload as a short inline string
414
579
  * for embedding in a `ui/message` consent prompt. Goal: human-
@@ -649,7 +814,7 @@ export declare function requestDisplayModeInParent(args: {
649
814
  * or honor an upstream `defaultPrevented` from a higher-priority
650
815
  * handler.
651
816
  *
652
- * Idempotent: re-mounts (via `installPostMountListener`) call this
817
+ * Idempotent: re-mounts (via `installPersistentToolResultListener`) call this
653
818
  * helper again; the {@link anchorClickInterceptInstalled} guard makes
654
819
  * subsequent calls a no-op.
655
820
  */