@ggui-ai/iframe-runtime 0.15.0 → 0.16.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/channel-transport.d.ts +2 -2
- package/dist/connection.d.ts +34 -12
- package/dist/connection.js +40 -12
- package/dist/host-context-emitter.d.ts +22 -1
- package/dist/host-context-emitter.js +37 -4
- package/dist/host-fonts.d.ts +10 -0
- package/dist/host-fonts.js +113 -0
- package/dist/host-palette-bridge.js +20 -15
- package/dist/iframe-runtime.js +67 -78
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/mcp-app-iframe-host.d.ts +15 -0
- package/dist/mcp-app-iframe-host.js +5 -1
- package/dist/meta-parse.d.ts +9 -0
- package/dist/meta-parse.js +14 -2
- package/dist/observability.d.ts +41 -1
- package/dist/react-renderer.d.ts +18 -36
- package/dist/react-renderer.js +64 -72
- package/dist/render-item.d.ts +6 -13
- package/dist/runtime.d.ts +23 -39
- package/dist/runtime.js +99 -68
- package/dist/system-cards/NoCredentialsCard.js +13 -13
- package/dist/system-cards/ProtocolProbeCard.js +11 -11
- package/dist/system-cards/QrCode.js +1 -1
- package/dist/system-cards/index.js +2 -2
- package/dist/wire-config.d.ts +2 -2
- package/package.json +7 -7
package/dist/runtime.js
CHANGED
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
// FIRST import on purpose — declares zod jitless before any
|
|
28
28
|
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
29
|
import './zod-jitless.js';
|
|
30
|
-
import {
|
|
30
|
+
import { runtimeConnectionWriter } from './connection.js';
|
|
31
31
|
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
32
32
|
import { effectiveThemeId, effectiveThemeMode, } from '@ggui-ai/protocol/integrations/theme-binding';
|
|
33
|
-
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
33
|
+
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, reportInvalidAppTheme, } from './meta-parse.js';
|
|
34
34
|
import { projectHostContext } from '@ggui-ai/protocol/wire';
|
|
35
35
|
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
|
|
36
|
-
import { applyHostContextStyling, attachListener as attachHostContextListener, seed as seedHostContext, } from './host-context-emitter.js';
|
|
36
|
+
import { applyHostContextStyling, attachListener as attachHostContextListener, seed as seedHostContext, subscribeThemeChange as subscribeHostThemeChange, } from './host-context-emitter.js';
|
|
37
37
|
import { mapHostPaletteToGguiVars } from './host-palette-bridge.js';
|
|
38
38
|
import { buildRootWireConfig, StreamBus, } from './wire-config.js';
|
|
39
39
|
import { createChannelTransportRouter, } from './channel-transport.js';
|
|
@@ -171,6 +171,11 @@ export function setCurrentApp(app) {
|
|
|
171
171
|
function getCurrentApp() {
|
|
172
172
|
return currentApp;
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The live theme re-injection's subscription (ggui#987 §4), held so a
|
|
176
|
+
* re-entered boot replaces it instead of stacking a second subscriber.
|
|
177
|
+
*/
|
|
178
|
+
let activeThemeUnsubscribe = null;
|
|
174
179
|
/**
|
|
175
180
|
* The host's announced color mode, read from the connected App's
|
|
176
181
|
* pre-merged hostContext (`ui/initialize` result, kept current by the
|
|
@@ -185,10 +190,11 @@ function getCurrentApp() {
|
|
|
185
190
|
* `color-scheme` via `applyDocumentTheme` — nothing in the token
|
|
186
191
|
* pipeline keys off it, so a dark host painted the light ladder
|
|
187
192
|
* (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
193
|
+
* Under ggui#987 §4 the embedding host OWNS runtime mode: when it
|
|
194
|
+
* announces one, `buildOpts` paints it over every stamped opinion; the
|
|
195
|
+
* stamped layers decide only when the host is silent. Read live from
|
|
196
|
+
* the App's pre-merged hostContext, so a `hostcontextchanged` flip
|
|
197
|
+
* re-resolves on the next `buildOpts`.
|
|
192
198
|
*
|
|
193
199
|
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
194
200
|
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
@@ -198,36 +204,30 @@ export function hostAnnouncedThemeMode() {
|
|
|
198
204
|
return theme === 'light' || theme === 'dark' ? theme : undefined;
|
|
199
205
|
}
|
|
200
206
|
/**
|
|
201
|
-
* Resolve the mount's
|
|
202
|
-
* the
|
|
203
|
-
* precedence ruling — "slice wins, host is the fallback, absent ≠
|
|
204
|
-
* light"):
|
|
207
|
+
* Resolve the mount's color mode (ggui#987 §4 — the embedding host owns
|
|
208
|
+
* runtime mode; the app theme's `mode` is a DEFAULT, never a pin):
|
|
205
209
|
*
|
|
206
|
-
* stamped `themeMode` > `theme.mode`
|
|
207
|
-
*
|
|
210
|
+
* host-announced theme > stamped `themeMode` > `theme.mode`
|
|
211
|
+
* (the app theme's default) > undefined
|
|
208
212
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* `hostContext.theme`: a light skeleton in a dark skin. The theme
|
|
216
|
-
* object's own `mode` already drove `color-scheme`; now it also
|
|
217
|
-
* selects the ladder.
|
|
213
|
+
* A card that ignored the host's announced mode painted a light
|
|
214
|
+
* skeleton inside a dark chat — the host's word is what the user
|
|
215
|
+
* actually sees around the card. The stamped layers (live provider >
|
|
216
|
+
* per-render override > app default > server default) decide only when
|
|
217
|
+
* the host is silent, which is the adapter-boundary case (a host
|
|
218
|
+
* helper that announces no `hostContext.theme`).
|
|
218
219
|
*
|
|
219
220
|
* `undefined` still means "no opinion anywhere" — the renderer's
|
|
220
|
-
* default applies; never coerced to `'light'
|
|
221
|
+
* default applies; never coerced to `'light'` here (ggui#551).
|
|
221
222
|
*
|
|
222
223
|
* @internal — exported for unit tests; production caller is
|
|
223
224
|
* `buildOpts` inside `bootSequence`.
|
|
224
225
|
*/
|
|
225
226
|
export function resolveMountThemeMode(meta) {
|
|
226
227
|
// The CLIENT projection of the protocol's normative theme-binding
|
|
227
|
-
// total order (@ggui-ai/protocol/integrations/theme-binding
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
// without rank drift.
|
|
228
|
+
// total order (@ggui-ai/protocol/integrations/theme-binding) — the
|
|
229
|
+
// server stamps via the sibling projection and the protocol's
|
|
230
|
+
// composition-law test pins that the two compose without rank drift.
|
|
231
231
|
return effectiveThemeMode({
|
|
232
232
|
stamped: meta.themeMode,
|
|
233
233
|
sessionSidecar: meta.theme?.mode,
|
|
@@ -235,20 +235,14 @@ export function resolveMountThemeMode(meta) {
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
/**
|
|
238
|
-
* Resolve the mount's
|
|
238
|
+
* Resolve the mount's compiled-ladder id from the slice (ggui#987 D2):
|
|
239
239
|
*
|
|
240
|
-
* stamped `themeId` >
|
|
241
|
-
* > undefined (renderer default ladder)
|
|
240
|
+
* stamped `themeId` > undefined (renderer default ladder)
|
|
242
241
|
*
|
|
243
|
-
* The name
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* vars carry brand" store-frame class). An unregistered name is
|
|
248
|
-
* harmless by construction: `getScopedThemeCss` falls back to the
|
|
249
|
-
* default theme, which is byte-identical to the no-themeId path
|
|
250
|
-
* (`getScopedCssTokens` ≡ `getScopedThemeCss(default)`), so a slice
|
|
251
|
-
* naming its theme "My Custom" renders exactly as before.
|
|
242
|
+
* The app theme's `name` is a LABEL the author's tooling reads back —
|
|
243
|
+
* it never selects a ladder. An app theme carries its own full
|
|
244
|
+
* projection (`overlays`, exactly the manifest minus the floor), so
|
|
245
|
+
* there is no coverage gap for a compiled theme to fill beneath it.
|
|
252
246
|
*
|
|
253
247
|
* @internal — exported for unit tests; production caller is
|
|
254
248
|
* `buildOpts` inside `bootSequence`.
|
|
@@ -256,10 +250,7 @@ export function resolveMountThemeMode(meta) {
|
|
|
256
250
|
export function resolveMountThemeId(meta) {
|
|
257
251
|
// Client projection of the normative theme-binding order — see
|
|
258
252
|
// resolveMountThemeMode above.
|
|
259
|
-
return effectiveThemeId({
|
|
260
|
-
stamped: meta.themeId,
|
|
261
|
-
sidecarName: meta.theme?.name,
|
|
262
|
-
});
|
|
253
|
+
return effectiveThemeId({ stamped: meta.themeId });
|
|
263
254
|
}
|
|
264
255
|
/**
|
|
265
256
|
* The host's announced palette, read from the connected App's
|
|
@@ -461,7 +452,7 @@ export async function bootSequence(opts) {
|
|
|
461
452
|
// `tools/call` (dispatchSubmitAction, channel-transport router)
|
|
462
453
|
// routes through `app.callServerTool` instead of the legacy raw
|
|
463
454
|
// postMessage pump. Idempotent: re-boot with the same App is a
|
|
464
|
-
// no-op; a different App throws (the iframe is single-
|
|
455
|
+
// no-op; a different App throws (the iframe is single-app).
|
|
465
456
|
setCurrentApp(app);
|
|
466
457
|
// Slice-meta resolution — spec-canonical primary, no in-house
|
|
467
458
|
// Reading-B (retired Phase 1.19b.3 with the App-class swap; the
|
|
@@ -1356,7 +1347,7 @@ export async function resolveMetaViaReadDoor(app, locator) {
|
|
|
1356
1347
|
const envelope = readGguiShellEnvelope(text);
|
|
1357
1348
|
if (envelope === undefined)
|
|
1358
1349
|
return null;
|
|
1359
|
-
const parsed = parseMcpAppAiGguiRenderMeta(envelope);
|
|
1350
|
+
const parsed = parseMcpAppAiGguiRenderMeta(envelope, { onInvalidTheme: reportInvalidAppTheme });
|
|
1360
1351
|
if (!parsed.ok || parsed.meta === undefined)
|
|
1361
1352
|
return null;
|
|
1362
1353
|
const validated = validateMeta(parsed.meta);
|
|
@@ -1406,7 +1397,7 @@ function awaitToolResultMeta(timeoutMs) {
|
|
|
1406
1397
|
* App instance even across re-mounts (e.g. an agent fires a second
|
|
1407
1398
|
* `ggui_render` and the listener re-applies through the published
|
|
1408
1399
|
* `applyRender`). Keyed by App — not a module boolean — so a fresh App
|
|
1409
|
-
* (a re-booted document in tests; production is single-
|
|
1400
|
+
* (a re-booted document in tests; production is single-app per
|
|
1410
1401
|
* iframe) always gets its own registration, never a stale latch.
|
|
1411
1402
|
*/
|
|
1412
1403
|
const persistentToolResultListenerApps = new WeakSet();
|
|
@@ -1883,7 +1874,7 @@ function applyFreezeCue(root) {
|
|
|
1883
1874
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
1884
1875
|
fontSize: '11px',
|
|
1885
1876
|
fontWeight: '600',
|
|
1886
|
-
color: 'var(--ggui-color-
|
|
1877
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
1887
1878
|
background: 'rgba(128,128,128,0.16)',
|
|
1888
1879
|
border: '1px solid currentColor',
|
|
1889
1880
|
opacity: '0.85',
|
|
@@ -2320,7 +2311,7 @@ let relayLatchTrigger;
|
|
|
2320
2311
|
let relayLatchScope = {};
|
|
2321
2312
|
function transitionRelayLatch(next, trigger, scope = {}) {
|
|
2322
2313
|
relayIncapabilityAnnounced = next;
|
|
2323
|
-
|
|
2314
|
+
runtimeConnectionWriter().set(!next);
|
|
2324
2315
|
if (next && trigger !== undefined) {
|
|
2325
2316
|
relayLatchedAt = Date.now();
|
|
2326
2317
|
relayDeadTaps = 0;
|
|
@@ -2435,6 +2426,10 @@ function isRelayNoticeVisible() {
|
|
|
2435
2426
|
* @internal — exported for unit tests.
|
|
2436
2427
|
*/
|
|
2437
2428
|
export function resetRelayLatchForBoot() {
|
|
2429
|
+
// The runtime's claim on the document's connection writer is taken HERE,
|
|
2430
|
+
// eagerly, before any transition (ggui#843): a second runtime in this
|
|
2431
|
+
// document fails at boot naming itself, never continuing unclaimed.
|
|
2432
|
+
runtimeConnectionWriter();
|
|
2438
2433
|
if (typeof document !== 'undefined') {
|
|
2439
2434
|
const el = document.getElementById(ACTION_TOAST_ID);
|
|
2440
2435
|
if (el !== null && el.hasAttribute(RELAY_NOTICE_ATTR))
|
|
@@ -2445,14 +2440,14 @@ export function resetRelayLatchForBoot() {
|
|
|
2445
2440
|
transitionRelayLatch(false);
|
|
2446
2441
|
}
|
|
2447
2442
|
else {
|
|
2448
|
-
|
|
2443
|
+
runtimeConnectionWriter().set(true);
|
|
2449
2444
|
}
|
|
2450
2445
|
}
|
|
2451
2446
|
/**
|
|
2452
2447
|
* Module-level handle to the CURRENT boot's telemetry sink so
|
|
2453
2448
|
* module-scope paths (the doorbell emitter below) can report without
|
|
2454
2449
|
* threading the sink through every call chain. Per-iframe
|
|
2455
|
-
* single-
|
|
2450
|
+
* single-app scope makes a single slot correct — same justification as
|
|
2456
2451
|
* `getCurrentApp()`.
|
|
2457
2452
|
*/
|
|
2458
2453
|
let currentTelemetrySink = null;
|
|
@@ -2540,7 +2535,7 @@ function resetRelayCueThrottles() {
|
|
|
2540
2535
|
/** @internal — exported for unit tests to reset module state. */
|
|
2541
2536
|
export function __resetRelayNoticeForTest() {
|
|
2542
2537
|
relayIncapabilityAnnounced = false;
|
|
2543
|
-
|
|
2538
|
+
runtimeConnectionWriter().set(true);
|
|
2544
2539
|
relayLatchedAt = undefined;
|
|
2545
2540
|
relayDeadTaps = 0;
|
|
2546
2541
|
relayLatchTrigger = undefined;
|
|
@@ -3712,10 +3707,10 @@ async function bootProduction(opts) {
|
|
|
3712
3707
|
// DEFAULT-handlers compositions only (a custom `handlers` list
|
|
3713
3708
|
// drains its own pipe; bridging the channel onto an instance
|
|
3714
3709
|
// nobody drains would buffer gestures into the void — see the
|
|
3715
|
-
// hoist note in server.ts).
|
|
3716
|
-
// caller: on it, a WS gesture lands on the retained ledger only
|
|
3710
|
+
// hoist note in server.ts). A deployment composing its own `handlers`
|
|
3711
|
+
// IS such a caller: on it, a WS gesture lands on the retained ledger only
|
|
3717
3712
|
// and never reaches the agent. Any WS action fallback (ggui#599's
|
|
3718
|
-
// open design leg) therefore requires the server
|
|
3713
|
+
// open design leg) therefore requires the server-side bridge
|
|
3719
3714
|
// FIRST — it is a capability gap on that composition today. The WS pipe otherwise carries
|
|
3720
3715
|
// streamSpec subscriptions (inbound `ggui_emit` fanout +
|
|
3721
3716
|
// `props_update` + `render` + `data` + `drain_ack` +
|
|
@@ -3864,29 +3859,27 @@ async function bootProduction(opts) {
|
|
|
3864
3859
|
!('createdAt' in render)
|
|
3865
3860
|
? { codeModuleUrl: meta.codeModuleUrl }
|
|
3866
3861
|
: {}),
|
|
3867
|
-
//
|
|
3868
|
-
//
|
|
3869
|
-
// registered name binds the full brand base; unregistered
|
|
3870
|
-
// names fall back to the default ladder, byte-identical to
|
|
3871
|
-
// the no-themeId path).
|
|
3862
|
+
// Compiled ladder (see `resolveMountThemeId`): the stamped
|
|
3863
|
+
// themeId alone; absent = the renderer's default ladder.
|
|
3872
3864
|
...(() => {
|
|
3873
3865
|
const themeId = resolveMountThemeId(meta);
|
|
3874
3866
|
return themeId !== undefined ? { themeId } : {};
|
|
3875
3867
|
})(),
|
|
3876
|
-
// Mode
|
|
3877
|
-
// stamped `themeMode` > the
|
|
3878
|
-
//
|
|
3879
|
-
//
|
|
3868
|
+
// Mode (ggui#987 §4 — see `resolveMountThemeMode`): host-
|
|
3869
|
+
// announced theme > stamped `themeMode` > the app theme's
|
|
3870
|
+
// default `mode`. Absent everywhere means "no opinion", not
|
|
3871
|
+
// "light". Re-resolved on every buildOpts, so the live
|
|
3872
|
+
// hostcontextchanged re-injection below picks up a flip.
|
|
3880
3873
|
...(() => {
|
|
3881
3874
|
const themeMode = resolveMountThemeMode(meta);
|
|
3882
3875
|
return themeMode !== undefined ? { themeMode } : {};
|
|
3883
3876
|
})(),
|
|
3884
|
-
// Per-app theme
|
|
3877
|
+
// Per-app theme (ggui#987 v2). Threaded straight from the
|
|
3885
3878
|
// bootstrap's `_meta["ai.ggui/render"].theme` (typed `AppTheme`,
|
|
3886
3879
|
// already injection-validated by the wire parser) onto the mount
|
|
3887
|
-
// options
|
|
3888
|
-
//
|
|
3889
|
-
// cards theme via the SystemCardHost/ThemeProvider path.
|
|
3880
|
+
// options; the renderer injects the effective mode's overlay
|
|
3881
|
+
// into the scoped block + `:root`. THEME IS COMPONENT-ONLY —
|
|
3882
|
+
// system cards theme via the SystemCardHost/ThemeProvider path.
|
|
3890
3883
|
...(meta.theme !== undefined ? { appTheme: meta.theme } : {}),
|
|
3891
3884
|
// Host palette: always threaded when the host announced one —
|
|
3892
3885
|
// unlike mode there is no either/or gate here, because the
|
|
@@ -3912,14 +3905,52 @@ async function bootProduction(opts) {
|
|
|
3912
3905
|
* layer just calls `applyRender(render)` without owning
|
|
3913
3906
|
* lifecycle state.
|
|
3914
3907
|
*/
|
|
3908
|
+
// Every in-place update of the mounted slot goes through ONE
|
|
3909
|
+
// chain: the WS render-frame / props_update path and the live
|
|
3910
|
+
// theme re-injection below are independent callers, and the
|
|
3911
|
+
// handle's `update` has no lock of its own — serializing them
|
|
3912
|
+
// here is what keeps a host flip landing mid-render from
|
|
3913
|
+
// interleaving two updates.
|
|
3914
|
+
let updateChain = Promise.resolve();
|
|
3915
|
+
const queueUpdate = (opts) => {
|
|
3916
|
+
const run = updateChain.then(async () => {
|
|
3917
|
+
if (renderHandle === null)
|
|
3918
|
+
return;
|
|
3919
|
+
await renderHandle.update(opts());
|
|
3920
|
+
});
|
|
3921
|
+
updateChain = run.catch(() => undefined);
|
|
3922
|
+
return run;
|
|
3923
|
+
};
|
|
3915
3924
|
const applyRender = async (render) => {
|
|
3916
3925
|
currentRender = render;
|
|
3917
3926
|
if (renderHandle === null) {
|
|
3918
3927
|
renderHandle = await mountRender(renderInto, buildOpts(render));
|
|
3919
3928
|
return;
|
|
3920
3929
|
}
|
|
3921
|
-
await
|
|
3930
|
+
await queueUpdate(() => buildOpts(render));
|
|
3922
3931
|
};
|
|
3932
|
+
// Live re-injection (ggui#987 §4): the host owns runtime mode and
|
|
3933
|
+
// may flip it mid-session. The trigger is the emitter's THEME
|
|
3934
|
+
// subscription — the raw `theme` / `styles` payload — because the
|
|
3935
|
+
// host-context projection excludes both by design and its
|
|
3936
|
+
// equality gate would never fire for a theme-only flip. The App
|
|
3937
|
+
// pre-merges the new hostContext before the emitter fans out, so
|
|
3938
|
+
// re-running `buildOpts` on the CURRENT render re-resolves mode +
|
|
3939
|
+
// palette and the renderer repaints from the retained other
|
|
3940
|
+
// overlay — no fetch, no remount. A rejection is reported to the
|
|
3941
|
+
// embedding host, never dropped on the floor.
|
|
3942
|
+
activeThemeUnsubscribe?.();
|
|
3943
|
+
activeThemeUnsubscribe = subscribeHostThemeChange(() => {
|
|
3944
|
+
if (renderHandle === null || currentRender === null)
|
|
3945
|
+
return;
|
|
3946
|
+
const render = currentRender;
|
|
3947
|
+
queueUpdate(() => buildOpts(render)).catch((err) => {
|
|
3948
|
+
postObservabilityToParent({
|
|
3949
|
+
kind: 'theme-reinject-failed',
|
|
3950
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3951
|
+
});
|
|
3952
|
+
});
|
|
3953
|
+
});
|
|
3923
3954
|
// Publish `applyRender` module-level. The persistent tool-result
|
|
3924
3955
|
// listener is ALREADY registered — `bootSequence` installs it
|
|
3925
3956
|
// pre-connect (ggui#586) — and reads this slot dynamically, so
|
|
@@ -36,7 +36,7 @@ function GguiWordmark() {
|
|
|
36
36
|
// off-white in dark). `gray-300` flips too — the gray scale in
|
|
37
37
|
// `darkTheme` is intentionally inverted, so the wordmark's
|
|
38
38
|
// contrasting "chrome" tone reads correctly under either theme.
|
|
39
|
-
const ink = 'var(--ggui-color-
|
|
39
|
+
const ink = 'var(--ggui-color-onContainer, #292929)';
|
|
40
40
|
const chrome = 'var(--ggui-color-neutral-300, #d1d5db)';
|
|
41
41
|
return (_jsxs("svg", { viewBox: "0 0 224 50", width: "72", height: "16", role: "img", "aria-label": "ggui", style: { display: 'block', flexShrink: 0 }, children: [_jsx("path", { d: "M 0 0 H 50 V 25 H 25 V 50 H 0 Z", fill: chrome }), _jsx("rect", { x: "33", y: "33", width: "17", height: "17", fill: ink }), _jsx("path", { d: "M 58 0 H 108 V 25 H 83 V 50 H 58 Z", fill: ink }), _jsx("rect", { x: "91", y: "33", width: "17", height: "17", fill: chrome }), _jsx("path", { d: "M 141 50 C 154.807 50 166 38.8071 166 25 V 0 H 116 V 25 C 116 38.8071 127.193 50 141 50 Z", fill: ink }), _jsx("rect", { x: "174", y: "0", width: "50", height: "50", fill: chrome })] }));
|
|
42
42
|
}
|
|
@@ -87,10 +87,10 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
87
87
|
// INVERTED in darkTheme so `gray-100` is light-on-dark when in
|
|
88
88
|
// dark mode and dark-on-light when in light mode).
|
|
89
89
|
//
|
|
90
|
-
// - `--ggui-color-
|
|
90
|
+
// - `--ggui-color-container` — opaque card surface (white in light,
|
|
91
91
|
// slate-800 in dark). Used for the URL block + QR backdrop.
|
|
92
|
-
// - `--ggui-color-
|
|
93
|
-
// - `--ggui-color-
|
|
92
|
+
// - `--ggui-color-onContainer` — high-contrast body text.
|
|
93
|
+
// - `--ggui-color-onSunken` — muted captions/headings.
|
|
94
94
|
// - `--ggui-color-neutral-100` — subtle tinted background that
|
|
95
95
|
// auto-flips. Used for the inner "details" panel — slightly
|
|
96
96
|
// differentiated from outer transparent canvas without being
|
|
@@ -103,7 +103,7 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
103
103
|
// background creates a card-on-card look. Padding stays so content
|
|
104
104
|
// doesn't crowd the iframe edges. Outside Claude (preview iframe,
|
|
105
105
|
// console inspector), the ThemeProvider's body background is the
|
|
106
|
-
// theme's `--ggui-color-
|
|
106
|
+
// theme's `--ggui-color-container`, so a transparent card just
|
|
107
107
|
// shows the theme's canvas behind it — also fine.
|
|
108
108
|
_jsx(Card, { padding: "lg", border: false, shadow: "none", style: { backgroundColor: 'transparent' }, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "md", children: [_jsxs("div", { style: {
|
|
109
109
|
display: 'flex',
|
|
@@ -118,11 +118,11 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
118
118
|
}, children: [_jsx(Heading, { level: 3, children: "Connect an LLM provider" }), _jsx("code", { style: {
|
|
119
119
|
fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
120
120
|
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
121
|
-
color: 'var(--ggui-color-
|
|
121
|
+
color: 'var(--ggui-color-onSunken, #71717a)',
|
|
122
122
|
opacity: 0.7,
|
|
123
123
|
userSelect: 'all',
|
|
124
124
|
}, title: "iframe-runtime build identifier \u2014 useful for diagnosing stale caches", children: BUILD_ID })] }), _jsx(Text, { style: {
|
|
125
|
-
color: 'var(--ggui-color-
|
|
125
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
126
126
|
lineHeight: '1.55',
|
|
127
127
|
}, children: "ggui needs an LLM key to render UIs. Open this server\u2019s settings page to paste an Anthropic, OpenAI, Google, or OpenRouter API key." })] }), _jsx("div", { style: {
|
|
128
128
|
display: 'flex',
|
|
@@ -148,7 +148,7 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
148
148
|
borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
|
|
149
149
|
lineHeight: 0,
|
|
150
150
|
}, children: _jsx(QrCode, { value: settingsUrl, size: 120, foreground: "#000000", background: "#ffffff" }) }), _jsx(Text, { size: "xs", style: {
|
|
151
|
-
color: 'var(--ggui-color-
|
|
151
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
152
152
|
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
153
153
|
textAlign: 'center',
|
|
154
154
|
}, children: "Scan with your phone" })] }), _jsxs("div", { style: {
|
|
@@ -158,7 +158,7 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
158
158
|
flex: '1 1 240px',
|
|
159
159
|
minWidth: '200px',
|
|
160
160
|
}, children: [_jsx(Text, { size: "xs", style: {
|
|
161
|
-
color: 'var(--ggui-color-
|
|
161
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
162
162
|
fontWeight: 'var(--ggui-font-weight-semibold, 600)',
|
|
163
163
|
textTransform: 'uppercase',
|
|
164
164
|
letterSpacing: '0.06em',
|
|
@@ -166,11 +166,11 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
166
166
|
}, children: "Or open this URL in your browser" }), _jsx("div", { style: {
|
|
167
167
|
fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
168
168
|
fontSize: 'var(--ggui-font-size-sm, 13px)',
|
|
169
|
-
color: 'var(--ggui-color-
|
|
169
|
+
color: 'var(--ggui-color-onContainer, #111)',
|
|
170
170
|
wordBreak: 'break-all',
|
|
171
171
|
userSelect: 'all',
|
|
172
172
|
padding: 'var(--ggui-spacing-3, 12px)',
|
|
173
|
-
background: 'var(--ggui-color-
|
|
173
|
+
background: 'var(--ggui-color-container, #fff)',
|
|
174
174
|
border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
|
|
175
175
|
borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
|
|
176
176
|
lineHeight: '1.45',
|
|
@@ -180,10 +180,10 @@ export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
|
180
180
|
gap: 'var(--ggui-spacing-3, 12px)',
|
|
181
181
|
flexWrap: 'wrap',
|
|
182
182
|
}, children: [_jsx(Button, { variant: "primary", size: "md", onClick: onPrimary, disabled: ctaDisabled, children: ctaLabel }), ctaState === 'idle' || ctaState === 'working' ? (_jsx(Text, { size: "xs", style: {
|
|
183
|
-
color: 'var(--ggui-color-
|
|
183
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
184
184
|
opacity: 0.85,
|
|
185
185
|
}, children: "or tap-and-hold the URL above to copy" })) : null] })] })] }) }), intent ? (_jsxs(Text, { size: "xs", style: {
|
|
186
|
-
color: 'var(--ggui-color-
|
|
186
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
187
187
|
opacity: 0.7,
|
|
188
188
|
}, children: ["Pending prompt: ", intent] })) : null] }) }));
|
|
189
189
|
}
|
|
@@ -372,7 +372,7 @@ function ProbeButtonRow({ method, description, call, }) {
|
|
|
372
372
|
padding: 'var(--ggui-spacing-3, 12px)',
|
|
373
373
|
borderRadius: 'var(--ggui-shape-radius-md, 10px)',
|
|
374
374
|
border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
|
|
375
|
-
background: 'var(--ggui-color-
|
|
375
|
+
background: 'var(--ggui-color-container, #fff)',
|
|
376
376
|
}, children: [_jsxs("div", { style: {
|
|
377
377
|
display: 'flex',
|
|
378
378
|
alignItems: 'center',
|
|
@@ -382,10 +382,10 @@ function ProbeButtonRow({ method, description, call, }) {
|
|
|
382
382
|
}, children: [_jsx("code", { style: {
|
|
383
383
|
fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
384
384
|
fontSize: 'var(--ggui-font-size-sm, 13px)',
|
|
385
|
-
color: 'var(--ggui-color-
|
|
385
|
+
color: 'var(--ggui-color-onContainer, #111)',
|
|
386
386
|
fontWeight: 'var(--ggui-font-weight-semibold, 600)',
|
|
387
387
|
}, children: method }), statusBadge] }), _jsx(Text, { size: "xs", style: {
|
|
388
|
-
color: 'var(--ggui-color-
|
|
388
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
389
389
|
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
390
390
|
}, children: description }), _jsx("div", { style: { display: 'flex', gap: 'var(--ggui-spacing-2, 8px)' }, children: _jsx(Button, { variant: "primary", size: "sm", onClick: onClick, disabled: outcome.state === 'pending', children: buttonLabel }) }), outcome.state === 'success' || outcome.state === 'error' ? (_jsx("pre", { style: {
|
|
391
391
|
margin: 0,
|
|
@@ -393,7 +393,7 @@ function ProbeButtonRow({ method, description, call, }) {
|
|
|
393
393
|
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
394
394
|
color: outcome.state === 'error'
|
|
395
395
|
? 'var(--ggui-color-warning-500, #b45309)'
|
|
396
|
-
: 'var(--ggui-color-
|
|
396
|
+
: 'var(--ggui-color-onSunken, #52525b)',
|
|
397
397
|
background: 'var(--ggui-color-neutral-100, #f4f4f5)',
|
|
398
398
|
padding: 'var(--ggui-spacing-2, 8px)',
|
|
399
399
|
borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
|
|
@@ -414,7 +414,7 @@ export function ProtocolProbeCard({ intent, }) {
|
|
|
414
414
|
gap: 'var(--ggui-spacing-3, 12px)',
|
|
415
415
|
flexWrap: 'wrap',
|
|
416
416
|
}, children: [_jsx(Heading, { level: 3, children: "MCP Apps protocol probe" }), _jsx(Badge, { variant: "info", children: "diagnostic" })] }), _jsxs(Text, { style: {
|
|
417
|
-
color: 'var(--ggui-color-
|
|
417
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
418
418
|
lineHeight: '1.55',
|
|
419
419
|
}, children: ["One button per primary host effect emitted by the iframe runtime's gesture surfaces (submit-action bridge / native- idiom interceptors). Click each to verify which methods this host honors. The", _jsx("code", { children: "tools/call" }), " row chains the full submit-action bridge (audit + context + consent), reproducing the production ", _jsx("code", { children: "dispatchSubmitAction" }), " envelope shape verbatim. The receive log below captures every notification the host emits while this card is mounted."] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(ProbeButtonRow, { method: "ui/message", description: "Submit-action bridge step 3 \u2014 consent-gated user authorization. Host pre-fills + waits for user confirm before the LLM acts on the paired pending-action context.", call: call }), _jsx(ProbeButtonRow, { method: "ui/update-model-context", description: "Submit-action bridge step 2 \u2014 silent structured `[ggui:pending-action]` context drop. LLM honors on next turn. No immediate UI.", call: call }), _jsx(ProbeButtonRow, { method: "ui/open-link", description: "Native-idiom anchor click interceptor primary effect. Opens ggui.ai in the host's browser / new tab. Real iframe code triggers via `<a href target='_blank'>` or cross-origin href.", call: call }), _jsx(ProbeButtonRow, { method: "ui/request-display-mode", description: "Native-idiom fullscreen interceptor primary effect. Request fullscreen \u2014 real iframe code calls `el.requestFullscreen()`; the runtime overrides the prototype to route through here.", call: call }), _jsx(ProbeButtonRow, { method: "tools/call", description: "Full submit-action bridge: `tools/call ggui_runtime_submit_action` (audit, kind:'dispatch') + `ui/update-model-context` + `ui/message`. Spec \u00A7401 requires `_meta.ui.visibility:['app']` on the receiving tool.", call: call })] }), _jsxs(Stack, { gap: "sm", children: [_jsxs("div", { style: {
|
|
420
420
|
display: 'flex',
|
|
@@ -422,10 +422,10 @@ export function ProtocolProbeCard({ intent, }) {
|
|
|
422
422
|
justifyContent: 'space-between',
|
|
423
423
|
gap: 'var(--ggui-spacing-3, 12px)',
|
|
424
424
|
}, children: [_jsx(Heading, { level: 4, children: "Receive log" }), _jsxs(Text, { size: "xs", style: {
|
|
425
|
-
color: 'var(--ggui-color-
|
|
425
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
426
426
|
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
427
427
|
}, children: [log.length, " ", log.length === 1 ? 'event' : 'events'] })] }), log.length === 0 ? (_jsx(Text, { size: "xs", style: {
|
|
428
|
-
color: 'var(--ggui-color-
|
|
428
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
429
429
|
fontStyle: 'italic',
|
|
430
430
|
}, children: "No host notifications observed yet. Click a probe button or wait for the host to emit one." })) : (_jsx("div", { style: {
|
|
431
431
|
display: 'flex',
|
|
@@ -443,17 +443,17 @@ export function ProtocolProbeCard({ intent, }) {
|
|
|
443
443
|
}, children: [_jsxs("summary", { style: {
|
|
444
444
|
cursor: 'pointer',
|
|
445
445
|
padding: 'var(--ggui-spacing-1, 4px) 0',
|
|
446
|
-
color: 'var(--ggui-color-
|
|
446
|
+
color: 'var(--ggui-color-onContainer, #111)',
|
|
447
447
|
}, children: [_jsx("span", { style: {
|
|
448
|
-
color: 'var(--ggui-color-
|
|
448
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
449
449
|
marginRight: '8px',
|
|
450
450
|
}, children: entry.tsIso.slice(11, 19) }), _jsx("span", { style: { fontWeight: 600 }, children: entry.method })] }), _jsx("pre", { style: {
|
|
451
451
|
margin: '4px 0 0 16px',
|
|
452
452
|
whiteSpace: 'pre-wrap',
|
|
453
453
|
wordBreak: 'break-word',
|
|
454
|
-
color: 'var(--ggui-color-
|
|
454
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
455
455
|
}, children: JSON.stringify(entry.params, null, 2) })] }, entry.id))) }))] }), intent ? (_jsxs(Text, { size: "xs", style: {
|
|
456
|
-
color: 'var(--ggui-color-
|
|
456
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
457
457
|
opacity: 0.7,
|
|
458
458
|
}, children: ["Triggered by: ", intent] })) : null] }) }));
|
|
459
459
|
}
|
|
@@ -22,7 +22,7 @@ import qrcodeFactory from 'qrcode-generator';
|
|
|
22
22
|
* fails (e.g., URL too long for any error-correction level) so the
|
|
23
23
|
* caller can render a fallback.
|
|
24
24
|
*/
|
|
25
|
-
export function QrCode({ value, size = 128, errorCorrection = 'M', foreground = 'var(--ggui-color-
|
|
25
|
+
export function QrCode({ value, size = 128, errorCorrection = 'M', foreground = 'var(--ggui-color-onContainer, #18181b)', background = 'var(--ggui-color-container, #ffffff)', ariaLabel, }) {
|
|
26
26
|
const moduleMatrix = React.useMemo(() => {
|
|
27
27
|
try {
|
|
28
28
|
// typeNumber=0 lets the encoder pick the smallest fit. The
|
|
@@ -50,8 +50,8 @@ function UnknownSystemCard({ kind, props, }) {
|
|
|
50
50
|
border: '1px solid var(--ggui-color-outlineVariant, #e4e4e7)',
|
|
51
51
|
borderRadius: 'var(--ggui-shape-radius-md, 10px)',
|
|
52
52
|
fontFamily: 'var(--ggui-font-family-sans, -apple-system, BlinkMacSystemFont, sans-serif)',
|
|
53
|
-
background: 'var(--ggui-color-
|
|
54
|
-
color: 'var(--ggui-color-
|
|
53
|
+
background: 'var(--ggui-color-sunken, #f4f4f5)',
|
|
54
|
+
color: 'var(--ggui-color-onSunken, #52525b)',
|
|
55
55
|
},
|
|
56
56
|
}, React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, `Unknown system card: ${kind}`), React.createElement('div', {
|
|
57
57
|
style: {
|
package/dist/wire-config.d.ts
CHANGED
|
@@ -98,8 +98,8 @@ export interface BuildRootWireConfigOptions {
|
|
|
98
98
|
* pending-events pipe. First-party `createGguiServer` bridges it
|
|
99
99
|
* ONLY when composed with DEFAULT handlers (`opts.handlers`
|
|
100
100
|
* undefined); a custom `handlers` list drains its own pipe, so the
|
|
101
|
-
* channel deliberately does not bridge —
|
|
102
|
-
* caller, and on it a WS gesture lands on the retained ledger only.
|
|
101
|
+
* channel deliberately does not bridge — a deployment composing its own
|
|
102
|
+
* `handlers` is such a caller, and on it a WS gesture lands on the retained ledger only.
|
|
103
103
|
* See `GguiSessionChannelOptions.pendingEventConsumer`. The default
|
|
104
104
|
* send remains the seam tests and direct-WS callers exercise.
|
|
105
105
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/iframe-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"react": "^19.0.0",
|
|
42
42
|
"react-dom": "^19.0.0",
|
|
43
43
|
"zod": "^4.3.6",
|
|
44
|
-
"@ggui-ai/design": "0.
|
|
45
|
-
"@ggui-ai/
|
|
46
|
-
"@ggui-ai/
|
|
47
|
-
"@ggui-ai/
|
|
48
|
-
"@ggui-ai/protocol": "0.
|
|
49
|
-
"@ggui-ai/wire": "0.
|
|
44
|
+
"@ggui-ai/design": "0.16.0",
|
|
45
|
+
"@ggui-ai/live-channel": "0.16.0",
|
|
46
|
+
"@ggui-ai/preview-a2ui": "0.16.0",
|
|
47
|
+
"@ggui-ai/gadgets": "0.16.0",
|
|
48
|
+
"@ggui-ai/protocol": "0.16.0",
|
|
49
|
+
"@ggui-ai/wire": "0.16.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.27.0"
|