@ggui-ai/iframe-runtime 0.14.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 +3 -3
- package/dist/channels/channel-payload.d.ts +1 -1
- package/dist/channels/data.d.ts +1 -1
- package/dist/channels/drain-ack.d.ts +1 -1
- package/dist/channels/props-update.d.ts +1 -1
- package/dist/channels/render-frame.d.ts +1 -1
- package/dist/compiled-validators.d.ts +1 -1
- package/dist/connection.d.ts +34 -12
- package/dist/connection.js +40 -12
- package/dist/context-observer.d.ts +1 -1
- package/dist/context-observer.js +1 -1
- package/dist/events-polling.d.ts +1 -1
- package/dist/host-context-emitter.d.ts +23 -2
- package/dist/host-context-emitter.js +38 -5
- 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 +97 -108
- 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 +15 -3
- package/dist/observability.d.ts +73 -14
- package/dist/provisional-renderer.d.ts +1 -1
- package/dist/react-renderer.d.ts +19 -37
- package/dist/react-renderer.js +64 -72
- package/dist/registry-subscribe.d.ts +1 -1
- package/dist/render-item.d.ts +7 -14
- package/dist/runtime-telemetry.js +1 -1
- package/dist/runtime.d.ts +48 -56
- package/dist/runtime.js +213 -115
- 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/types.d.ts +1 -1
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +1 -1
- package/dist/wire-config.d.ts +3 -3
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './m
|
|
|
37
37
|
export type { RendererBootFailureReason, RendererBootFailedMessage, } from './runtime.js';
|
|
38
38
|
export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
|
|
39
39
|
export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
|
|
40
|
-
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
|
|
40
|
+
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, FontFaceBlockedEvent, AppThemeInvalidEvent, ThemeReinjectFailedEvent, SubscribeFailedEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, ChannelPollDegradedEvent, ChannelPollRecoveredEvent, UiFeedbackEvent, RelayDeadTapEvent, RelayIncapabilityEvent, RelayLatchTrigger, UnknownObservabilityEvent, } from './observability.js';
|
|
41
41
|
export { isObservabilityMessage, isRelayDeadTapEvent, isRelayIncapabilityEvent, postObservabilityToParent, } from './observability.js';
|
|
42
42
|
export type { LifecycleEmitter } from './lifecycle.js';
|
|
43
43
|
export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
|
|
@@ -50,6 +50,21 @@ export interface McpAppIframeMountOptions {
|
|
|
50
50
|
/** Optional locale; defaults to `navigator.language` when
|
|
51
51
|
* available. */
|
|
52
52
|
readonly locale?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The color mode this host paints around the card (ggui#987 §4: the
|
|
55
|
+
* embedding host owns runtime mode). Announced as
|
|
56
|
+
* `hostContext.theme`; the card's ladder, overlay and `color-scheme`
|
|
57
|
+
* follow it. Omit to announce nothing — the card falls back to the
|
|
58
|
+
* slice's stamped mode.
|
|
59
|
+
*/
|
|
60
|
+
readonly theme?: 'light' | 'dark';
|
|
61
|
+
/**
|
|
62
|
+
* `@font-face` rules the card may install (ggui#987 §5), announced
|
|
63
|
+
* as `hostContext.styles.css.fonts`. The card's CSP decides whether
|
|
64
|
+
* each `src` loads; a refused face is reported back as
|
|
65
|
+
* `font-face-blocked`.
|
|
66
|
+
*/
|
|
67
|
+
readonly fonts?: string;
|
|
53
68
|
}
|
|
54
69
|
export interface McpAppIframeMount {
|
|
55
70
|
/** Tear down — posts `ui/resource-teardown` then removes the iframe. */
|
|
@@ -113,7 +113,9 @@ export function mountMcpAppIframe(container, opts) {
|
|
|
113
113
|
// `hostCapabilities` + `hostContext`; the pre-App draft shape
|
|
114
114
|
// fails that gate and kills the embedded mount. Adapter
|
|
115
115
|
// boundary — `hostContext` carries `{locale,
|
|
116
|
-
// containerDimensions}`
|
|
116
|
+
// containerDimensions}` plus the host's OWN `theme` / fonts
|
|
117
|
+
// when the embedder passed them (ggui#987 §4/§5); never any
|
|
118
|
+
// outer ggui render state.
|
|
117
119
|
const requestedRaw = req.params?.['protocolVersion'];
|
|
118
120
|
const requested = typeof requestedRaw === 'string' && requestedRaw.length > 0
|
|
119
121
|
? requestedRaw
|
|
@@ -133,6 +135,8 @@ export function mountMcpAppIframe(container, opts) {
|
|
|
133
135
|
locale: opts.locale ??
|
|
134
136
|
(typeof navigator !== 'undefined' ? navigator.language : 'en-US'),
|
|
135
137
|
containerDimensions: dims,
|
|
138
|
+
...(opts.theme !== undefined ? { theme: opts.theme } : {}),
|
|
139
|
+
...(opts.fonts !== undefined ? { styles: { css: { fonts: opts.fonts } } } : {}),
|
|
136
140
|
},
|
|
137
141
|
};
|
|
138
142
|
response = { jsonrpc: '2.0', id, result };
|
package/dist/meta-parse.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ import type { McpAppAiGguiMetaParseResult } from './types.js';
|
|
|
16
16
|
* has nothing to mount.
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateMeta(meta: McpAppAiGguiRenderMeta): McpAppAiGguiMetaParseResult;
|
|
19
|
+
/**
|
|
20
|
+
* The wire parser refused the slice's `theme` (ggui#987 §3.4): report it
|
|
21
|
+
* to the embedding host as `app-theme-invalid` and mount without it. A
|
|
22
|
+
* write door should have refused this upstream — the event is the
|
|
23
|
+
* operator's pointer to the door that did not. Passed as
|
|
24
|
+
* `onInvalidTheme` on every `parseMcpAppAiGguiRenderMeta` call in this
|
|
25
|
+
* package.
|
|
26
|
+
*/
|
|
27
|
+
export declare function reportInvalidAppTheme(issues: readonly string[]): void;
|
|
19
28
|
/**
|
|
20
29
|
* Parse the render slice from `globalThis.__GGUI_META__` — the
|
|
21
30
|
* synchronous self-contained shell delivery channel.
|
package/dist/meta-parse.js
CHANGED
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
* At least one mode MUST be present. Half-live (wsUrl without wsToken)
|
|
51
51
|
* is structurally rejected by the combiner.
|
|
52
52
|
*/
|
|
53
|
-
import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol';
|
|
53
|
+
import { PUBLIC_ENV_APP_KEY_RE } from '@ggui-ai/protocol/wire';
|
|
54
54
|
import { parseMcpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
55
|
+
import { postObservabilityToParent } from './observability.js';
|
|
55
56
|
/**
|
|
56
57
|
* Type guard — true iff `value` is a non-null, non-array plain object.
|
|
57
58
|
*/
|
|
@@ -323,6 +324,17 @@ export function validateMeta(meta) {
|
|
|
323
324
|
return result;
|
|
324
325
|
return { ok: true, meta: result.meta };
|
|
325
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* The wire parser refused the slice's `theme` (ggui#987 §3.4): report it
|
|
329
|
+
* to the embedding host as `app-theme-invalid` and mount without it. A
|
|
330
|
+
* write door should have refused this upstream — the event is the
|
|
331
|
+
* operator's pointer to the door that did not. Passed as
|
|
332
|
+
* `onInvalidTheme` on every `parseMcpAppAiGguiRenderMeta` call in this
|
|
333
|
+
* package.
|
|
334
|
+
*/
|
|
335
|
+
export function reportInvalidAppTheme(issues) {
|
|
336
|
+
postObservabilityToParent({ kind: 'app-theme-invalid', issues: [...issues] });
|
|
337
|
+
}
|
|
326
338
|
/**
|
|
327
339
|
* Parse the render slice from `globalThis.__GGUI_META__` — the
|
|
328
340
|
* synchronous self-contained shell delivery channel.
|
|
@@ -345,7 +357,7 @@ export function parseMetaFromGlobal() {
|
|
|
345
357
|
if (!isPlainObject(raw)) {
|
|
346
358
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
347
359
|
}
|
|
348
|
-
const parsed = parseMcpAppAiGguiRenderMeta(raw);
|
|
360
|
+
const parsed = parseMcpAppAiGguiRenderMeta(raw, { onInvalidTheme: reportInvalidAppTheme });
|
|
349
361
|
if (!parsed.ok) {
|
|
350
362
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
351
363
|
}
|
|
@@ -396,7 +408,7 @@ export function parseMetaFromToolResult(params) {
|
|
|
396
408
|
return { ok: false, reason: 'MISSING_META_GGUI_BOOTSTRAP' };
|
|
397
409
|
}
|
|
398
410
|
const meta = topMeta;
|
|
399
|
-
const parsed = parseMcpAppAiGguiRenderMeta(meta);
|
|
411
|
+
const parsed = parseMcpAppAiGguiRenderMeta(meta, { onInvalidTheme: reportInvalidAppTheme });
|
|
400
412
|
if (!parsed.ok) {
|
|
401
413
|
return { ok: false, reason: 'MALFORMED_BOOTSTRAP' };
|
|
402
414
|
}
|
package/dist/observability.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
|
33
33
|
*
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | UnknownObservabilityEvent;
|
|
36
|
+
export type ObservabilityEvent = SchemaVersionMismatchEvent | FontFaceBlockedEvent | AppThemeInvalidEvent | ThemeReinjectFailedEvent | SubscribeFailedEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | ChannelPollDegradedEvent | ChannelPollRecoveredEvent | UiFeedbackEvent | RelayIncapabilityEvent | RelayDeadTapEvent | UnknownObservabilityEvent;
|
|
37
37
|
/**
|
|
38
38
|
* Fired when the protocol-version handshake rejects the connection.
|
|
39
39
|
* Parallel to `UpgradeRequiredError` — the host can choose to render
|
|
@@ -179,15 +179,29 @@ export interface UiFeedbackEvent {
|
|
|
179
179
|
readonly toolName?: string;
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
* The evidence that latched a relay dead zone
|
|
183
|
-
* `'confirmed-refusal'` — the relay answered a declared refusal code
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
182
|
+
* The evidence that latched a relay dead zone:
|
|
183
|
+
* - `'confirmed-refusal'` — the relay answered a declared refusal code
|
|
184
|
+
* (helper-minted; outranks any positive advertisement) (ggui#599
|
|
185
|
+
* cycle-2);
|
|
186
|
+
* - `'advert-silent'` — the host never advertised `serverTools` and
|
|
187
|
+
* the attempt failed relay-shaped;
|
|
188
|
+
* - `'boot-failed'` — the mandatory `ui/initialize` handshake did not
|
|
189
|
+
* complete (refused, timed out, or the transport failed), so
|
|
190
|
+
* nothing can ever be delivered in this document (ggui#830). Not a
|
|
191
|
+
* probe the runtime minted: the outcome of the one request every
|
|
192
|
+
* session begins with — the strongest confirmed outcome there is.
|
|
193
|
+
* It clears the only way a handshake failure can: the next boot's
|
|
194
|
+
* close edge.
|
|
195
|
+
*
|
|
196
|
+
* The union is closed per renderer version; nothing in a host branches
|
|
197
|
+
* on the trigger (the first-party host passes it through untouched) —
|
|
198
|
+
* the edge is the fact, the trigger is why. A host that does branch on
|
|
199
|
+
* it should treat a value it does not know as "latched, reason
|
|
200
|
+
* unknown", never as "not latched".
|
|
187
201
|
*
|
|
188
202
|
* @public
|
|
189
203
|
*/
|
|
190
|
-
export type RelayLatchTrigger = 'confirmed-refusal' | 'advert-silent';
|
|
204
|
+
export type RelayLatchTrigger = 'confirmed-refusal' | 'advert-silent' | 'boot-failed';
|
|
191
205
|
/**
|
|
192
206
|
* Fired at the two transition edges of the renderer's
|
|
193
207
|
* relay-incapability latch — the runtime's confirmed determination
|
|
@@ -195,7 +209,9 @@ export type RelayLatchTrigger = 'confirmed-refusal' | 'advert-silent';
|
|
|
195
209
|
*
|
|
196
210
|
* - `'latched'` — a real user gesture just failed relay-shaped on a
|
|
197
211
|
* host whose captured capability handshake never advertised
|
|
198
|
-
* `serverTools`, or the relay answered a declared refusal code
|
|
212
|
+
* `serverTools`, or the relay answered a declared refusal code, or
|
|
213
|
+
* the host refused `ui/initialize` itself (`'boot-failed'`,
|
|
214
|
+
* ggui#830 — no gesture is needed to know nothing can be delivered).
|
|
199
215
|
* The runtime now treats relay as confirmed-unavailable: it shows
|
|
200
216
|
* one persistent explanation, arms the dead-zone cue for every
|
|
201
217
|
* later gesture, and channel polls fail fast without a transport
|
|
@@ -214,9 +230,11 @@ export type RelayLatchTrigger = 'confirmed-refusal' | 'advert-silent';
|
|
|
214
230
|
* a latch standing from a prior mount closes it with a `'cleared'`
|
|
215
231
|
* edge before the new session starts.
|
|
216
232
|
*
|
|
217
|
-
* Always emitted via the postMessage-to-parent default — the
|
|
218
|
-
*
|
|
219
|
-
* graph
|
|
233
|
+
* Always emitted via the postMessage-to-parent default, by rule — the
|
|
234
|
+
* gesture-dispatch emission sites run outside any given boot call, and
|
|
235
|
+
* the boot-graph site (the `boot-failed` edge, ggui#830) follows the
|
|
236
|
+
* same rule — so these edges never flow through an injected
|
|
237
|
+
* `onObserve` sink and a host reads every edge the same way.
|
|
220
238
|
*
|
|
221
239
|
* @public
|
|
222
240
|
*/
|
|
@@ -249,9 +267,10 @@ export type RelayIncapabilityEvent = {
|
|
|
249
267
|
* document (pulse, spoken cue, fallback toast); this event makes the
|
|
250
268
|
* attempt COUNTABLE by the host.
|
|
251
269
|
*
|
|
252
|
-
* Contract: exactly one per gesture
|
|
253
|
-
*
|
|
254
|
-
*
|
|
270
|
+
* Contract: exactly one per gesture whose UNDELIVERED outcome
|
|
271
|
+
* (relay-shaped failure or a declared refusal) lands while the latch
|
|
272
|
+
* stands — an in-flight gesture that raced the latching one counts;
|
|
273
|
+
* never emitted off-latch, and never from the channel
|
|
255
274
|
* router's fail-fast ticks. Counted at the outcome, not the tap: the
|
|
256
275
|
* gesture that latched the zone is the `'latched'` edge and the gesture
|
|
257
276
|
* that heals it is the `'cleared'` edge — neither is a dead tap.
|
|
@@ -284,6 +303,46 @@ export interface RelayDeadTapEvent {
|
|
|
284
303
|
*
|
|
285
304
|
* @public
|
|
286
305
|
*/
|
|
306
|
+
/**
|
|
307
|
+
* A host-delivered `@font-face` src the document's CSP refused
|
|
308
|
+
* (ggui#987 §5): the card installed the host's `styles.css.fonts`, the
|
|
309
|
+
* browser blocked the fetch under `font-src`, and the family will fall
|
|
310
|
+
* back to its stack. Reported ONCE per (family, host) per document —
|
|
311
|
+
* the operator's signal that the shell's `font-src` allowlist and the
|
|
312
|
+
* host's font origin disagree.
|
|
313
|
+
*
|
|
314
|
+
* @public
|
|
315
|
+
*/
|
|
316
|
+
export interface FontFaceBlockedEvent {
|
|
317
|
+
readonly kind: 'font-face-blocked';
|
|
318
|
+
/** The `font-family` the refused `@font-face` block declared. */
|
|
319
|
+
readonly family: string;
|
|
320
|
+
/** Host of the blocked `src` URL (`blockedURI`, origin-stripped by CSP). */
|
|
321
|
+
readonly host: string;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The render slice carried a `theme` the wire schema refused (ggui#987
|
|
325
|
+
* §3.4): the parser dropped it and the card painted the compiled ladder
|
|
326
|
+
* alone. `issues` are the schema's own messages — the operator's
|
|
327
|
+
* pointer to what the write door should have refused.
|
|
328
|
+
*
|
|
329
|
+
* @public
|
|
330
|
+
*/
|
|
331
|
+
export interface AppThemeInvalidEvent {
|
|
332
|
+
readonly kind: 'app-theme-invalid';
|
|
333
|
+
readonly issues: readonly string[];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The live re-injection on a host theme flip (ggui#987 §4) failed: the
|
|
337
|
+
* renderer's update rejected, so the card kept its previous overlay /
|
|
338
|
+
* ladder / `color-scheme`. `message` is the rejection's.
|
|
339
|
+
*
|
|
340
|
+
* @public
|
|
341
|
+
*/
|
|
342
|
+
export interface ThemeReinjectFailedEvent {
|
|
343
|
+
readonly kind: 'theme-reinject-failed';
|
|
344
|
+
readonly message: string;
|
|
345
|
+
}
|
|
287
346
|
export interface UnknownObservabilityEvent {
|
|
288
347
|
readonly kind: string & {};
|
|
289
348
|
readonly [field: string]: unknown;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* while suspended, matching host-SDK behavior.
|
|
32
32
|
*/
|
|
33
33
|
import React, { type ReactNode } from 'react';
|
|
34
|
-
import type { StreamEnvelope } from '@ggui-ai/protocol';
|
|
34
|
+
import type { StreamEnvelope } from '@ggui-ai/protocol/wire';
|
|
35
35
|
export interface ProvisionalMount {
|
|
36
36
|
/**
|
|
37
37
|
* Feed a stream envelope. Ignores non-PREVIEW_CHANNEL envelopes
|
package/dist/react-renderer.d.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* TypeScript config doesn't need a JSX runtime plumbing.
|
|
31
31
|
*/
|
|
32
32
|
import React, { type ReactNode } from 'react';
|
|
33
|
-
import type { AppTheme } from '@ggui-ai/protocol';
|
|
33
|
+
import type { AppTheme } from '@ggui-ai/protocol/wire';
|
|
34
34
|
export interface ReactRootMountOptions {
|
|
35
35
|
/**
|
|
36
36
|
* GguiSession whose componentCode + props drive the mount. Only
|
|
@@ -63,44 +63,26 @@ export interface ReactRootMountOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
readonly themeMode?: 'light' | 'dark';
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
66
|
+
* The per-app theme (protocol's `AppTheme`, ggui#987 v2): BOTH modes'
|
|
67
|
+
* derived `--ggui-*` projections (`overlays.light` / `overlays.dark`),
|
|
68
|
+
* optional mode-agnostic `cssVariables`, per-mode `keyframes`, and
|
|
69
|
+
* the `frameless` flag. The renderer injects the overlay for the
|
|
70
|
+
* EFFECTIVE mode (`themeMode`, host-owned per §4; absent ⇒ light) —
|
|
71
|
+
* a mode switch re-paints from the retained other set with no
|
|
72
|
+
* network activity. Merged INTO the scoped in-body token block, the
|
|
73
|
+
* only placement that can recolor tree content (a `:root`-only
|
|
74
|
+
* append is cascade-dead for components — the scoped block sits
|
|
75
|
+
* later in document order and re-specifies every token; browser-
|
|
76
|
+
* verified, see the scoped-merge comment in `renderTree`). The
|
|
77
|
+
* `:root` append still happens, for body chrome only.
|
|
77
78
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
79
|
+
* Injection order (§3.3), one document-order cascade:
|
|
80
|
+
* compiled ladder (`themeId` ?? default) < `hostPalette` <
|
|
81
|
+
* `overlays[mode]` < `cssVariables` < `cssOverrides`, then
|
|
82
|
+
* `keyframes[mode]` and the frameless rule. Values are injection-
|
|
83
|
+
* safe by the wire schema — never re-validated here.
|
|
81
84
|
*/
|
|
82
|
-
readonly appTheme?:
|
|
83
|
-
readonly mode: 'light' | 'dark';
|
|
84
|
-
readonly cssVariables: Record<string, string>;
|
|
85
|
-
/**
|
|
86
|
-
* The REGISTERED base ladder, delivered over the wire (ggui#598-C
|
|
87
|
-
* runtime theme registration — see protocol's `appThemeSchema`).
|
|
88
|
-
* BOTH modes' resolved variable sets ride the envelope; when
|
|
89
|
-
* present, the mode-selected set REPLACES the compiled ladder
|
|
90
|
-
* block (`getScopedThemeCss`/`getScopedCssTokens`) at the base
|
|
91
|
-
* position of the #573 precedence above — delivery changes WHERE
|
|
92
|
-
* the ladder's bytes come from, never WHICH layer wins. Mode
|
|
93
|
-
* selection reuses the mount's already-resolved `themeMode` (one
|
|
94
|
-
* projection — no second mode resolution here), and a mode switch
|
|
95
|
-
* re-paints from the retained other set with no network activity.
|
|
96
|
-
* Values are injection-safe by the wire schema — never
|
|
97
|
-
* re-validated here; serialization is key-sorted for byte-stable
|
|
98
|
-
* output. `documentHash` is the registration's identity (joins a
|
|
99
|
-
* painted ladder to its registration record); the renderer
|
|
100
|
-
* carries it but exposes no observability surface for it today.
|
|
101
|
-
*/
|
|
102
|
-
readonly base?: NonNullable<AppTheme['base']>;
|
|
103
|
-
};
|
|
85
|
+
readonly appTheme?: AppTheme;
|
|
104
86
|
/**
|
|
105
87
|
* Host-announced palette, already translated onto `--ggui-*` tokens
|
|
106
88
|
* (see `host-palette-bridge.ts` — the spec `--color-*` vocabulary
|
package/dist/react-renderer.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import React, { Component, createElement, Fragment, } from 'react';
|
|
33
33
|
import { createRoot } from 'react-dom/client';
|
|
34
|
-
import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens,
|
|
34
|
+
import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, framelessSuppressionRule, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
|
|
35
35
|
import { hoistImports, loadModule, loadModuleInline, probeUrlModuleLoad, } from '@ggui-ai/design/module-loader';
|
|
36
36
|
/**
|
|
37
37
|
* Document-lifetime verdict: URL-scheme module loading (`blob:` main
|
|
@@ -109,7 +109,7 @@ class RcrErrorBoundary extends Component {
|
|
|
109
109
|
// The two fallback UIs match the host-SDK error boundary
|
|
110
110
|
// verbatim (same inline styles so operator-visible DOM is
|
|
111
111
|
// identical between the host-SDK and iframe-renderer paths).
|
|
112
|
-
// Colors inherit the themed body (`--ggui-color-
|
|
112
|
+
// Colors inherit the themed body (`--ggui-color-onContainer`) with
|
|
113
113
|
// opacity for hierarchy — hardcoded white-alpha text was
|
|
114
114
|
// illegible on light shells (#481); the boundary must stay
|
|
115
115
|
// readable on WHATEVER background the theme painted, including
|
|
@@ -126,7 +126,7 @@ class RcrErrorBoundary extends Component {
|
|
|
126
126
|
gap: 8,
|
|
127
127
|
fontFamily: FONT,
|
|
128
128
|
fontSize: 13,
|
|
129
|
-
color: 'var(--ggui-color-
|
|
129
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
130
130
|
opacity: 0.6,
|
|
131
131
|
},
|
|
132
132
|
}, createElement('div', {
|
|
@@ -151,7 +151,7 @@ class RcrErrorBoundary extends Component {
|
|
|
151
151
|
gap: 12,
|
|
152
152
|
textAlign: 'center',
|
|
153
153
|
fontFamily: FONT,
|
|
154
|
-
color: 'var(--ggui-color-
|
|
154
|
+
color: 'var(--ggui-color-onContainer, inherit)',
|
|
155
155
|
},
|
|
156
156
|
}, createElement('div', {
|
|
157
157
|
style: {
|
|
@@ -314,33 +314,15 @@ export async function mountReactRoot(container, opts) {
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
function renderTree(opts) {
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let themeCss = deliveredBase !== undefined
|
|
327
|
-
? // Design-package assembler (ggui#598-C, injection-review flag 1):
|
|
328
|
-
// the delivered ladder gets the SAME scaffolding the compiled
|
|
329
|
-
// ladder gets — base inherits, derived gradient/effect tokens
|
|
330
|
-
// (color-mix fallback split), box-sizing, font-inherit — from
|
|
331
|
-
// ONE shared builder, so the two paths cannot drift.
|
|
332
|
-
assembleDeliveredThemeCss(scopeClass, opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light, {
|
|
333
|
-
// ggui#613 residual 2 — the two former v1 deltas ride:
|
|
334
|
-
// the active mode's keyframes block + the frameless
|
|
335
|
-
// silhouette suppression, both same-rule as compiled.
|
|
336
|
-
keyframes: opts.themeMode === 'dark'
|
|
337
|
-
? deliveredBase.keyframes?.dark
|
|
338
|
-
: deliveredBase.keyframes?.light,
|
|
339
|
-
frameless: deliveredBase.frameless,
|
|
340
|
-
})
|
|
341
|
-
: opts.themeId
|
|
342
|
-
? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
|
|
343
|
-
: getScopedCssTokens(scopeClass, opts.themeMode);
|
|
317
|
+
// The effective mode (ggui#987 §4: the host owns it; the runtime
|
|
318
|
+
// resolves it ONCE into `themeMode`). Absent means no opinion
|
|
319
|
+
// anywhere — the light ladder paints, and every mode-keyed layer
|
|
320
|
+
// below selects with the same `m`, so the ladder, the overlay, the
|
|
321
|
+
// keyframes and `color-scheme` can never disagree.
|
|
322
|
+
const m = opts.themeMode ?? 'light';
|
|
323
|
+
let themeCss = opts.themeId
|
|
324
|
+
? getScopedThemeCss(opts.themeId, scopeClass, m)
|
|
325
|
+
: getScopedCssTokens(scopeClass, m);
|
|
344
326
|
if (opts.hostPalette) {
|
|
345
327
|
// Host palette = the fallback layer (ggui#572, #573 ruling):
|
|
346
328
|
// after the base scoped block so it can repaint at all (the base
|
|
@@ -349,22 +331,35 @@ export async function mountReactRoot(container, opts) {
|
|
|
349
331
|
// arrive pre-sanitized from the host-palette bridge.
|
|
350
332
|
themeCss += `.${scopeClass}{${toCssDecls(opts.hostPalette)}}`;
|
|
351
333
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
334
|
+
// The per-app theme's two variable layers, in this order: the
|
|
335
|
+
// derived projection for the effective mode, then the mode-agnostic
|
|
336
|
+
// `cssVariables` above it. Both must be merged HERE, into the
|
|
337
|
+
// scoped style that mounts INSIDE the scope div — the `:root`
|
|
338
|
+
// append below is cascade-dead for tree content: the in-scope block
|
|
339
|
+
// sits later in document order than anything in `<head>`, so at
|
|
340
|
+
// equal specificity the base scoped tokens always win over a head-
|
|
341
|
+
// level override (browser-verified in rnd/gen-ui/beauty/
|
|
342
|
+
// experiments/001, the probe sequence that found this). Appended
|
|
343
|
+
// AFTER the base block + host palette and BEFORE `cssOverrides`:
|
|
344
|
+
// compiled < host palette < overlays[m] < cssVariables < overrides
|
|
345
|
+
// (ggui#573: slice wins, host fallback). Values validated
|
|
346
|
+
// injection-safe upstream (same guarantee the `:root` append cites).
|
|
347
|
+
const appVarBlocks = opts.appTheme
|
|
348
|
+
? [opts.appTheme.overlays[m], opts.appTheme.cssVariables]
|
|
349
|
+
.filter((vars) => vars !== undefined)
|
|
350
|
+
.map((vars) => `.${scopeClass}{${toCssDecls(vars)}}`)
|
|
351
|
+
.join('')
|
|
352
|
+
: '';
|
|
353
|
+
themeCss += appVarBlocks;
|
|
354
|
+
// Trailing rules (§3.3): the effective mode's `@keyframes` block and
|
|
355
|
+
// the frameless silhouette suppression — the SAME rule text the
|
|
356
|
+
// compiled path emits for `$metadata.frameless` — after
|
|
357
|
+
// `cssOverrides`, so an override can never be shadowed by them.
|
|
358
|
+
// Keyframes ride VERBATIM: their admission is the write door's
|
|
359
|
+
// (`appThemeSchema.keyframes`), not re-checked here.
|
|
360
|
+
const appTrailingCss = opts.appTheme
|
|
361
|
+
? `${opts.appTheme.keyframes?.[m] ?? ''}${opts.appTheme.frameless === true ? framelessSuppressionRule(scopeClass) : ''}`
|
|
362
|
+
: '';
|
|
368
363
|
// Also inject theme CSS at `:root` on `document.head`. The scoped
|
|
369
364
|
// injection above isolates token resolution to the React tree, but
|
|
370
365
|
// the embedding shell's body styles (font-family, color, background)
|
|
@@ -375,17 +370,10 @@ export async function mountReactRoot(container, opts) {
|
|
|
375
370
|
// `ThemeProvider` so a Studio/Portal context with both runtime + RP
|
|
376
371
|
// active doesn't double-stack.
|
|
377
372
|
if (typeof document !== 'undefined') {
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
// compiled/default ladder regardless, and body chrome painted the
|
|
383
|
-
// default brand under a registered theme.
|
|
384
|
-
let rootCss = deliveredBase !== undefined
|
|
385
|
-
? `:root{${toCssDecls(opts.themeMode === 'dark' ? deliveredBase.dark : deliveredBase.light)}}`
|
|
386
|
-
: opts.themeId
|
|
387
|
-
? getThemeCss(opts.themeId, opts.themeMode)
|
|
388
|
-
: getCssTokens(opts.themeMode);
|
|
373
|
+
// Same ladder, same mode as the scoped block above — the
|
|
374
|
+
// standalone viewer's body chrome and its tree content resolve
|
|
375
|
+
// the same values.
|
|
376
|
+
let rootCss = opts.themeId ? getThemeCss(opts.themeId, m) : getCssTokens(m);
|
|
389
377
|
if (opts.hostPalette) {
|
|
390
378
|
// BODY-CHROME ONLY, same as the appTheme `:root` append below —
|
|
391
379
|
// and beneath it, mirroring the scoped-block order (#573:
|
|
@@ -393,19 +381,23 @@ export async function mountReactRoot(container, opts) {
|
|
|
393
381
|
// body font/color/background coherent with the tree.
|
|
394
382
|
rootCss += `:root{${toCssDecls(opts.hostPalette)}}`;
|
|
395
383
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
384
|
+
// BODY-CHROME ONLY. `color-scheme` follows the effective mode so
|
|
385
|
+
// native controls / scrollbars / canvas agree with the ladder
|
|
386
|
+
// painted; the app theme's two variable layers ride the same
|
|
387
|
+
// block, in the scoped order. None of this reaches tree content —
|
|
388
|
+
// the scoped in-body token block wins by document order (see the
|
|
389
|
+
// scoped merge above, and rnd/gen-ui/beauty/experiments/001 for
|
|
390
|
+
// the probe that falsified the old "inherits into the tree"
|
|
391
|
+
// claim). Values were validated injection-safe upstream (write-
|
|
392
|
+
// side `appThemeSchema` + the wire parser re-validates), so the
|
|
393
|
+
// string-join here is safe — do NOT re-sanitize.
|
|
394
|
+
const rootAppDecls = opts.appTheme
|
|
395
|
+
? [opts.appTheme.overlays[m], opts.appTheme.cssVariables]
|
|
396
|
+
.filter((vars) => vars !== undefined)
|
|
397
|
+
.map(toCssDecls)
|
|
398
|
+
.join('')
|
|
399
|
+
: '';
|
|
400
|
+
rootCss += `:root{color-scheme:${m};${rootAppDecls}}`;
|
|
409
401
|
const styleId = 'ggui-theme-vars';
|
|
410
402
|
let styleEl = document.getElementById(styleId);
|
|
411
403
|
if (!styleEl) {
|
|
@@ -426,7 +418,7 @@ export async function mountReactRoot(container, opts) {
|
|
|
426
418
|
// The scope `<div>` + inline `<style>` + error-boundary-wrapped
|
|
427
419
|
// children match RCR L376–383. `key={currentCode?.length}` mirrors
|
|
428
420
|
// RCR's "reset boundary when code changes" heuristic.
|
|
429
|
-
root.render(createElement('div', { className: scopeClass }, createElement('style', null, `${themeCss}${opts.cssOverrides ?? ''}`), createElement(RcrErrorBoundary, {
|
|
421
|
+
root.render(createElement('div', { className: scopeClass }, createElement('style', null, `${themeCss}${opts.cssOverrides ?? ''}${appTrailingCss}`), createElement(RcrErrorBoundary, {
|
|
430
422
|
key: currentCode?.length ?? 0,
|
|
431
423
|
...(opts.onError ? { onError: opts.onError } : {}),
|
|
432
424
|
}, createElement(Fragment, null, wrapped))));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AckPayload } from '@ggui-ai/protocol';
|
|
1
|
+
import type { AckPayload } from '@ggui-ai/protocol/wire';
|
|
2
2
|
import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
|
|
3
3
|
import type { McpAppAiGguiRenderMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
4
4
|
import type { AnyTransportHandle, ChannelLogger, ChannelRegistry, RegistryPollingOptions, RegistrySseOptions, WsTransportHandle } from '@ggui-ai/live-channel';
|
package/dist/render-item.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { AppTheme, GguiSession } from '@ggui-ai/protocol';
|
|
2
|
+
import type { AppTheme, GguiSession } from '@ggui-ai/protocol/wire';
|
|
3
3
|
import type { GguiSessionSeedInput } from './types.js';
|
|
4
4
|
import { type WireConfig } from '@ggui-ai/wire';
|
|
5
5
|
import type { StreamBus } from './wire-config.js';
|
|
@@ -47,20 +47,13 @@ export interface RenderItemOptions {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly themeMode?: 'light' | 'dark';
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* `
|
|
55
|
-
* verbatim; the renderer injects the mode-selected set in place of
|
|
56
|
-
* the compiled ladder block (see
|
|
57
|
-
* {@link ReactRootMountOptions.appTheme}).
|
|
50
|
+
* The per-app theme (protocol's `AppTheme`, ggui#987 v2) forwarded
|
|
51
|
+
* verbatim to {@link ReactRootMountOptions.appTheme}: the renderer
|
|
52
|
+
* injects the overlay for the effective `themeMode` into the scoped
|
|
53
|
+
* block and at `:root`. Sourced from the bootstrap's
|
|
54
|
+
* `_meta["ai.ggui/render"].theme`.
|
|
58
55
|
*/
|
|
59
|
-
readonly appTheme?:
|
|
60
|
-
readonly mode: 'light' | 'dark';
|
|
61
|
-
readonly cssVariables: Record<string, string>;
|
|
62
|
-
readonly base?: NonNullable<AppTheme['base']>;
|
|
63
|
-
};
|
|
56
|
+
readonly appTheme?: AppTheme;
|
|
64
57
|
/**
|
|
65
58
|
* Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
|
|
66
59
|
* host-palette bridge. Forwarded to
|