@ggui-ai/iframe-runtime 0.15.0 → 0.17.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/events-polling.js +12 -0
- 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 +69 -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 +17 -0
- package/dist/meta-parse.js +30 -2
- package/dist/observability.d.ts +73 -1
- package/dist/react-renderer.d.ts +20 -36
- package/dist/react-renderer.js +80 -96
- package/dist/render-item.d.ts +12 -13
- package/dist/render-item.js +5 -0
- package/dist/runtime.d.ts +32 -39
- package/dist/runtime.js +116 -68
- package/dist/status-dom.js +7 -0
- 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.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { type McpAppAiGguiRenderMeta, type McpAppBootstrapFailedMessage, type Mc
|
|
|
31
31
|
import type { GguiSessionSeedInput } from './types.js';
|
|
32
32
|
import type { McpAppAiGguiMetaParseFailureReason } from './types.js';
|
|
33
33
|
import { App } from '@modelcontextprotocol/ext-apps';
|
|
34
|
+
import type { McpUiDisplayMode } from '@modelcontextprotocol/ext-apps';
|
|
34
35
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
35
36
|
import type { GadgetPackageRegistry } from './globals.js';
|
|
36
37
|
import { StreamBus } from './wire-config.js';
|
|
@@ -97,36 +98,40 @@ export declare function setCurrentApp(app: App): void;
|
|
|
97
98
|
* `color-scheme` via `applyDocumentTheme` — nothing in the token
|
|
98
99
|
* pipeline keys off it, so a dark host painted the light ladder
|
|
99
100
|
* (host-fit 46.0 vs 87.7 with the dark ladder, probe `cc728a8eb`).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* Under ggui#987 §4 the embedding host OWNS runtime mode: when it
|
|
102
|
+
* announces one, `buildOpts` paints it over every stamped opinion; the
|
|
103
|
+
* stamped layers decide only when the host is silent. Read live from
|
|
104
|
+
* the App's pre-merged hostContext, so a `hostcontextchanged` flip
|
|
105
|
+
* re-resolves on the next `buildOpts`.
|
|
104
106
|
*
|
|
105
107
|
* @internal — exported for unit tests (`setCurrentApp` injects the
|
|
106
108
|
* App); production caller is `buildOpts` inside `bootSequence`.
|
|
107
109
|
*/
|
|
108
110
|
export declare function hostAnnouncedThemeMode(): 'light' | 'dark' | undefined;
|
|
109
111
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
112
|
+
* The host's announced display mode, from the same pre-merged host context the
|
|
113
|
+
* theme mode reads (ggui#1041). `fullscreen` means the host shows this page as
|
|
114
|
+
* the whole of a canvas — the runtime then composes the tree with `fit: 'fill'`
|
|
115
|
+
* so the mounted root drops its own silhouette and fills the page; anything
|
|
116
|
+
* else (or an absent context) keeps the inline card.
|
|
117
|
+
*/
|
|
118
|
+
export declare function hostAnnouncedDisplayMode(): McpUiDisplayMode | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the mount's color mode (ggui#987 §4 — the embedding host owns
|
|
121
|
+
* runtime mode; the app theme's `mode` is a DEFAULT, never a pin):
|
|
114
122
|
*
|
|
115
|
-
* stamped `themeMode` > `theme.mode`
|
|
116
|
-
*
|
|
123
|
+
* host-announced theme > stamped `themeMode` > `theme.mode`
|
|
124
|
+
* (the app theme's default) > undefined
|
|
117
125
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `hostContext.theme`: a light skeleton in a dark skin. The theme
|
|
125
|
-
* object's own `mode` already drove `color-scheme`; now it also
|
|
126
|
-
* selects the ladder.
|
|
126
|
+
* A card that ignored the host's announced mode painted a light
|
|
127
|
+
* skeleton inside a dark chat — the host's word is what the user
|
|
128
|
+
* actually sees around the card. The stamped layers (live provider >
|
|
129
|
+
* per-render override > app default > server default) decide only when
|
|
130
|
+
* the host is silent, which is the adapter-boundary case (a host
|
|
131
|
+
* helper that announces no `hostContext.theme`).
|
|
127
132
|
*
|
|
128
133
|
* `undefined` still means "no opinion anywhere" — the renderer's
|
|
129
|
-
* default applies; never coerced to `'light'
|
|
134
|
+
* default applies; never coerced to `'light'` here (ggui#551).
|
|
130
135
|
*
|
|
131
136
|
* @internal — exported for unit tests; production caller is
|
|
132
137
|
* `buildOpts` inside `bootSequence`.
|
|
@@ -134,36 +139,24 @@ export declare function hostAnnouncedThemeMode(): 'light' | 'dark' | undefined;
|
|
|
134
139
|
export declare function resolveMountThemeMode(meta: {
|
|
135
140
|
readonly themeMode?: 'light' | 'dark';
|
|
136
141
|
readonly theme?: {
|
|
137
|
-
readonly mode
|
|
138
|
-
readonly cssVariables?: Record<string, string>;
|
|
142
|
+
readonly mode?: 'light' | 'dark';
|
|
139
143
|
};
|
|
140
144
|
}): 'light' | 'dark' | undefined;
|
|
141
145
|
/**
|
|
142
|
-
* Resolve the mount's
|
|
146
|
+
* Resolve the mount's compiled-ladder id from the slice (ggui#987 D2):
|
|
143
147
|
*
|
|
144
|
-
* stamped `themeId` >
|
|
145
|
-
* > undefined (renderer default ladder)
|
|
148
|
+
* stamped `themeId` > undefined (renderer default ladder)
|
|
146
149
|
*
|
|
147
|
-
* The name
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* vars carry brand" store-frame class). An unregistered name is
|
|
152
|
-
* harmless by construction: `getScopedThemeCss` falls back to the
|
|
153
|
-
* default theme, which is byte-identical to the no-themeId path
|
|
154
|
-
* (`getScopedCssTokens` ≡ `getScopedThemeCss(default)`), so a slice
|
|
155
|
-
* naming its theme "My Custom" renders exactly as before.
|
|
150
|
+
* The app theme's `name` is a LABEL the author's tooling reads back —
|
|
151
|
+
* it never selects a ladder. An app theme carries its own full
|
|
152
|
+
* projection (`overlays`, exactly the manifest minus the floor), so
|
|
153
|
+
* there is no coverage gap for a compiled theme to fill beneath it.
|
|
156
154
|
*
|
|
157
155
|
* @internal — exported for unit tests; production caller is
|
|
158
156
|
* `buildOpts` inside `bootSequence`.
|
|
159
157
|
*/
|
|
160
158
|
export declare function resolveMountThemeId(meta: {
|
|
161
159
|
readonly themeId?: string;
|
|
162
|
-
readonly theme?: {
|
|
163
|
-
readonly name?: string;
|
|
164
|
-
readonly mode?: 'light' | 'dark';
|
|
165
|
-
readonly cssVariables?: Record<string, string>;
|
|
166
|
-
};
|
|
167
160
|
}): string | undefined;
|
|
168
161
|
/**
|
|
169
162
|
* The host's announced palette, read from the connected App's
|
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, reportStrippedThemeMembers, } 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,40 @@ export function hostAnnouncedThemeMode() {
|
|
|
198
204
|
return theme === 'light' || theme === 'dark' ? theme : undefined;
|
|
199
205
|
}
|
|
200
206
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
207
|
+
* The host's announced display mode, from the same pre-merged host context the
|
|
208
|
+
* theme mode reads (ggui#1041). `fullscreen` means the host shows this page as
|
|
209
|
+
* the whole of a canvas — the runtime then composes the tree with `fit: 'fill'`
|
|
210
|
+
* so the mounted root drops its own silhouette and fills the page; anything
|
|
211
|
+
* else (or an absent context) keeps the inline card.
|
|
212
|
+
*/
|
|
213
|
+
export function hostAnnouncedDisplayMode() {
|
|
214
|
+
return currentApp?.getHostContext()?.displayMode;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the mount's color mode (ggui#987 §4 — the embedding host owns
|
|
218
|
+
* runtime mode; the app theme's `mode` is a DEFAULT, never a pin):
|
|
205
219
|
*
|
|
206
|
-
* stamped `themeMode` > `theme.mode`
|
|
207
|
-
*
|
|
220
|
+
* host-announced theme > stamped `themeMode` > `theme.mode`
|
|
221
|
+
* (the app theme's default) > undefined
|
|
208
222
|
*
|
|
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.
|
|
223
|
+
* A card that ignored the host's announced mode painted a light
|
|
224
|
+
* skeleton inside a dark chat — the host's word is what the user
|
|
225
|
+
* actually sees around the card. The stamped layers (live provider >
|
|
226
|
+
* per-render override > app default > server default) decide only when
|
|
227
|
+
* the host is silent, which is the adapter-boundary case (a host
|
|
228
|
+
* helper that announces no `hostContext.theme`).
|
|
218
229
|
*
|
|
219
230
|
* `undefined` still means "no opinion anywhere" — the renderer's
|
|
220
|
-
* default applies; never coerced to `'light'
|
|
231
|
+
* default applies; never coerced to `'light'` here (ggui#551).
|
|
221
232
|
*
|
|
222
233
|
* @internal — exported for unit tests; production caller is
|
|
223
234
|
* `buildOpts` inside `bootSequence`.
|
|
224
235
|
*/
|
|
225
236
|
export function resolveMountThemeMode(meta) {
|
|
226
237
|
// 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.
|
|
238
|
+
// total order (@ggui-ai/protocol/integrations/theme-binding) — the
|
|
239
|
+
// server stamps via the sibling projection and the protocol's
|
|
240
|
+
// composition-law test pins that the two compose without rank drift.
|
|
231
241
|
return effectiveThemeMode({
|
|
232
242
|
stamped: meta.themeMode,
|
|
233
243
|
sessionSidecar: meta.theme?.mode,
|
|
@@ -235,20 +245,14 @@ export function resolveMountThemeMode(meta) {
|
|
|
235
245
|
});
|
|
236
246
|
}
|
|
237
247
|
/**
|
|
238
|
-
* Resolve the mount's
|
|
248
|
+
* Resolve the mount's compiled-ladder id from the slice (ggui#987 D2):
|
|
239
249
|
*
|
|
240
|
-
* stamped `themeId` >
|
|
241
|
-
* > undefined (renderer default ladder)
|
|
250
|
+
* stamped `themeId` > undefined (renderer default ladder)
|
|
242
251
|
*
|
|
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.
|
|
252
|
+
* The app theme's `name` is a LABEL the author's tooling reads back —
|
|
253
|
+
* it never selects a ladder. An app theme carries its own full
|
|
254
|
+
* projection (`overlays`, exactly the manifest minus the floor), so
|
|
255
|
+
* there is no coverage gap for a compiled theme to fill beneath it.
|
|
252
256
|
*
|
|
253
257
|
* @internal — exported for unit tests; production caller is
|
|
254
258
|
* `buildOpts` inside `bootSequence`.
|
|
@@ -256,10 +260,7 @@ export function resolveMountThemeMode(meta) {
|
|
|
256
260
|
export function resolveMountThemeId(meta) {
|
|
257
261
|
// Client projection of the normative theme-binding order — see
|
|
258
262
|
// resolveMountThemeMode above.
|
|
259
|
-
return effectiveThemeId({
|
|
260
|
-
stamped: meta.themeId,
|
|
261
|
-
sidecarName: meta.theme?.name,
|
|
262
|
-
});
|
|
263
|
+
return effectiveThemeId({ stamped: meta.themeId });
|
|
263
264
|
}
|
|
264
265
|
/**
|
|
265
266
|
* The host's announced palette, read from the connected App's
|
|
@@ -461,7 +462,7 @@ export async function bootSequence(opts) {
|
|
|
461
462
|
// `tools/call` (dispatchSubmitAction, channel-transport router)
|
|
462
463
|
// routes through `app.callServerTool` instead of the legacy raw
|
|
463
464
|
// postMessage pump. Idempotent: re-boot with the same App is a
|
|
464
|
-
// no-op; a different App throws (the iframe is single-
|
|
465
|
+
// no-op; a different App throws (the iframe is single-app).
|
|
465
466
|
setCurrentApp(app);
|
|
466
467
|
// Slice-meta resolution — spec-canonical primary, no in-house
|
|
467
468
|
// Reading-B (retired Phase 1.19b.3 with the App-class swap; the
|
|
@@ -1356,7 +1357,10 @@ export async function resolveMetaViaReadDoor(app, locator) {
|
|
|
1356
1357
|
const envelope = readGguiShellEnvelope(text);
|
|
1357
1358
|
if (envelope === undefined)
|
|
1358
1359
|
return null;
|
|
1359
|
-
const parsed = parseMcpAppAiGguiRenderMeta(envelope
|
|
1360
|
+
const parsed = parseMcpAppAiGguiRenderMeta(envelope, {
|
|
1361
|
+
onInvalidTheme: reportInvalidAppTheme,
|
|
1362
|
+
onStrippedThemeMembers: reportStrippedThemeMembers,
|
|
1363
|
+
});
|
|
1360
1364
|
if (!parsed.ok || parsed.meta === undefined)
|
|
1361
1365
|
return null;
|
|
1362
1366
|
const validated = validateMeta(parsed.meta);
|
|
@@ -1406,7 +1410,7 @@ function awaitToolResultMeta(timeoutMs) {
|
|
|
1406
1410
|
* App instance even across re-mounts (e.g. an agent fires a second
|
|
1407
1411
|
* `ggui_render` and the listener re-applies through the published
|
|
1408
1412
|
* `applyRender`). Keyed by App — not a module boolean — so a fresh App
|
|
1409
|
-
* (a re-booted document in tests; production is single-
|
|
1413
|
+
* (a re-booted document in tests; production is single-app per
|
|
1410
1414
|
* iframe) always gets its own registration, never a stale latch.
|
|
1411
1415
|
*/
|
|
1412
1416
|
const persistentToolResultListenerApps = new WeakSet();
|
|
@@ -1883,7 +1887,7 @@ function applyFreezeCue(root) {
|
|
|
1883
1887
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
1884
1888
|
fontSize: '11px',
|
|
1885
1889
|
fontWeight: '600',
|
|
1886
|
-
color: 'var(--ggui-color-
|
|
1890
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
1887
1891
|
background: 'rgba(128,128,128,0.16)',
|
|
1888
1892
|
border: '1px solid currentColor',
|
|
1889
1893
|
opacity: '0.85',
|
|
@@ -2320,7 +2324,7 @@ let relayLatchTrigger;
|
|
|
2320
2324
|
let relayLatchScope = {};
|
|
2321
2325
|
function transitionRelayLatch(next, trigger, scope = {}) {
|
|
2322
2326
|
relayIncapabilityAnnounced = next;
|
|
2323
|
-
|
|
2327
|
+
runtimeConnectionWriter().set(!next);
|
|
2324
2328
|
if (next && trigger !== undefined) {
|
|
2325
2329
|
relayLatchedAt = Date.now();
|
|
2326
2330
|
relayDeadTaps = 0;
|
|
@@ -2435,6 +2439,10 @@ function isRelayNoticeVisible() {
|
|
|
2435
2439
|
* @internal — exported for unit tests.
|
|
2436
2440
|
*/
|
|
2437
2441
|
export function resetRelayLatchForBoot() {
|
|
2442
|
+
// The runtime's claim on the document's connection writer is taken HERE,
|
|
2443
|
+
// eagerly, before any transition (ggui#843): a second runtime in this
|
|
2444
|
+
// document fails at boot naming itself, never continuing unclaimed.
|
|
2445
|
+
runtimeConnectionWriter();
|
|
2438
2446
|
if (typeof document !== 'undefined') {
|
|
2439
2447
|
const el = document.getElementById(ACTION_TOAST_ID);
|
|
2440
2448
|
if (el !== null && el.hasAttribute(RELAY_NOTICE_ATTR))
|
|
@@ -2445,14 +2453,14 @@ export function resetRelayLatchForBoot() {
|
|
|
2445
2453
|
transitionRelayLatch(false);
|
|
2446
2454
|
}
|
|
2447
2455
|
else {
|
|
2448
|
-
|
|
2456
|
+
runtimeConnectionWriter().set(true);
|
|
2449
2457
|
}
|
|
2450
2458
|
}
|
|
2451
2459
|
/**
|
|
2452
2460
|
* Module-level handle to the CURRENT boot's telemetry sink so
|
|
2453
2461
|
* module-scope paths (the doorbell emitter below) can report without
|
|
2454
2462
|
* threading the sink through every call chain. Per-iframe
|
|
2455
|
-
* single-
|
|
2463
|
+
* single-app scope makes a single slot correct — same justification as
|
|
2456
2464
|
* `getCurrentApp()`.
|
|
2457
2465
|
*/
|
|
2458
2466
|
let currentTelemetrySink = null;
|
|
@@ -2540,7 +2548,7 @@ function resetRelayCueThrottles() {
|
|
|
2540
2548
|
/** @internal — exported for unit tests to reset module state. */
|
|
2541
2549
|
export function __resetRelayNoticeForTest() {
|
|
2542
2550
|
relayIncapabilityAnnounced = false;
|
|
2543
|
-
|
|
2551
|
+
runtimeConnectionWriter().set(true);
|
|
2544
2552
|
relayLatchedAt = undefined;
|
|
2545
2553
|
relayDeadTaps = 0;
|
|
2546
2554
|
relayLatchTrigger = undefined;
|
|
@@ -3712,10 +3720,10 @@ async function bootProduction(opts) {
|
|
|
3712
3720
|
// DEFAULT-handlers compositions only (a custom `handlers` list
|
|
3713
3721
|
// drains its own pipe; bridging the channel onto an instance
|
|
3714
3722
|
// 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
|
|
3723
|
+
// hoist note in server.ts). A deployment composing its own `handlers`
|
|
3724
|
+
// IS such a caller: on it, a WS gesture lands on the retained ledger only
|
|
3717
3725
|
// and never reaches the agent. Any WS action fallback (ggui#599's
|
|
3718
|
-
// open design leg) therefore requires the server
|
|
3726
|
+
// open design leg) therefore requires the server-side bridge
|
|
3719
3727
|
// FIRST — it is a capability gap on that composition today. The WS pipe otherwise carries
|
|
3720
3728
|
// streamSpec subscriptions (inbound `ggui_emit` fanout +
|
|
3721
3729
|
// `props_update` + `render` + `data` + `drain_ack` +
|
|
@@ -3864,29 +3872,31 @@ async function bootProduction(opts) {
|
|
|
3864
3872
|
!('createdAt' in render)
|
|
3865
3873
|
? { codeModuleUrl: meta.codeModuleUrl }
|
|
3866
3874
|
: {}),
|
|
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).
|
|
3875
|
+
// Compiled ladder (see `resolveMountThemeId`): the stamped
|
|
3876
|
+
// themeId alone; absent = the renderer's default ladder.
|
|
3872
3877
|
...(() => {
|
|
3873
3878
|
const themeId = resolveMountThemeId(meta);
|
|
3874
3879
|
return themeId !== undefined ? { themeId } : {};
|
|
3875
3880
|
})(),
|
|
3876
|
-
// Mode
|
|
3877
|
-
// stamped `themeMode` > the
|
|
3878
|
-
//
|
|
3879
|
-
//
|
|
3881
|
+
// Mode (ggui#987 §4 — see `resolveMountThemeMode`): host-
|
|
3882
|
+
// announced theme > stamped `themeMode` > the app theme's
|
|
3883
|
+
// default `mode`. Absent everywhere means "no opinion", not
|
|
3884
|
+
// "light". Re-resolved on every buildOpts, so the live
|
|
3885
|
+
// hostcontextchanged re-injection below picks up a flip.
|
|
3880
3886
|
...(() => {
|
|
3881
3887
|
const themeMode = resolveMountThemeMode(meta);
|
|
3882
3888
|
return themeMode !== undefined ? { themeMode } : {};
|
|
3883
3889
|
})(),
|
|
3884
|
-
//
|
|
3890
|
+
// The host's canvas is the chrome (ggui#1041): a `fullscreen` display mode
|
|
3891
|
+
// composes the tree with `fit: 'fill'`; the hostcontextchanged re-injection
|
|
3892
|
+
// below picks up a flip exactly as it does the theme mode.
|
|
3893
|
+
...(hostAnnouncedDisplayMode() === 'fullscreen' ? { fit: 'fill' } : {}),
|
|
3894
|
+
// Per-app theme (ggui#987 v2). Threaded straight from the
|
|
3885
3895
|
// bootstrap's `_meta["ai.ggui/render"].theme` (typed `AppTheme`,
|
|
3886
3896
|
// already injection-validated by the wire parser) onto the mount
|
|
3887
|
-
// options
|
|
3888
|
-
//
|
|
3889
|
-
// cards theme via the SystemCardHost/ThemeProvider path.
|
|
3897
|
+
// options; the renderer injects the effective mode's overlay
|
|
3898
|
+
// into the scoped block + `:root`. THEME IS COMPONENT-ONLY —
|
|
3899
|
+
// system cards theme via the SystemCardHost/ThemeProvider path.
|
|
3890
3900
|
...(meta.theme !== undefined ? { appTheme: meta.theme } : {}),
|
|
3891
3901
|
// Host palette: always threaded when the host announced one —
|
|
3892
3902
|
// unlike mode there is no either/or gate here, because the
|
|
@@ -3912,14 +3922,52 @@ async function bootProduction(opts) {
|
|
|
3912
3922
|
* layer just calls `applyRender(render)` without owning
|
|
3913
3923
|
* lifecycle state.
|
|
3914
3924
|
*/
|
|
3925
|
+
// Every in-place update of the mounted slot goes through ONE
|
|
3926
|
+
// chain: the WS render-frame / props_update path and the live
|
|
3927
|
+
// theme re-injection below are independent callers, and the
|
|
3928
|
+
// handle's `update` has no lock of its own — serializing them
|
|
3929
|
+
// here is what keeps a host flip landing mid-render from
|
|
3930
|
+
// interleaving two updates.
|
|
3931
|
+
let updateChain = Promise.resolve();
|
|
3932
|
+
const queueUpdate = (opts) => {
|
|
3933
|
+
const run = updateChain.then(async () => {
|
|
3934
|
+
if (renderHandle === null)
|
|
3935
|
+
return;
|
|
3936
|
+
await renderHandle.update(opts());
|
|
3937
|
+
});
|
|
3938
|
+
updateChain = run.catch(() => undefined);
|
|
3939
|
+
return run;
|
|
3940
|
+
};
|
|
3915
3941
|
const applyRender = async (render) => {
|
|
3916
3942
|
currentRender = render;
|
|
3917
3943
|
if (renderHandle === null) {
|
|
3918
3944
|
renderHandle = await mountRender(renderInto, buildOpts(render));
|
|
3919
3945
|
return;
|
|
3920
3946
|
}
|
|
3921
|
-
await
|
|
3947
|
+
await queueUpdate(() => buildOpts(render));
|
|
3922
3948
|
};
|
|
3949
|
+
// Live re-injection (ggui#987 §4): the host owns runtime mode and
|
|
3950
|
+
// may flip it mid-session. The trigger is the emitter's THEME
|
|
3951
|
+
// subscription — the raw `theme` / `styles` payload — because the
|
|
3952
|
+
// host-context projection excludes both by design and its
|
|
3953
|
+
// equality gate would never fire for a theme-only flip. The App
|
|
3954
|
+
// pre-merges the new hostContext before the emitter fans out, so
|
|
3955
|
+
// re-running `buildOpts` on the CURRENT render re-resolves mode +
|
|
3956
|
+
// palette and the renderer repaints from the retained other
|
|
3957
|
+
// overlay — no fetch, no remount. A rejection is reported to the
|
|
3958
|
+
// embedding host, never dropped on the floor.
|
|
3959
|
+
activeThemeUnsubscribe?.();
|
|
3960
|
+
activeThemeUnsubscribe = subscribeHostThemeChange(() => {
|
|
3961
|
+
if (renderHandle === null || currentRender === null)
|
|
3962
|
+
return;
|
|
3963
|
+
const render = currentRender;
|
|
3964
|
+
queueUpdate(() => buildOpts(render)).catch((err) => {
|
|
3965
|
+
postObservabilityToParent({
|
|
3966
|
+
kind: 'theme-reinject-failed',
|
|
3967
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3968
|
+
});
|
|
3969
|
+
});
|
|
3970
|
+
});
|
|
3923
3971
|
// Publish `applyRender` module-level. The persistent tool-result
|
|
3924
3972
|
// listener is ALREADY registered — `bootSequence` installs it
|
|
3925
3973
|
// pre-connect (ggui#586) — and reads this slot dynamically, so
|
package/dist/status-dom.js
CHANGED
|
@@ -35,6 +35,13 @@ export function ensureStatusDom(doc) {
|
|
|
35
35
|
(() => {
|
|
36
36
|
const el = doc.createElement('ul');
|
|
37
37
|
el.setAttribute('data-ggui-session-root', '');
|
|
38
|
+
// The list is the runtime's mount target, not a list: without this
|
|
39
|
+
// reset the browser's defaults (16 px margins, a 40 px indent, disc
|
|
40
|
+
// bullets) sat inside every compact card's iframe — the card 40 px
|
|
41
|
+
// right of the pane edge and 32 px taller than its box (ggui#1096).
|
|
42
|
+
el.style.margin = '0';
|
|
43
|
+
el.style.padding = '0';
|
|
44
|
+
el.style.listStyle = 'none';
|
|
38
45
|
doc.body.appendChild(el);
|
|
39
46
|
return el;
|
|
40
47
|
})();
|
|
@@ -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
|
}
|