@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.
- package/dist/compiled-validators.d.ts +16 -7
- package/dist/compiled-validators.js +41 -8
- package/dist/host-palette-bridge.d.ts +43 -0
- package/dist/host-palette-bridge.js +107 -0
- package/dist/iframe-runtime.js +75 -73
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/meta-parse.d.ts +13 -0
- package/dist/meta-parse.js +28 -0
- package/dist/react-renderer.d.ts +38 -6
- package/dist/react-renderer.js +75 -8
- package/dist/render-item.d.ts +17 -0
- package/dist/render-item.js +10 -0
- package/dist/runtime.d.ts +166 -1
- package/dist/runtime.js +349 -67
- package/dist/server.d.ts +19 -0
- package/dist/server.js +19 -0
- package/dist/shims/app-components.js +1 -0
- package/dist/shims/components.js +1 -0
- package/dist/shims/compositions.js +1 -0
- package/dist/shims/design.js +1 -0
- package/dist/shims/gadgets.js +1 -0
- package/dist/shims/interact.js +1 -0
- package/dist/shims/jsx-runtime.js +1 -0
- package/dist/shims/primitives.js +1 -0
- package/dist/shims/react-dom.js +1 -0
- package/dist/shims/react.js +1 -0
- package/dist/shims/templates.js +1 -0
- package/dist/shims/tokens.js +1 -0
- package/dist/shims/wire.js +1 -0
- package/dist/system-cards/ProtocolProbeCard.js +1 -1
- package/dist/zod-jitless.d.ts +1 -0
- package/dist/zod-jitless.js +18 -0
- package/package.json +8 -7
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
// FIRST import on purpose — declares zod jitless before any
|
|
28
|
+
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
|
+
import './zod-jitless.js';
|
|
30
|
+
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
31
|
+
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
3
32
|
import { projectHostContext } from '@ggui-ai/protocol';
|
|
4
33
|
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
|
|
5
34
|
import { applyHostContextStyling, attachListener as attachHostContextListener, seed as seedHostContext, } from './host-context-emitter.js';
|
|
35
|
+
import { mapHostPaletteToGguiVars } from './host-palette-bridge.js';
|
|
6
36
|
import { buildRootWireConfig, StreamBus, } from './wire-config.js';
|
|
7
37
|
import { createChannelTransportRouter, } from './channel-transport.js';
|
|
8
38
|
import { RelayIncapableError } from './relay-incapability.js';
|
|
@@ -58,7 +88,7 @@ function dispatchDrainAck(payload) {
|
|
|
58
88
|
// App-class plumbing (Phase 1.19b.3). The renderer used to roll its own
|
|
59
89
|
// JSON-RPC pump (a closure-scoped `makeJsonRpcCaller` for the single
|
|
60
90
|
// `ui/initialize` request + a module-level `ensurePostRpcToParentListener`
|
|
61
|
-
// for `tools/call` and a `
|
|
91
|
+
// for `tools/call` and a `installPersistentToolResultListener` for inbound
|
|
62
92
|
// `ui/notifications/tool-result`). Every one of those was a hand-rolled
|
|
63
93
|
// reimplementation of a primitive `@modelcontextprotocol/ext-apps`'s
|
|
64
94
|
// `App` class already ships — drift across them produced the Reading-B
|
|
@@ -131,6 +161,106 @@ export function setCurrentApp(app) {
|
|
|
131
161
|
function getCurrentApp() {
|
|
132
162
|
return currentApp;
|
|
133
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* The host's announced color mode, read from the connected App's
|
|
166
|
+
* pre-merged hostContext (`ui/initialize` result, kept current by the
|
|
167
|
+
* App across `host-context-changed`). `undefined` when no App is
|
|
168
|
+
* connected, the host sent no `theme`, or it sent something outside
|
|
169
|
+
* the spec's `'light' | 'dark'` literals (dropped, never guessed —
|
|
170
|
+
* the same tolerant posture as `projectHostContext`).
|
|
171
|
+
*
|
|
172
|
+
* This is the INPUT leg of host-theme adaptation
|
|
173
|
+
* (rnd/gen-ui/beauty/experiments/002-host-theme-adaptation.md, ggui#551):
|
|
174
|
+
* `hostContext.theme` alone reaches only `data-theme` +
|
|
175
|
+
* `color-scheme` via `applyDocumentTheme` — nothing in the token
|
|
176
|
+
* pipeline keys off it, so a dark host painted the light ladder
|
|
177
|
+
* (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
|
|
178
|
+
* `buildOpts` consumes this ONLY when the slice stamps no `themeMode`:
|
|
179
|
+
* every operator layer (live provider > per-render override > app
|
|
180
|
+
* default > server default) still wins by being stamped; the host
|
|
181
|
+
* fills the gap where no operator had an opinion.
|
|
182
|
+
*
|
|
183
|
+
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
184
|
+
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
185
|
+
*/
|
|
186
|
+
export function hostAnnouncedThemeMode() {
|
|
187
|
+
const theme = currentApp?.getHostContext()?.theme;
|
|
188
|
+
return theme === 'light' || theme === 'dark' ? theme : undefined;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the mount's base-stylesheet color mode from the slice, with
|
|
192
|
+
* the host as the final fallback (ggui#589, completing the ggui#551
|
|
193
|
+
* precedence ruling — "slice wins, host is the fallback, absent ≠
|
|
194
|
+
* light"):
|
|
195
|
+
*
|
|
196
|
+
* stamped `themeMode` > `theme.mode` (the slice theme OBJECT) >
|
|
197
|
+
* host-announced theme > undefined
|
|
198
|
+
*
|
|
199
|
+
* The middle leg is the ggui#589 fix: a render envelope carrying a
|
|
200
|
+
* per-app theme (`theme: {mode, cssVariables}`) but no top-level
|
|
201
|
+
* `themeMode` is still a slice-stamped mode opinion — before this,
|
|
202
|
+
* the base token ladder ignored it and painted the LIGHT token set
|
|
203
|
+
* under a dark overlay whenever the host (correctly, per the
|
|
204
|
+
* adapter-boundary doctrine in the native host helpers) announced no
|
|
205
|
+
* `hostContext.theme`: a light skeleton in a dark skin. The theme
|
|
206
|
+
* object's own `mode` already drove `color-scheme`; now it also
|
|
207
|
+
* selects the ladder.
|
|
208
|
+
*
|
|
209
|
+
* `undefined` still means "no opinion anywhere" — the renderer's
|
|
210
|
+
* default applies; never coerced to `'light'`.
|
|
211
|
+
*
|
|
212
|
+
* @internal — exported for unit tests; production caller is
|
|
213
|
+
* `buildOpts` inside `bootSequence`.
|
|
214
|
+
*/
|
|
215
|
+
export function resolveMountThemeMode(meta) {
|
|
216
|
+
return meta.themeMode ?? meta.theme?.mode ?? hostAnnouncedThemeMode();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve the mount's base-theme id from the slice (ggui#589 ask 3):
|
|
220
|
+
*
|
|
221
|
+
* stamped `themeId` > `theme.name` (the slice theme OBJECT's name)
|
|
222
|
+
* > undefined (renderer default ladder)
|
|
223
|
+
*
|
|
224
|
+
* The name leg makes a slice theme whose `name` matches a REGISTERED
|
|
225
|
+
* theme id select that theme as the BASE token ladder — the overlay's
|
|
226
|
+
* cssVariables still apply above it, so the base carries the full
|
|
227
|
+
* brand ramp coverage a sparse overlay cannot (the "only the mapped
|
|
228
|
+
* vars carry brand" store-frame class). An unregistered name is
|
|
229
|
+
* harmless by construction: `getScopedThemeCss` falls back to the
|
|
230
|
+
* default theme, which is byte-identical to the no-themeId path
|
|
231
|
+
* (`getScopedCssTokens` ≡ `getScopedThemeCss(default)`), so a slice
|
|
232
|
+
* naming its theme "My Custom" renders exactly as before.
|
|
233
|
+
*
|
|
234
|
+
* @internal — exported for unit tests; production caller is
|
|
235
|
+
* `buildOpts` inside `bootSequence`.
|
|
236
|
+
*/
|
|
237
|
+
export function resolveMountThemeId(meta) {
|
|
238
|
+
return meta.themeId ?? meta.theme?.name;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The host's announced palette, read from the connected App's
|
|
242
|
+
* pre-merged hostContext and translated onto `--ggui-*` tokens by the
|
|
243
|
+
* host-palette bridge. `undefined` when no App is connected, the host
|
|
244
|
+
* sent no `styles.variables`, or nothing in it maps/survives
|
|
245
|
+
* sanitization — callers spread-skip the option, exactly like
|
|
246
|
+
* {@link hostAnnouncedThemeMode}.
|
|
247
|
+
*
|
|
248
|
+
* This is the palette input leg of host-theme adaptation (ggui#572,
|
|
249
|
+
* the color analog of #551's mode leg above): the spec `--color-*`
|
|
250
|
+
* variables land as inline custom properties on `<html>` via the
|
|
251
|
+
* canonical ext-apps helper, but nothing ggui renders consumes that
|
|
252
|
+
* vocabulary, and the scoped token block shadows root inheritance
|
|
253
|
+
* anyway — so without this mapping a host palette repaints nothing.
|
|
254
|
+
* `buildOpts` threads it onto the mount options; the renderer merges
|
|
255
|
+
* it as the fallback layer BENEATH the slice's own theme (ggui#573
|
|
256
|
+
* ruling: slice wins, host fallback).
|
|
257
|
+
*
|
|
258
|
+
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
259
|
+
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
260
|
+
*/
|
|
261
|
+
export function hostAnnouncedPalette() {
|
|
262
|
+
return mapHostPaletteToGguiVars(currentApp?.getHostContext()?.styles?.variables);
|
|
263
|
+
}
|
|
134
264
|
/**
|
|
135
265
|
* @internal — exported for unit tests to reset module state between
|
|
136
266
|
* scenarios.
|
|
@@ -259,6 +389,14 @@ export async function bootSequence(opts) {
|
|
|
259
389
|
const toolResultPromise = opts.preResolvedMeta !== undefined
|
|
260
390
|
? Promise.resolve(null)
|
|
261
391
|
: awaitToolResultMetaFromApp(app, toolResultTimeoutMs);
|
|
392
|
+
// Persistent re-mount listener — ALSO pre-connect (ggui#586). On a
|
|
393
|
+
// Tier-1/preResolved boot the one-shot listener above is skipped,
|
|
394
|
+
// which made the post-mount installation the FIRST toolresult
|
|
395
|
+
// registration — after the handshake, tripping the ext-apps timing
|
|
396
|
+
// notice and leaning on SDK replay for correctness. Registering here
|
|
397
|
+
// covers every boot path; arrivals before a renderer publishes
|
|
398
|
+
// `applyRender` no-op inside the listener.
|
|
399
|
+
installPersistentToolResultListener(app);
|
|
262
400
|
const initResult = await connectApp(app, transport);
|
|
263
401
|
if (!initResult.ok) {
|
|
264
402
|
setStatus(refs, `ui/initialize failed: ${initResult.message}`, 'error');
|
|
@@ -314,7 +452,16 @@ export async function bootSequence(opts) {
|
|
|
314
452
|
parsed = inline;
|
|
315
453
|
}
|
|
316
454
|
else {
|
|
317
|
-
|
|
455
|
+
// Tier 1.5 — the read-plane door (ggui#537). A pre-boot tool
|
|
456
|
+
// result named the view but carried no material; now that the
|
|
457
|
+
// App is connected, resolve the locator through the host's
|
|
458
|
+
// `resources/read` proxy. One round trip, ahead of the Tier 2
|
|
459
|
+
// wait; a miss falls through to Tier 2 unchanged (its listener
|
|
460
|
+
// is already armed and resolves locator-only arrivals too).
|
|
461
|
+
const viaDoor = opts.preResolvedLocator !== undefined
|
|
462
|
+
? await resolveMetaViaReadDoor(app, opts.preResolvedLocator)
|
|
463
|
+
: null;
|
|
464
|
+
const fromToolResult = viaDoor ?? (await toolResultPromise);
|
|
318
465
|
if (fromToolResult !== null) {
|
|
319
466
|
parsed = { ok: true, meta: fromToolResult };
|
|
320
467
|
}
|
|
@@ -1052,6 +1199,26 @@ export function readPendingToolResults() {
|
|
|
1052
1199
|
}
|
|
1053
1200
|
return null;
|
|
1054
1201
|
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Sibling of {@link readPendingToolResults} for the read-plane-only
|
|
1204
|
+
* posture (ggui#537): the NEWEST buffered tool result that carries a
|
|
1205
|
+
* `ui://ggui/render/…` locator but no bootstrap material. Consulted only
|
|
1206
|
+
* when the buffer yielded no inline slice; the autostart threads it to
|
|
1207
|
+
* `bootSequence` as `preResolvedLocator`, resolved after the handshake.
|
|
1208
|
+
*/
|
|
1209
|
+
export function readPendingToolResultLocator() {
|
|
1210
|
+
if (typeof window === 'undefined')
|
|
1211
|
+
return null;
|
|
1212
|
+
const raw = window.__GGUI_PENDING_TOOL_RESULTS__;
|
|
1213
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
1214
|
+
return null;
|
|
1215
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
1216
|
+
const locator = extractLocatorFromToolResult(raw[i]);
|
|
1217
|
+
if (locator !== null)
|
|
1218
|
+
return locator;
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1055
1222
|
/**
|
|
1056
1223
|
* Listen for a `ui/notifications/tool-result` notification via the
|
|
1057
1224
|
* App's spec-canonical event surface and resolve to the extracted
|
|
@@ -1067,36 +1234,75 @@ export function readPendingToolResults() {
|
|
|
1067
1234
|
function awaitToolResultMetaFromApp(app, timeoutMs) {
|
|
1068
1235
|
return new Promise((resolve) => {
|
|
1069
1236
|
let settled = false;
|
|
1070
|
-
const
|
|
1237
|
+
const settle = (value) => {
|
|
1071
1238
|
if (settled)
|
|
1072
1239
|
return;
|
|
1073
|
-
const meta = extractMetaFromToolResult(params);
|
|
1074
|
-
if (meta === null)
|
|
1075
|
-
return;
|
|
1076
1240
|
settled = true;
|
|
1077
1241
|
app.removeEventListener('toolresult', handler);
|
|
1078
1242
|
clearTimeout(timer);
|
|
1079
|
-
resolve(
|
|
1243
|
+
resolve(value);
|
|
1080
1244
|
};
|
|
1081
|
-
|
|
1082
|
-
const timer = setTimeout(() => {
|
|
1245
|
+
const handler = (params) => {
|
|
1083
1246
|
if (settled)
|
|
1084
1247
|
return;
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1248
|
+
const meta = extractMetaFromToolResult(params);
|
|
1249
|
+
if (meta !== null) {
|
|
1250
|
+
settle(meta);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
// Read-plane door (ggui#537): identity-only result → resolve the
|
|
1254
|
+
// locator through the host's `resources/read` proxy. Async, and
|
|
1255
|
+
// it never settles `null` — a later result carrying the slice
|
|
1256
|
+
// inline (or the timeout) still decides.
|
|
1257
|
+
const locator = extractLocatorFromToolResult(params);
|
|
1258
|
+
if (locator === null)
|
|
1259
|
+
return;
|
|
1260
|
+
void resolveMetaViaReadDoor(app, locator).then((resolved) => {
|
|
1261
|
+
if (resolved !== null)
|
|
1262
|
+
settle(resolved);
|
|
1263
|
+
});
|
|
1264
|
+
};
|
|
1265
|
+
app.addEventListener('toolresult', handler);
|
|
1266
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
1089
1267
|
});
|
|
1090
1268
|
}
|
|
1091
1269
|
/**
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
|
|
1270
|
+
* The read-plane door (ggui#537). A server running the read-plane-only
|
|
1271
|
+
* posture publishes only the view's identity on the tool result — the
|
|
1272
|
+
* `ui://ggui/render/…` locator — and no bootstrap material; the
|
|
1273
|
+
* per-render resource that locator names is the self-contained shell,
|
|
1274
|
+
* whose document inlines the very envelope this runtime boots from.
|
|
1275
|
+
* Ask the HOST to read it (`app.readServerResource` → `resources/read`,
|
|
1276
|
+
* proxied by the host's own MCP client — no fetch from this sandbox, so
|
|
1277
|
+
* a host CSP without `connect-src` to the server is not in the way),
|
|
1278
|
+
* recover the envelope with the protocol's writer/reader pair
|
|
1279
|
+
* ({@link readGguiShellEnvelope}), and validate it exactly as an inline
|
|
1280
|
+
* slice would be. `null` on any miss (host cannot proxy reads, resource
|
|
1281
|
+
* gone, no envelope) — the caller's other tiers still decide.
|
|
1282
|
+
*/
|
|
1283
|
+
export async function resolveMetaViaReadDoor(app, locator) {
|
|
1284
|
+
let text;
|
|
1285
|
+
try {
|
|
1286
|
+
const res = await app.readServerResource({ uri: locator });
|
|
1287
|
+
const first = res.contents[0];
|
|
1288
|
+
text = first !== undefined && 'text' in first && typeof first.text === 'string' ? first.text : undefined;
|
|
1289
|
+
}
|
|
1290
|
+
catch (err) {
|
|
1291
|
+
// eslint-disable-next-line no-console -- operator-visible: the door is a boot tier, its miss must be legible
|
|
1292
|
+
console.warn(`[ggui-runtime] read-plane door: host could not read ${locator} — ${err instanceof Error ? err.message : String(err)}`);
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
if (text === undefined)
|
|
1296
|
+
return null;
|
|
1297
|
+
const envelope = readGguiShellEnvelope(text);
|
|
1298
|
+
if (envelope === undefined)
|
|
1299
|
+
return null;
|
|
1300
|
+
const parsed = parseMcpAppAiGguiRenderMeta(envelope);
|
|
1301
|
+
if (!parsed.ok || parsed.meta === undefined)
|
|
1302
|
+
return null;
|
|
1303
|
+
const validated = validateMeta(parsed.meta);
|
|
1304
|
+
return validated.ok ? validated.meta : null;
|
|
1305
|
+
}
|
|
1100
1306
|
function awaitToolResultMeta(timeoutMs) {
|
|
1101
1307
|
if (typeof window === 'undefined')
|
|
1102
1308
|
return Promise.resolve(null);
|
|
@@ -1119,25 +1325,36 @@ function awaitToolResultMeta(timeoutMs) {
|
|
|
1119
1325
|
return;
|
|
1120
1326
|
}
|
|
1121
1327
|
const meta = extractMetaFromToolResult(m.params);
|
|
1122
|
-
if (meta !== null)
|
|
1123
|
-
settle(meta);
|
|
1328
|
+
if (meta !== null) {
|
|
1329
|
+
settle({ kind: 'meta', meta });
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
// Identity-only result (ggui#537): settle NOW with the locator so
|
|
1333
|
+
// boot starts immediately and the door runs after the handshake —
|
|
1334
|
+
// waiting the full timeout for a slice this posture never sends
|
|
1335
|
+
// would cost every spec-host view 30 s for nothing.
|
|
1336
|
+
const locator = extractLocatorFromToolResult(m.params);
|
|
1337
|
+
if (locator !== null)
|
|
1338
|
+
settle({ kind: 'locator', locator });
|
|
1124
1339
|
};
|
|
1125
1340
|
window.addEventListener('message', onMessage);
|
|
1126
1341
|
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
1127
1342
|
});
|
|
1128
1343
|
}
|
|
1129
1344
|
/**
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1345
|
+
* Per-App guard for the persistent toolresult re-listener. Ensures
|
|
1346
|
+
* exactly one `app.addEventListener('toolresult', …)` registration per
|
|
1347
|
+
* App instance even across re-mounts (e.g. an agent fires a second
|
|
1348
|
+
* `ggui_render` and the listener re-applies through the published
|
|
1349
|
+
* `applyRender`). Keyed by App — not a module boolean — so a fresh App
|
|
1350
|
+
* (a re-booted document in tests; production is single-tenant per
|
|
1351
|
+
* iframe) always gets its own registration, never a stale latch.
|
|
1135
1352
|
*/
|
|
1136
|
-
|
|
1353
|
+
const persistentToolResultListenerApps = new WeakSet();
|
|
1137
1354
|
/**
|
|
1138
1355
|
* Module-level handle to the active renderer's `applyRender`, published
|
|
1139
1356
|
* by `bootProduction`'s `setup()` once the renderer is built. The
|
|
1140
|
-
* module-level {@link
|
|
1357
|
+
* module-level {@link installPersistentToolResultListener} resolves this to re-mount
|
|
1141
1358
|
* on a host-re-emitted tool-result WITHOUT a fresh boot — no second mount,
|
|
1142
1359
|
* no second WS. This is the no-WS live-re-render channel for
|
|
1143
1360
|
* spec-compliant MCP-Apps hosts (claude.ai / ChatGPT / Claude Desktop)
|
|
@@ -1149,8 +1366,8 @@ let postMountListenerInstalled = false;
|
|
|
1149
1366
|
let activeApplyRender = null;
|
|
1150
1367
|
/**
|
|
1151
1368
|
* Module-level guard for {@link installAnchorClickInterceptor}. Same
|
|
1152
|
-
* rationale as {@link
|
|
1153
|
-
* triggered by `
|
|
1369
|
+
* rationale as {@link persistentToolResultListenerApps}: re-mounts are
|
|
1370
|
+
* triggered by `installPersistentToolResultListener`, and stacking capture-phase
|
|
1154
1371
|
* click listeners across re-mounts would multiply the audit envelopes
|
|
1155
1372
|
* that fire on a single click.
|
|
1156
1373
|
*/
|
|
@@ -2681,7 +2898,7 @@ export function requestDisplayModeInParent(args) {
|
|
|
2681
2898
|
* or honor an upstream `defaultPrevented` from a higher-priority
|
|
2682
2899
|
* handler.
|
|
2683
2900
|
*
|
|
2684
|
-
* Idempotent: re-mounts (via `
|
|
2901
|
+
* Idempotent: re-mounts (via `installPersistentToolResultListener`) call this
|
|
2685
2902
|
* helper again; the {@link anchorClickInterceptInstalled} guard makes
|
|
2686
2903
|
* subsequent calls a no-op.
|
|
2687
2904
|
*/
|
|
@@ -2822,15 +3039,26 @@ export function __resetInterceptorsForTest() {
|
|
|
2822
3039
|
fullscreenInterceptInstalled = false;
|
|
2823
3040
|
}
|
|
2824
3041
|
/**
|
|
2825
|
-
* Install
|
|
2826
|
-
* that catches `ui/notifications/tool-result` notifications
|
|
2827
|
-
*
|
|
2828
|
-
* different bootstrap (different sessionId / codeUrl / kind)
|
|
2829
|
-
* a re-mount through the published `applyRender
|
|
3042
|
+
* Install the persistent `app.addEventListener('toolresult', …)`
|
|
3043
|
+
* listener that catches `ui/notifications/tool-result` notifications
|
|
3044
|
+
* arriving at ANY point in the document's life. Each tool-result that
|
|
3045
|
+
* carries a different bootstrap (different sessionId / codeUrl / kind)
|
|
3046
|
+
* triggers a re-mount through the published `applyRender`; arrivals
|
|
3047
|
+
* before a renderer publishes `applyRender` no-op safely (and do NOT
|
|
3048
|
+
* latch the dedupe key — see below). This closes the boot-only-
|
|
2830
3049
|
* listener gap that prevented live re-render when an agent issued
|
|
2831
3050
|
* a second `ggui_render` to the same render-resource.
|
|
2832
3051
|
*
|
|
2833
|
-
*
|
|
3052
|
+
* REGISTRATION TIMING (ggui#586): called by `bootSequence` BEFORE
|
|
3053
|
+
* `app.connect(transport)` — on a Tier-1/preResolved boot the one-shot
|
|
3054
|
+
* Tier-2 listener never registers, so this used to be the FIRST
|
|
3055
|
+
* `toolresult` registration and landed post-handshake (the ext-apps
|
|
3056
|
+
* `_assertHandlerTiming` notice a guuey production render surfaced).
|
|
3057
|
+
* The SDK's replay made that harmless; pre-registering removes the
|
|
3058
|
+
* dependence on replay entirely.
|
|
3059
|
+
*
|
|
3060
|
+
* Idempotent per App: repeat calls with the same App no-op via
|
|
3061
|
+
* {@link persistentToolResultListenerApps}.
|
|
2834
3062
|
*
|
|
2835
3063
|
* Why module-level vs scoped to a single mount: re-mounts re-apply
|
|
2836
3064
|
* through `applyRender` while the listener stays live, and the
|
|
@@ -2840,17 +3068,13 @@ export function __resetInterceptorsForTest() {
|
|
|
2840
3068
|
* Spec-canonical (post-Phase-1.19b.3): the previous hand-rolled
|
|
2841
3069
|
* `window.addEventListener('message', …)` is gone; App handles every
|
|
2842
3070
|
* inbound `ui/notifications/tool-result` envelope and dispatches via
|
|
2843
|
-
* its event system.
|
|
2844
|
-
*
|
|
2845
|
-
* path before this helper runs.
|
|
3071
|
+
* its event system. The App handle is passed by the boot path
|
|
3072
|
+
* directly (pre-connect, so `getCurrentApp()` is not yet set).
|
|
2846
3073
|
*/
|
|
2847
|
-
function
|
|
2848
|
-
if (
|
|
3074
|
+
function installPersistentToolResultListener(app) {
|
|
3075
|
+
if (persistentToolResultListenerApps.has(app))
|
|
2849
3076
|
return;
|
|
2850
|
-
|
|
2851
|
-
if (app === null)
|
|
2852
|
-
return; // pre-connect; caller bug, swallow safely
|
|
2853
|
-
postMountListenerInstalled = true;
|
|
3077
|
+
persistentToolResultListenerApps.add(app);
|
|
2854
3078
|
let lastMetaKey = null;
|
|
2855
3079
|
app.addEventListener('toolresult', (params) => {
|
|
2856
3080
|
const meta = extractMetaFromToolResult(params);
|
|
@@ -2888,15 +3112,21 @@ function installPostMountListener() {
|
|
|
2888
3112
|
].join('|');
|
|
2889
3113
|
if (key === lastMetaKey)
|
|
2890
3114
|
return;
|
|
2891
|
-
lastMetaKey = key;
|
|
2892
3115
|
// Re-mount through the EXISTING renderer's `applyRender` (published
|
|
2893
3116
|
// module-level by bootProduction's setup) — NOT a fresh boot. One
|
|
2894
3117
|
// mount surface, one WS; the prior stale-closure / second-WS race
|
|
2895
3118
|
// (the #290 root cause) is structurally impossible. Project the new
|
|
2896
3119
|
// tool-result meta into a seed and re-apply it.
|
|
3120
|
+
//
|
|
3121
|
+
// The apply-null check runs BEFORE the dedupe key latches (ggui#586
|
|
3122
|
+
// ordering): now that this listener registers pre-connect, the
|
|
3123
|
+
// BOOT-path tool-result also flows through here while no renderer
|
|
3124
|
+
// is published yet — latching its key then would silently dedupe a
|
|
3125
|
+
// legitimate post-mount re-broadcast of the same envelope.
|
|
2897
3126
|
const apply = activeApplyRender;
|
|
2898
3127
|
if (apply === null)
|
|
2899
3128
|
return; // no renderer published yet — no-op safely
|
|
3129
|
+
lastMetaKey = key;
|
|
2900
3130
|
void buildGguiSessionSeedInput(meta).then((seed) => {
|
|
2901
3131
|
if (seed === null)
|
|
2902
3132
|
return;
|
|
@@ -2969,7 +3199,7 @@ function readLiveBootstrapShape() {
|
|
|
2969
3199
|
* re-await the same postMessage or re-parse the same global), saving
|
|
2970
3200
|
* up to the 30s postMessage timeout for spec-strict hosts.
|
|
2971
3201
|
*/
|
|
2972
|
-
function runBootProduction(preResolvedMeta) {
|
|
3202
|
+
function runBootProduction(preResolvedMeta, preResolvedLocator) {
|
|
2973
3203
|
const { app, transport } = createDefaultApp();
|
|
2974
3204
|
void bootProduction({
|
|
2975
3205
|
doc: document,
|
|
@@ -2985,6 +3215,7 @@ function runBootProduction(preResolvedMeta) {
|
|
|
2985
3215
|
onObserve: postObservabilityToParent,
|
|
2986
3216
|
onLifecycle: postLifecycleToParent,
|
|
2987
3217
|
...(preResolvedMeta !== undefined ? { preResolvedMeta } : {}),
|
|
3218
|
+
...(preResolvedLocator !== undefined ? { preResolvedLocator } : {}),
|
|
2988
3219
|
});
|
|
2989
3220
|
}
|
|
2990
3221
|
if (shouldAutostart() && typeof window !== 'undefined') {
|
|
@@ -3015,15 +3246,26 @@ if (shouldAutostart() && typeof window !== 'undefined') {
|
|
|
3015
3246
|
}
|
|
3016
3247
|
else {
|
|
3017
3248
|
const buffered = readPendingToolResults();
|
|
3249
|
+
const bufferedLocator = buffered === null ? readPendingToolResultLocator() : null;
|
|
3018
3250
|
if (buffered !== null) {
|
|
3019
3251
|
runBootProduction(buffered);
|
|
3020
3252
|
}
|
|
3253
|
+
else if (bufferedLocator !== null) {
|
|
3254
|
+
// Read-plane-only posture (ggui#537): the buffered result named
|
|
3255
|
+
// the view; the door resolves it right after the handshake.
|
|
3256
|
+
runBootProduction(undefined, bufferedLocator);
|
|
3257
|
+
}
|
|
3021
3258
|
else if (readLiveBootstrapShape()) {
|
|
3022
3259
|
runBootProduction();
|
|
3023
3260
|
}
|
|
3024
3261
|
else {
|
|
3025
|
-
void awaitToolResultMeta(POSTMESSAGE_BOOT_TIMEOUT_MS).then((
|
|
3026
|
-
|
|
3262
|
+
void awaitToolResultMeta(POSTMESSAGE_BOOT_TIMEOUT_MS).then((pre) => {
|
|
3263
|
+
if (pre === null)
|
|
3264
|
+
runBootProduction();
|
|
3265
|
+
else if (pre.kind === 'meta')
|
|
3266
|
+
runBootProduction(pre.meta);
|
|
3267
|
+
else
|
|
3268
|
+
runBootProduction(undefined, pre.locator);
|
|
3027
3269
|
});
|
|
3028
3270
|
}
|
|
3029
3271
|
}
|
|
@@ -3302,10 +3544,38 @@ async function bootProduction(opts) {
|
|
|
3302
3544
|
...(meta.gadgets !== undefined
|
|
3303
3545
|
? { gadgetPackages: meta.gadgets.map((g) => g.package) }
|
|
3304
3546
|
: {}),
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3547
|
+
// Strict-CSP module variant (ggui#522 slice 2) — attached to
|
|
3548
|
+
// the INLINE SEED mount only, detected by the absence of the
|
|
3549
|
+
// server-assigned ledger fields (`buildGguiSessionSeedInput`
|
|
3550
|
+
// omits them; every wire-delivered GguiSession carries them).
|
|
3551
|
+
// The variant URL names the SEEDED bytes: a WS-delivered
|
|
3552
|
+
// body may differ, and importing a stale variant would paint
|
|
3553
|
+
// the WRONG component rather than fail. A WS redelivery of
|
|
3554
|
+
// the SAME bytes loses nothing — same code ⇒ the renderer
|
|
3555
|
+
// skips re-evaluation entirely.
|
|
3556
|
+
...(meta.codeModuleUrl !== undefined &&
|
|
3557
|
+
render.type !== 'mcpApps' &&
|
|
3558
|
+
render.type !== 'system' &&
|
|
3559
|
+
!('createdAt' in render)
|
|
3560
|
+
? { codeModuleUrl: meta.codeModuleUrl }
|
|
3308
3561
|
: {}),
|
|
3562
|
+
// Base-theme ladder (ggui#589 ask 3 — see `resolveMountThemeId`):
|
|
3563
|
+
// stamped themeId > the slice theme object's NAME (a
|
|
3564
|
+
// registered name binds the full brand base; unregistered
|
|
3565
|
+
// names fall back to the default ladder, byte-identical to
|
|
3566
|
+
// the no-themeId path).
|
|
3567
|
+
...(() => {
|
|
3568
|
+
const themeId = resolveMountThemeId(meta);
|
|
3569
|
+
return themeId !== undefined ? { themeId } : {};
|
|
3570
|
+
})(),
|
|
3571
|
+
// Mode ladder (ggui#551 + #589 — see `resolveMountThemeMode`):
|
|
3572
|
+
// stamped `themeMode` > the slice theme OBJECT's `mode` >
|
|
3573
|
+
// host-announced theme. Absent everywhere means "no opinion",
|
|
3574
|
+
// not "light".
|
|
3575
|
+
...(() => {
|
|
3576
|
+
const themeMode = resolveMountThemeMode(meta);
|
|
3577
|
+
return themeMode !== undefined ? { themeMode } : {};
|
|
3578
|
+
})(),
|
|
3309
3579
|
// Per-app theme overlay (St3 M2.2). Threaded straight from the
|
|
3310
3580
|
// bootstrap's `_meta["ai.ggui/render"].theme` (typed `AppTheme`,
|
|
3311
3581
|
// already injection-validated by the wire parser) onto the mount
|
|
@@ -3313,6 +3583,15 @@ async function bootProduction(opts) {
|
|
|
3313
3583
|
// `color-scheme` at `:root`. THEME IS COMPONENT-ONLY — system
|
|
3314
3584
|
// cards theme via the SystemCardHost/ThemeProvider path.
|
|
3315
3585
|
...(meta.theme !== undefined ? { appTheme: meta.theme } : {}),
|
|
3586
|
+
// Host palette: always threaded when the host announced one —
|
|
3587
|
+
// unlike mode there is no either/or gate here, because the
|
|
3588
|
+
// precedence is CSS document order inside the scoped block
|
|
3589
|
+
// (base < hostPalette < appTheme, ggui#572/#573: slice wins,
|
|
3590
|
+
// host fallback), not a value-level ?? fallback.
|
|
3591
|
+
...(() => {
|
|
3592
|
+
const hostPalette = hostAnnouncedPalette();
|
|
3593
|
+
return hostPalette !== undefined ? { hostPalette } : {};
|
|
3594
|
+
})(),
|
|
3316
3595
|
...(wrapOuter !== undefined ? { wrapOuter } : {}),
|
|
3317
3596
|
};
|
|
3318
3597
|
};
|
|
@@ -3336,17 +3615,17 @@ async function bootProduction(opts) {
|
|
|
3336
3615
|
}
|
|
3337
3616
|
await renderHandle.update(buildOpts(render));
|
|
3338
3617
|
};
|
|
3339
|
-
// Publish `applyRender` module-level
|
|
3340
|
-
//
|
|
3341
|
-
// (
|
|
3342
|
-
//
|
|
3343
|
-
//
|
|
3344
|
-
//
|
|
3345
|
-
//
|
|
3346
|
-
//
|
|
3347
|
-
//
|
|
3618
|
+
// Publish `applyRender` module-level. The persistent tool-result
|
|
3619
|
+
// listener is ALREADY registered — `bootSequence` installs it
|
|
3620
|
+
// pre-connect (ggui#586) — and reads this slot dynamically, so
|
|
3621
|
+
// publishing here is what arms it. On a host-re-emitted
|
|
3622
|
+
// tool-result (claude.ai re-broadcast; the
|
|
3623
|
+
// Anthropic-SDK-strips-_meta refetch), the listener re-mounts
|
|
3624
|
+
// through THIS `applyRender` — no fresh boot, no second WS. The
|
|
3625
|
+
// no-WS live-re-render channel; for WS hosts the render-frame
|
|
3626
|
+
// handler already covers re-render, so it's a redundant-safe
|
|
3627
|
+
// fallback (guarded by the sessionId pin + liveTrio dedupe).
|
|
3348
3628
|
activeApplyRender = applyRender;
|
|
3349
|
-
installPostMountListener();
|
|
3350
3629
|
// Validator context — A2UI default for `_ggui:preview`; no
|
|
3351
3630
|
// bootstrap-supplied overrides today (the
|
|
3352
3631
|
// `extraReservedValidators` injection slot is reserved for a
|
|
@@ -3493,5 +3772,8 @@ async function bootProduction(opts) {
|
|
|
3493
3772
|
...(opts.preResolvedMeta !== undefined
|
|
3494
3773
|
? { preResolvedMeta: opts.preResolvedMeta }
|
|
3495
3774
|
: {}),
|
|
3775
|
+
...(opts.preResolvedLocator !== undefined
|
|
3776
|
+
? { preResolvedLocator: opts.preResolvedLocator }
|
|
3777
|
+
: {}),
|
|
3496
3778
|
});
|
|
3497
3779
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -34,3 +34,22 @@ export declare const RUNTIME_DIST_DIR: string;
|
|
|
34
34
|
* `bootstrap.runtimeUrl`, never hardcodes it.
|
|
35
35
|
*/
|
|
36
36
|
export declare const RUNTIME_BUNDLE_URL_PATH: "/_ggui/iframe-runtime.js";
|
|
37
|
+
/**
|
|
38
|
+
* Directory of the static shim modules (`dist/shims/<name>.js`) the
|
|
39
|
+
* build emits alongside the bundle (ggui#522 slice 2 — `asset-url`
|
|
40
|
+
* delivery). The mcp-server captures these files at composition and
|
|
41
|
+
* serves them under `/_ggui/shims/<runtime-hash>/<name>.js` with the
|
|
42
|
+
* same immutable posture as the hashed bundle route: the shims ship in
|
|
43
|
+
* the same dist as the bundle, so the bundle's content hash versions
|
|
44
|
+
* both — a shim-affecting change in the design package also changes
|
|
45
|
+
* the bundle bytes.
|
|
46
|
+
*/
|
|
47
|
+
export declare const RUNTIME_SHIMS_DIR: string;
|
|
48
|
+
/**
|
|
49
|
+
* The URL prefix under which the mcp-server publishes the shim
|
|
50
|
+
* directory family. The full shim directory for a given build is
|
|
51
|
+
* `<origin>${RUNTIME_SHIMS_URL_PREFIX}/<runtime-hash>` — the server-
|
|
52
|
+
* side `asset-url` rewrite composes shim URLs on it, and the shim
|
|
53
|
+
* route parses it back.
|
|
54
|
+
*/
|
|
55
|
+
export declare const RUNTIME_SHIMS_URL_PREFIX: "/_ggui/shims";
|
package/dist/server.js
CHANGED
|
@@ -70,3 +70,22 @@ export const RUNTIME_DIST_DIR = path.dirname(RUNTIME_BUNDLE_FILE);
|
|
|
70
70
|
* `bootstrap.runtimeUrl`, never hardcodes it.
|
|
71
71
|
*/
|
|
72
72
|
export const RUNTIME_BUNDLE_URL_PATH = '/_ggui/iframe-runtime.js';
|
|
73
|
+
/**
|
|
74
|
+
* Directory of the static shim modules (`dist/shims/<name>.js`) the
|
|
75
|
+
* build emits alongside the bundle (ggui#522 slice 2 — `asset-url`
|
|
76
|
+
* delivery). The mcp-server captures these files at composition and
|
|
77
|
+
* serves them under `/_ggui/shims/<runtime-hash>/<name>.js` with the
|
|
78
|
+
* same immutable posture as the hashed bundle route: the shims ship in
|
|
79
|
+
* the same dist as the bundle, so the bundle's content hash versions
|
|
80
|
+
* both — a shim-affecting change in the design package also changes
|
|
81
|
+
* the bundle bytes.
|
|
82
|
+
*/
|
|
83
|
+
export const RUNTIME_SHIMS_DIR = path.resolve(here, 'shims');
|
|
84
|
+
/**
|
|
85
|
+
* The URL prefix under which the mcp-server publishes the shim
|
|
86
|
+
* directory family. The full shim directory for a given build is
|
|
87
|
+
* `<origin>${RUNTIME_SHIMS_URL_PREFIX}/<runtime-hash>` — the server-
|
|
88
|
+
* side `asset-url` rewrite composes shim URLs on it, and the shim
|
|
89
|
+
* route parses it back.
|
|
90
|
+
*/
|
|
91
|
+
export const RUNTIME_SHIMS_URL_PREFIX = '/_ggui/shims';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const M = (globalThis.__ggui__&&globalThis.__ggui__["appComponents"]||window["__GGUI_APP_COMPONENTS"]) || {}; const handler = { get(_, key) { return M[key]; } }; export default new Proxy({}, handler);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const M = Object.assign({}, (globalThis.__ggui__&&globalThis.__ggui__["components"]||window["__GGUI_COMPONENTS"]||{}),(globalThis.__ggui__&&globalThis.__ggui__["primitives"]||window["__GGUI_PRIMITIVES"]||{}),(globalThis.__ggui__&&globalThis.__ggui__["compositions"]||window["__GGUI_COMPOSITIONS"]||{}),(globalThis.__ggui__&&globalThis.__ggui__["interact"]||window["__GGUI_INTERACT"]||{})); export const Container = M["Container"]; export const Card = M["Card"]; export const Stack = M["Stack"]; export const Row = M["Row"]; export const Grid = M["Grid"]; export const Box = M["Box"]; export const Divider = M["Divider"]; export const Spacer = M["Spacer"]; export const Text = M["Text"]; export const Heading = M["Heading"]; export const Button = M["Button"]; export const Input = M["Input"]; export const TextArea = M["TextArea"]; export const Select = M["Select"]; export const Checkbox = M["Checkbox"]; export const Toggle = M["Toggle"]; export const RadioGroup = M["RadioGroup"]; export const Slider = M["Slider"]; export const Badge = M["Badge"]; export const Spinner = M["Spinner"]; export const Skeleton = M["Skeleton"]; export const Avatar = M["Avatar"]; export const Alert = M["Alert"]; export const Progress = M["Progress"]; export const Image = M["Image"]; export const Icon = M["Icon"]; export const Link = M["Link"]; export const Tooltip = M["Tooltip"]; export const Table = M["Table"]; export const Tabs = M["Tabs"]; export const Toast = M["Toast"]; export const Accordion = M["Accordion"]; export const MotionKeyframes = M["MotionKeyframes"]; export const useMotion = M["useMotion"]; export const useAnimationKey = M["useAnimationKey"]; export const SearchField = M["SearchField"]; export const FormField = M["FormField"]; export const MenuItem = M["MenuItem"]; export const Tag = M["Tag"]; export const Dropdown = M["Dropdown"]; export const Autocomplete = M["Autocomplete"]; export const Breadcrumb = M["Breadcrumb"]; export const Pagination = M["Pagination"]; export const EmptyState = M["EmptyState"]; export const Stat = M["Stat"]; export const Stepper = M["Stepper"]; export const Markdown = M["Markdown"]; export const MarkdownInline = M["MarkdownInline"]; export const renderRichTextInlines = M["renderRichTextInlines"]; export const Header = M["Header"]; export const Sidebar = M["Sidebar"]; export const CardGrid = M["CardGrid"]; export const CommentThread = M["CommentThread"]; export const DataTable = M["DataTable"]; export const ChatWindow = M["ChatWindow"]; export const NavigationBar = M["NavigationBar"]; export const FileUploader = M["FileUploader"]; export const NotificationCenter = M["NotificationCenter"]; export const Modal = M["Modal"]; export const CommandPalette = M["CommandPalette"]; export const Footer = M["Footer"]; export const Hero = M["Hero"]; export const Clickable = M["Clickable"]; export const Hoverable = M["Hoverable"]; export const Pressable = M["Pressable"]; export default M;
|