@ggui-ai/iframe-runtime 0.11.0 → 0.13.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/host-capabilities.d.ts +6 -4
- package/dist/iframe-runtime.js +70 -60
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/mcp-app-iframe-host.js +7 -2
- package/dist/observability.d.ts +2 -7
- package/dist/react-renderer.d.ts +22 -0
- package/dist/react-renderer.js +20 -4
- package/dist/render-item.d.ts +9 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +78 -30
- package/dist/status-dom.d.ts +9 -0
- package/dist/status-dom.js +11 -0
- package/dist/system-cards/UiFeedbackCard.d.ts +13 -8
- package/dist/system-cards/UiFeedbackCard.js +32 -41
- package/dist/ui-feedback-chrome.d.ts +46 -0
- package/dist/ui-feedback-chrome.js +42 -0
- package/dist/wire-config.d.ts +7 -3
- package/package.json +8 -8
|
@@ -77,8 +77,13 @@ export function mountMcpAppIframe(container, opts) {
|
|
|
77
77
|
iframe.style.maxWidth = dims.maxWidth !== undefined ? `${dims.maxWidth}px` : '100%';
|
|
78
78
|
if (dims.maxHeight !== undefined)
|
|
79
79
|
iframe.style.maxHeight = `${dims.maxHeight}px`;
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// Round-6 doctrine (ggui#589 @6e15724a1, kit C1 grade): the helper
|
|
81
|
+
// paints CONTAINMENT ONLY — silhouette (border/radius) belongs to
|
|
82
|
+
// the embedding host's own chrome, tokens to the theme. The former
|
|
83
|
+
// 1px #e5e5e5 / radius-8 here was the same ungoverned-chrome class
|
|
84
|
+
// the RN helper shipped, surfaced by the host-helper catalog's
|
|
85
|
+
// first-party run (ggui#600 OPEN FINDING, closed here).
|
|
86
|
+
iframe.style.border = 'none';
|
|
82
87
|
iframe.style.display = 'block';
|
|
83
88
|
container.appendChild(iframe);
|
|
84
89
|
// Cache contentWindow — on detach, `iframe.contentWindow` is null
|
package/dist/observability.d.ts
CHANGED
|
@@ -151,10 +151,7 @@ export interface ChannelPollRecoveredEvent {
|
|
|
151
151
|
* mirror `UiFeedbackPayload` in `@ggui-ai/mcp-apps-react` / `@ggui-ai/mcp-apps-react-native`
|
|
152
152
|
* (the host-chrome twin of this affordance):
|
|
153
153
|
*
|
|
154
|
-
* - `verdict` — `'
|
|
155
|
-
* free-text flow.
|
|
156
|
-
* - `comment` — present only for `verdict: 'other'` with a non-empty
|
|
157
|
-
* trimmed comment.
|
|
154
|
+
* - `verdict` — `'up'` / `'down'` (the two thumb verdicts, #653).
|
|
158
155
|
* - `sessionId` / `toolName` — present exactly when the runtime knew
|
|
159
156
|
* them at mount time.
|
|
160
157
|
*
|
|
@@ -175,9 +172,7 @@ export interface ChannelPollRecoveredEvent {
|
|
|
175
172
|
*/
|
|
176
173
|
export interface UiFeedbackEvent {
|
|
177
174
|
readonly kind: 'ui-feedback';
|
|
178
|
-
readonly verdict: '
|
|
179
|
-
/** Trimmed free-text comment; only on `verdict: 'other'`, never empty. */
|
|
180
|
-
readonly comment?: string;
|
|
175
|
+
readonly verdict: 'up' | 'down';
|
|
181
176
|
/** GguiSession id of the render the feedback is about. */
|
|
182
177
|
readonly sessionId?: string;
|
|
183
178
|
/** Tool that produced the render (e.g. `ggui_render`). */
|
package/dist/react-renderer.d.ts
CHANGED
|
@@ -81,6 +81,28 @@ export interface ReactRootMountOptions {
|
|
|
81
81
|
readonly appTheme?: {
|
|
82
82
|
readonly mode: 'light' | 'dark';
|
|
83
83
|
readonly cssVariables: Record<string, string>;
|
|
84
|
+
/**
|
|
85
|
+
* The REGISTERED base ladder, delivered over the wire (ggui#598-C
|
|
86
|
+
* runtime theme registration — see protocol's `appThemeSchema`).
|
|
87
|
+
* BOTH modes' resolved variable sets ride the envelope; when
|
|
88
|
+
* present, the mode-selected set REPLACES the compiled ladder
|
|
89
|
+
* block (`getScopedThemeCss`/`getScopedCssTokens`) at the base
|
|
90
|
+
* position of the #573 precedence above — delivery changes WHERE
|
|
91
|
+
* the ladder's bytes come from, never WHICH layer wins. Mode
|
|
92
|
+
* selection reuses the mount's already-resolved `themeMode` (one
|
|
93
|
+
* projection — no second mode resolution here), and a mode switch
|
|
94
|
+
* re-paints from the retained other set with no network activity.
|
|
95
|
+
* Values are injection-safe by the wire schema — never
|
|
96
|
+
* re-validated here; serialization is key-sorted for byte-stable
|
|
97
|
+
* output. `documentHash` is the registration's identity (joins a
|
|
98
|
+
* painted ladder to its registration record); the renderer
|
|
99
|
+
* carries it but exposes no observability surface for it today.
|
|
100
|
+
*/
|
|
101
|
+
readonly base?: {
|
|
102
|
+
readonly documentHash: string;
|
|
103
|
+
readonly light: Readonly<Record<string, string>>;
|
|
104
|
+
readonly dark: Readonly<Record<string, string>>;
|
|
105
|
+
};
|
|
84
106
|
};
|
|
85
107
|
/**
|
|
86
108
|
* Host-announced palette, already translated onto `--ggui-*` tokens
|
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, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
|
|
34
|
+
import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, assembleDeliveredThemeCss, 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
|
|
@@ -314,9 +314,25 @@ export async function mountReactRoot(container, opts) {
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
function renderTree(opts) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
// Delivered base ladder (ggui#598-C): when the slice theme carries
|
|
318
|
+
// `base`, the mode-selected set REPLACES the compiled ladder block
|
|
319
|
+
// at the base position — same precedence slot, different byte
|
|
320
|
+
// source. Mode selection reuses the already-resolved `themeMode`
|
|
321
|
+
// (one projection; absent means light — the same default
|
|
322
|
+
// `getScopedThemeCss` applies). Both sets stay retained on the
|
|
323
|
+
// options, so a mode-switch re-render picks the other ladder from
|
|
324
|
+
// memory — never the network.
|
|
325
|
+
const deliveredBase = opts.appTheme?.base;
|
|
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
|
+
: opts.themeId
|
|
334
|
+
? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
|
|
335
|
+
: getScopedCssTokens(scopeClass, opts.themeMode);
|
|
320
336
|
if (opts.hostPalette) {
|
|
321
337
|
// Host palette = the fallback layer (ggui#572, #573 ruling):
|
|
322
338
|
// after the base scoped block so it can repaint at all (the base
|
package/dist/render-item.d.ts
CHANGED
|
@@ -51,10 +51,19 @@ export interface RenderItemOptions {
|
|
|
51
51
|
* so the renderer injects the `--ggui-*` overrides + `color-scheme` at
|
|
52
52
|
* `:root`. Sourced from the bootstrap's `_meta["ai.ggui/render"].theme`.
|
|
53
53
|
* A structural subset of protocol's `AppTheme` (`name` is display-only).
|
|
54
|
+
* `base` is the delivered registered ladder (ggui#598-C) — forwarded
|
|
55
|
+
* verbatim; the renderer injects the mode-selected set in place of
|
|
56
|
+
* the compiled ladder block (see
|
|
57
|
+
* {@link ReactRootMountOptions.appTheme}).
|
|
54
58
|
*/
|
|
55
59
|
readonly appTheme?: {
|
|
56
60
|
readonly mode: 'light' | 'dark';
|
|
57
61
|
readonly cssVariables: Record<string, string>;
|
|
62
|
+
readonly base?: {
|
|
63
|
+
readonly documentHash: string;
|
|
64
|
+
readonly light: Readonly<Record<string, string>>;
|
|
65
|
+
readonly dark: Readonly<Record<string, string>>;
|
|
66
|
+
};
|
|
58
67
|
};
|
|
59
68
|
/**
|
|
60
69
|
* Host-announced palette, pre-mapped onto `--ggui-*` tokens by the
|
package/dist/runtime.d.ts
CHANGED
|
@@ -276,6 +276,18 @@ export interface BootSequenceOptions {
|
|
|
276
276
|
* or not this option is bound.
|
|
277
277
|
*/
|
|
278
278
|
readonly onObserve?: ObservabilityEmitter;
|
|
279
|
+
/**
|
|
280
|
+
* Opt-in knob for the in-iframe UI-feedback chrome (#653). DEFAULT
|
|
281
|
+
* OFF by founder ruling — a generated component card carries no
|
|
282
|
+
* feedback affordance unless the embedding deployment passes `true`
|
|
283
|
+
* here. Even when opted in, the #471 live-sink gate still applies
|
|
284
|
+
* (see `shouldMountUiFeedbackChrome`): hosts that drop the
|
|
285
|
+
* `ggui:observe` envelope never get the affordance. Chrome
|
|
286
|
+
* configuration, not wire data — the agent cannot observe this knob
|
|
287
|
+
* (data-vs-behavior), so it is a boot option, never a contract
|
|
288
|
+
* field.
|
|
289
|
+
*/
|
|
290
|
+
readonly uiFeedback?: boolean;
|
|
279
291
|
/**
|
|
280
292
|
* Optional {@link LifecycleEmitter} sink. Fires on every renderer
|
|
281
293
|
* mount-state transition (`mounting` → `code-ready` | `error`,
|
package/dist/runtime.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// schema-defining dependency initializes (see zod-jitless.ts).
|
|
29
29
|
import './zod-jitless.js';
|
|
30
30
|
import { MCP_APP_BOOTSTRAP_FAILED_TYPE, MCP_APP_RENDERER_READY_TYPE, parseMcpAppAiGguiRenderMeta, readGguiShellEnvelope, } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
31
|
+
import { effectiveThemeId, effectiveThemeMode, } from '@ggui-ai/protocol/integrations/theme-binding';
|
|
31
32
|
import { extractLocatorFromToolResult, parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './meta-parse.js';
|
|
32
33
|
import { projectHostContext } from '@ggui-ai/protocol';
|
|
33
34
|
import { App, PostMessageTransport } from '@modelcontextprotocol/ext-apps';
|
|
@@ -42,13 +43,13 @@ import { createChannelErrorHandler, createChannelPayloadHandler, createDataHandl
|
|
|
42
43
|
import { connectViaRegistry, } from './registry-subscribe.js';
|
|
43
44
|
import { buildBridgePolling, buildEventsPolling, createSequenceCursor, } from './events-polling.js';
|
|
44
45
|
import { unwrapCallToolResult } from './call-tool-unwrap.js';
|
|
45
|
-
import { ensureStatusDom, setStatus, setConnectedStatus, } from './status-dom.js';
|
|
46
|
+
import { ensureStatusDom, setStatus, setConnectedStatus, retireShellLoadingIndicator, } from './status-dom.js';
|
|
46
47
|
import { mergeReservedValidators, setActiveValidatorSet, } from './validation.js';
|
|
47
48
|
import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
|
|
48
49
|
import { mountRender, } from './render-item.js';
|
|
49
50
|
import { fromBootstrapFailure, } from './protocol-error.js';
|
|
50
51
|
import { postObservabilityToParent, } from './observability.js';
|
|
51
|
-
import { mountUiFeedbackChrome } from './ui-feedback-chrome.js';
|
|
52
|
+
import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome, shouldMountUiFeedbackChrome, } from './ui-feedback-chrome.js';
|
|
52
53
|
import { hostCanReceiveMessages, hostCanRelayToolCalls, hostCapabilitiesCaptured, setHostCapabilities, } from './host-capabilities.js';
|
|
53
54
|
import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
|
|
54
55
|
import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
|
|
@@ -213,7 +214,16 @@ export function hostAnnouncedThemeMode() {
|
|
|
213
214
|
* `buildOpts` inside `bootSequence`.
|
|
214
215
|
*/
|
|
215
216
|
export function resolveMountThemeMode(meta) {
|
|
216
|
-
|
|
217
|
+
// The CLIENT projection of the protocol's normative theme-binding
|
|
218
|
+
// total order (@ggui-ai/protocol/integrations/theme-binding,
|
|
219
|
+
// ggui#598 leg 4) — the server stamps via the sibling projection and
|
|
220
|
+
// the protocol's composition-law test pins that the two compose
|
|
221
|
+
// without rank drift.
|
|
222
|
+
return effectiveThemeMode({
|
|
223
|
+
stamped: meta.themeMode,
|
|
224
|
+
sessionSidecar: meta.theme?.mode,
|
|
225
|
+
hostAnnounced: hostAnnouncedThemeMode(),
|
|
226
|
+
});
|
|
217
227
|
}
|
|
218
228
|
/**
|
|
219
229
|
* Resolve the mount's base-theme id from the slice (ggui#589 ask 3):
|
|
@@ -235,7 +245,12 @@ export function resolveMountThemeMode(meta) {
|
|
|
235
245
|
* `buildOpts` inside `bootSequence`.
|
|
236
246
|
*/
|
|
237
247
|
export function resolveMountThemeId(meta) {
|
|
238
|
-
|
|
248
|
+
// Client projection of the normative theme-binding order — see
|
|
249
|
+
// resolveMountThemeMode above.
|
|
250
|
+
return effectiveThemeId({
|
|
251
|
+
stamped: meta.themeId,
|
|
252
|
+
sidecarName: meta.theme?.name,
|
|
253
|
+
});
|
|
239
254
|
}
|
|
240
255
|
/**
|
|
241
256
|
* The host's announced palette, read from the connected App's
|
|
@@ -320,7 +335,7 @@ function postBootFailure(reason, message) {
|
|
|
320
335
|
}
|
|
321
336
|
}
|
|
322
337
|
export async function bootSequence(opts) {
|
|
323
|
-
const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle } = opts;
|
|
338
|
+
const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle, uiFeedback } = opts;
|
|
324
339
|
const connectFn = opts.connectFn ?? connectViaRegistry;
|
|
325
340
|
const toolResultTimeoutMs = opts.toolResultTimeoutMs ?? POSTMESSAGE_BOOT_TIMEOUT_MS;
|
|
326
341
|
// Emit typed {@link ProtocolError} for every bootstrap-failure site
|
|
@@ -400,6 +415,7 @@ export async function bootSequence(opts) {
|
|
|
400
415
|
const initResult = await connectApp(app, transport);
|
|
401
416
|
if (!initResult.ok) {
|
|
402
417
|
setStatus(refs, `ui/initialize failed: ${initResult.message}`, 'error');
|
|
418
|
+
retireShellLoadingIndicator(doc);
|
|
403
419
|
emitBootFailure('UI_INITIALIZE_FAILED', initResult.message);
|
|
404
420
|
return { ok: false, mountedRender };
|
|
405
421
|
}
|
|
@@ -496,6 +512,7 @@ export async function bootSequence(opts) {
|
|
|
496
512
|
if (!parsed.ok) {
|
|
497
513
|
const message = `slice-meta parse failed: ${parsed.reason}`;
|
|
498
514
|
setStatus(refs, message, 'error');
|
|
515
|
+
retireShellLoadingIndicator(doc);
|
|
499
516
|
emitBootFailure(parsed.reason, message);
|
|
500
517
|
return { ok: false, mountedRender };
|
|
501
518
|
}
|
|
@@ -532,10 +549,12 @@ export async function bootSequence(opts) {
|
|
|
532
549
|
...(onObserve !== undefined ? { onObserve } : {}),
|
|
533
550
|
})
|
|
534
551
|
: null;
|
|
535
|
-
// In-iframe UI-feedback affordance (ggui#244) —
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
552
|
+
// In-iframe UI-feedback affordance (ggui#244) — DEFAULT OFF since
|
|
553
|
+
// #653 (founder ruling): mounts only when the boot options opt in
|
|
554
|
+
// via `uiFeedback: true`. When opted in it is additionally gated
|
|
555
|
+
// inside the helper on `window.parent !== window` (top-level
|
|
556
|
+
// `/r/<shortCode>` tabs have no `ggui:observe` egress, so they get
|
|
557
|
+
// NO affordance) AND here on a LIVE SINK: an
|
|
539
558
|
// injected `onObserve` emitter, or a first-party embed host — the
|
|
540
559
|
// only parent that consumes the `ggui:observe` envelope
|
|
541
560
|
// (mcp-app-iframe-host answers `ui/initialize` with exactly this
|
|
@@ -545,9 +564,12 @@ export async function bootSequence(opts) {
|
|
|
545
564
|
// retest surfaced it as unstyled clutter that could never deliver
|
|
546
565
|
// feedback anywhere. Fire-and-forget: chrome never blocks or fails
|
|
547
566
|
// the boot.
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
567
|
+
const mountFeedback = shouldMountUiFeedbackChrome({
|
|
568
|
+
uiFeedback,
|
|
569
|
+
onObserveBound: onObserve !== undefined,
|
|
570
|
+
hostName: app.getHostVersion()?.name,
|
|
571
|
+
});
|
|
572
|
+
if (mountFeedback) {
|
|
551
573
|
void mountUiFeedbackChrome(doc, {
|
|
552
574
|
emit: onObserve ?? postObservabilityToParent,
|
|
553
575
|
sessionId: meta.sessionId,
|
|
@@ -615,6 +637,9 @@ export async function bootSequence(opts) {
|
|
|
615
637
|
if (codeReadyEmitted)
|
|
616
638
|
return;
|
|
617
639
|
codeReadyEmitted = true;
|
|
640
|
+
// First real paint — the served shell's working-state mark (#667)
|
|
641
|
+
// becomes a lie the moment content lands.
|
|
642
|
+
retireShellLoadingIndicator(doc);
|
|
618
643
|
onLifecycle?.(makeLifecycleEvent('code-ready', { sessionId: meta.sessionId }));
|
|
619
644
|
};
|
|
620
645
|
// Mode discriminators. The mount surface is DECOUPLED from the live
|
|
@@ -706,6 +731,7 @@ export async function bootSequence(opts) {
|
|
|
706
731
|
if (!hasLiveTrio && mountedRender === null) {
|
|
707
732
|
const message = 'bootstrap carries neither static content (codeUrl/codeB64/kind) nor a live trio (wsUrl/wsToken)';
|
|
708
733
|
setStatus(refs, message, 'error');
|
|
734
|
+
retireShellLoadingIndicator(doc);
|
|
709
735
|
emitBootFailure('MISSING_META_GGUI_BOOTSTRAP', message);
|
|
710
736
|
return { ok: false, mountedRender };
|
|
711
737
|
}
|
|
@@ -1630,9 +1656,11 @@ export function emitAudit(args) {
|
|
|
1630
1656
|
* Returns `'success'` when the handler said `ok:true`. Returns
|
|
1631
1657
|
* `'fallback'` for any other outcome — pipe missing
|
|
1632
1658
|
* (`PIPE_NOT_FOUND`), envelope rejected (`INVALID_ACTION_KIND`),
|
|
1633
|
-
* relay error, no host relay wired, or any unexpected shape.
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1659
|
+
* relay error, no host relay wired, or any unexpected shape. On
|
|
1660
|
+
* `'fallback'` the gesture was NOT enqueued and is dropped with local
|
|
1661
|
+
* feedback only (toast / #440 latch) — no `ui/message` fires (the
|
|
1662
|
+
* doorbell is success-path-only: there is nothing to point it at), and
|
|
1663
|
+
* no alternate transport is attempted today (ggui#599's open leg).
|
|
1636
1664
|
*/
|
|
1637
1665
|
/**
|
|
1638
1666
|
* Unwrap the payload record of a relayed submit-action tool result.
|
|
@@ -1698,6 +1726,16 @@ function isRelayShapedFailure(resp) {
|
|
|
1698
1726
|
* consumer" — the doorbell rings. Shares {@link submitActionPayload}
|
|
1699
1727
|
* with {@link classifySubmitActionResponse} so both reads agree on
|
|
1700
1728
|
* which envelope tier carries the payload.
|
|
1729
|
+
*
|
|
1730
|
+
* RELAY CONTRACT (ggui#603): this read only works when the host's
|
|
1731
|
+
* `tools/call` relay threads the server's tool result VERBATIM — the
|
|
1732
|
+
* `consumerPresent` marker rides `structuredContent` on the real
|
|
1733
|
+
* result. A relay that wraps or synthesizes the response silently
|
|
1734
|
+
* strips the marker, and every healthy gesture then misclassifies as
|
|
1735
|
+
* "agent not listening" (a persistent `action_required` toast per
|
|
1736
|
+
* gesture). Verbatim threading is the same obligation that keeps
|
|
1737
|
+
* `_meta` alive on `resources/read` relays; a conformance vector for
|
|
1738
|
+
* it is tracked on ggui#603.
|
|
1701
1739
|
*/
|
|
1702
1740
|
function extractConsumerPresent(resp) {
|
|
1703
1741
|
const inner = submitActionPayload(resp);
|
|
@@ -1908,7 +1946,7 @@ function showActionToast(text, kind, onDismiss) {
|
|
|
1908
1946
|
'bottom:16px',
|
|
1909
1947
|
'left:50%',
|
|
1910
1948
|
'transform:translateX(-50%)',
|
|
1911
|
-
|
|
1949
|
+
`z-index:${ACTION_TOAST_Z_INDEX}`,
|
|
1912
1950
|
'pointer-events:auto',
|
|
1913
1951
|
'max-width:90%',
|
|
1914
1952
|
'padding:8px 14px',
|
|
@@ -3404,20 +3442,30 @@ async function bootProduction(opts) {
|
|
|
3404
3442
|
}
|
|
3405
3443
|
},
|
|
3406
3444
|
};
|
|
3407
|
-
// Spec-canonical outbound dispatch
|
|
3408
|
-
//
|
|
3409
|
-
// `
|
|
3410
|
-
//
|
|
3411
|
-
//
|
|
3412
|
-
//
|
|
3413
|
-
//
|
|
3414
|
-
// `
|
|
3415
|
-
//
|
|
3416
|
-
// `
|
|
3417
|
-
//
|
|
3418
|
-
//
|
|
3419
|
-
//
|
|
3420
|
-
//
|
|
3445
|
+
// Spec-canonical outbound dispatch: user actions go through the
|
|
3446
|
+
// MCP-Apps host relay (SPEC §4.7): postMessage
|
|
3447
|
+
// `tools/call:ggui_runtime_submit_action` to the parent →
|
|
3448
|
+
// `AppRenderer.onCallTool` → host's MCP client → ggui MCP server
|
|
3449
|
+
// → `createGguiSubmitActionHandler.append` → `pendingEventConsumer`
|
|
3450
|
+
// → `ggui_consume` wakes the agent. NOTE (truth, ggui#596 +
|
|
3451
|
+
// adversarial-cycle correction): the server's WS ingress
|
|
3452
|
+
// dual-writes accepted `data:submit` frames onto the consume pipe
|
|
3453
|
+
// ONLY when the live channel is composed with a
|
|
3454
|
+
// `pendingEventConsumer` — which `createGguiServer` threads for
|
|
3455
|
+
// DEFAULT-handlers compositions only (a custom `handlers` list
|
|
3456
|
+
// drains its own pipe; bridging the channel onto an instance
|
|
3457
|
+
// nobody drains would buffer gestures into the void — see the
|
|
3458
|
+
// hoist note in server.ts). The hosted pod IS a custom-handlers
|
|
3459
|
+
// caller: on it, a WS gesture lands on the retained ledger only
|
|
3460
|
+
// and never reaches the agent. Any WS action fallback (ggui#599's
|
|
3461
|
+
// open design leg) therefore requires the server/pod bridge
|
|
3462
|
+
// FIRST — it is a capability gap on that composition today. The WS pipe otherwise carries
|
|
3463
|
+
// streamSpec subscriptions (inbound `ggui_emit` fanout +
|
|
3464
|
+
// `props_update` + `render` + `data` + `drain_ack` +
|
|
3465
|
+
// `channel_payload`). `routeDispatch` is the shared named-export
|
|
3466
|
+
// helper (production + tests exercise the same code path);
|
|
3467
|
+
// threading it here keeps LIVE-mode on the canonical dispatch
|
|
3468
|
+
// pipeline.
|
|
3421
3469
|
// Single mounted render — populated by `applyRender` on the first
|
|
3422
3470
|
// render frame. The wire config + data channel handler read it
|
|
3423
3471
|
// through `currentRender`-returning thunks so they always see the
|
package/dist/status-dom.d.ts
CHANGED
|
@@ -75,3 +75,12 @@ export declare function setStatus(refs: StatusRefs, text: string, state: StatusK
|
|
|
75
75
|
* intercepting `console.log`) get a stable string on every render.
|
|
76
76
|
*/
|
|
77
77
|
export declare function setConnectedStatus(refs: StatusRefs): void;
|
|
78
|
+
/**
|
|
79
|
+
* Retire the served shell's pre-render working-state mark (#667 —
|
|
80
|
+
* `[data-ggui-shell-loading]`, the ggui glyph treatment or an
|
|
81
|
+
* operator's court block). Called at `code-ready` (the first real
|
|
82
|
+
* paint makes a loading mark a lie) and on terminal boot failure (the
|
|
83
|
+
* failure card must not sit under an "everything is fine" animation).
|
|
84
|
+
* Idempotent; a shell without the mark is a no-op.
|
|
85
|
+
*/
|
|
86
|
+
export declare function retireShellLoadingIndicator(doc: Document): void;
|
package/dist/status-dom.js
CHANGED
|
@@ -66,3 +66,14 @@ export function setStatus(refs, text, state) {
|
|
|
66
66
|
export function setConnectedStatus(refs) {
|
|
67
67
|
setStatus(refs, 'Connected.', 'connected');
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Retire the served shell's pre-render working-state mark (#667 —
|
|
71
|
+
* `[data-ggui-shell-loading]`, the ggui glyph treatment or an
|
|
72
|
+
* operator's court block). Called at `code-ready` (the first real
|
|
73
|
+
* paint makes a loading mark a lie) and on terminal boot failure (the
|
|
74
|
+
* failure card must not sit under an "everything is fine" animation).
|
|
75
|
+
* Idempotent; a shell without the mark is a no-op.
|
|
76
|
+
*/
|
|
77
|
+
export function retireShellLoadingIndicator(doc) {
|
|
78
|
+
doc.querySelector('[data-ggui-shell-loading]')?.remove();
|
|
79
|
+
}
|
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
* ("did this generated UI work for you?").
|
|
4
4
|
*
|
|
5
5
|
* The in-iframe twin of the host-chrome `UiFeedback` component in
|
|
6
|
-
* `@ggui-ai/mcp-apps-react`: same
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* `@ggui-ai/mcp-apps-react`: same two verdicts — thumbs up / thumbs
|
|
7
|
+
* down (#653) — same acknowledgement after submit, same dismiss
|
|
8
|
+
* control, same payload semantics (context stamps present exactly when
|
|
9
|
+
* known). Where the host-chrome twin hands a payload to an
|
|
10
|
+
* `onUiFeedback` callback, this card builds a {@link UiFeedbackEvent}
|
|
11
|
+
* and hands it to the injected {@link ObservabilityEmitter} —
|
|
12
|
+
* production binds the `ggui:observe` postMessage-to-parent default,
|
|
13
|
+
* tests record.
|
|
14
|
+
*
|
|
15
|
+
* The verdict buttons are icon-only: monochrome stroked SVGs drawn in
|
|
16
|
+
* `currentColor`, so the icon ink follows the button's `--ggui-*`
|
|
17
|
+
* color token in both modes (the #653 theme-applicability
|
|
18
|
+
* requirement). The verdict's name lives in the `aria-label`.
|
|
14
19
|
*
|
|
15
20
|
* This is CHROME the runtime mounts adjacent to the session root
|
|
16
21
|
* (see `ui-feedback-chrome.ts`), NOT a system card: it has no
|
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* UiFeedbackCard — the runtime's in-iframe UI-feedback affordance
|
|
4
4
|
* ("did this generated UI work for you?").
|
|
5
5
|
*
|
|
6
6
|
* The in-iframe twin of the host-chrome `UiFeedback` component in
|
|
7
|
-
* `@ggui-ai/mcp-apps-react`: same
|
|
8
|
-
* (
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* `@ggui-ai/mcp-apps-react`: same two verdicts — thumbs up / thumbs
|
|
8
|
+
* down (#653) — same acknowledgement after submit, same dismiss
|
|
9
|
+
* control, same payload semantics (context stamps present exactly when
|
|
10
|
+
* known). Where the host-chrome twin hands a payload to an
|
|
11
|
+
* `onUiFeedback` callback, this card builds a {@link UiFeedbackEvent}
|
|
12
|
+
* and hands it to the injected {@link ObservabilityEmitter} —
|
|
13
|
+
* production binds the `ggui:observe` postMessage-to-parent default,
|
|
14
|
+
* tests record.
|
|
15
|
+
*
|
|
16
|
+
* The verdict buttons are icon-only: monochrome stroked SVGs drawn in
|
|
17
|
+
* `currentColor`, so the icon ink follows the button's `--ggui-*`
|
|
18
|
+
* color token in both modes (the #653 theme-applicability
|
|
19
|
+
* requirement). The verdict's name lives in the `aria-label`.
|
|
15
20
|
*
|
|
16
21
|
* This is CHROME the runtime mounts adjacent to the session root
|
|
17
22
|
* (see `ui-feedback-chrome.ts`), NOT a system card: it has no
|
|
@@ -56,6 +61,8 @@ function buildStyles(scheme) {
|
|
|
56
61
|
},
|
|
57
62
|
verdictButton: {
|
|
58
63
|
appearance: 'none',
|
|
64
|
+
display: 'inline-flex',
|
|
65
|
+
alignItems: 'center',
|
|
59
66
|
background: dark
|
|
60
67
|
? 'var(--ggui-color-neutral-50, #1f2937)'
|
|
61
68
|
: 'var(--ggui-color-neutral-50, #f9fafb)',
|
|
@@ -63,9 +70,10 @@ function buildStyles(scheme) {
|
|
|
63
70
|
? '1px solid var(--ggui-color-neutral-200, #374151)'
|
|
64
71
|
: '1px solid var(--ggui-color-neutral-200, #e5e7eb)',
|
|
65
72
|
borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
|
|
66
|
-
padding: '
|
|
73
|
+
padding: '3px 7px',
|
|
67
74
|
fontFamily: 'inherit',
|
|
68
75
|
fontSize: 'inherit',
|
|
76
|
+
// The icons stroke in currentColor — this token IS the icon ink.
|
|
69
77
|
color: dark
|
|
70
78
|
? 'var(--ggui-color-neutral-600, #d1d5db)'
|
|
71
79
|
: 'var(--ggui-color-neutral-600, #4b5563)',
|
|
@@ -83,51 +91,34 @@ function buildStyles(scheme) {
|
|
|
83
91
|
: 'var(--ggui-color-neutral-400, #9ca3af)',
|
|
84
92
|
cursor: 'pointer',
|
|
85
93
|
},
|
|
86
|
-
commentForm: {
|
|
87
|
-
display: 'inline-flex',
|
|
88
|
-
alignItems: 'center',
|
|
89
|
-
gap: 4,
|
|
90
|
-
margin: 0,
|
|
91
|
-
},
|
|
92
|
-
commentInput: {
|
|
93
|
-
fontFamily: 'inherit',
|
|
94
|
-
fontSize: 'inherit',
|
|
95
|
-
padding: '2px 6px',
|
|
96
|
-
minWidth: 140,
|
|
97
|
-
border: dark
|
|
98
|
-
? '1px solid var(--ggui-color-neutral-200, #374151)'
|
|
99
|
-
: '1px solid var(--ggui-color-neutral-200, #e5e7eb)',
|
|
100
|
-
borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
|
|
101
|
-
background: dark
|
|
102
|
-
? 'var(--ggui-color-neutral-50, #111827)'
|
|
103
|
-
: 'var(--ggui-color-neutral-50, #ffffff)',
|
|
104
|
-
color: dark
|
|
105
|
-
? 'var(--ggui-color-neutral-800, #e5e7eb)'
|
|
106
|
-
: 'var(--ggui-color-neutral-800, #1f2937)',
|
|
107
|
-
},
|
|
108
94
|
};
|
|
109
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Monochrome stroked thumb icons. `stroke="currentColor"` is the
|
|
98
|
+
* theme-applicability contract: the glyph takes whatever ink the
|
|
99
|
+
* button's `color` token resolves to, in either mode.
|
|
100
|
+
*/
|
|
101
|
+
function ThumbIcon({ direction, }) {
|
|
102
|
+
return (_jsx("svg", { width: 13, height: 13, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: direction === 'up' ? (_jsxs(_Fragment, { children: [_jsx("path", { d: "M7 10v12" }), _jsx("path", { d: "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z" })] })) : (_jsxs(_Fragment, { children: [_jsx("path", { d: "M17 14V2" }), _jsx("path", { d: "M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z" })] })) }));
|
|
103
|
+
}
|
|
104
|
+
const VERDICT_LABELS = {
|
|
105
|
+
up: 'Thumbs up',
|
|
106
|
+
down: 'Thumbs down',
|
|
107
|
+
};
|
|
110
108
|
export function UiFeedbackCard({ emit, sessionId, toolName, }) {
|
|
111
109
|
const [phase, setPhase] = React.useState('idle');
|
|
112
|
-
const [comment, setComment] = React.useState('');
|
|
113
110
|
const scheme = useColorScheme();
|
|
114
111
|
const styles = buildStyles(scheme);
|
|
115
|
-
const send = React.useCallback((verdict
|
|
116
|
-
const trimmed = commentText?.trim();
|
|
112
|
+
const send = React.useCallback((verdict) => {
|
|
117
113
|
emit({
|
|
118
114
|
kind: 'ui-feedback',
|
|
119
115
|
verdict,
|
|
120
|
-
...(trimmed !== undefined && trimmed.length > 0 ? { comment: trimmed } : {}),
|
|
121
116
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
122
117
|
...(toolName !== undefined ? { toolName } : {}),
|
|
123
118
|
});
|
|
124
119
|
setPhase('sent');
|
|
125
120
|
}, [emit, sessionId, toolName]);
|
|
126
|
-
const onCommentSubmit = React.useCallback((e) => {
|
|
127
|
-
e.preventDefault();
|
|
128
|
-
send('other', comment);
|
|
129
|
-
}, [send, comment]);
|
|
130
121
|
if (phase === 'dismissed')
|
|
131
122
|
return null;
|
|
132
|
-
return (_jsxs("div", { "data-ggui-ui-feedback": true, role: "group", "aria-label": "Share feedback on this UI", style: styles.root, children: [phase === 'sent' ? (_jsx("span", { "data-ggui-ui-feedback-thanks": true, children: "Thanks for the feedback" })) :
|
|
123
|
+
return (_jsxs("div", { "data-ggui-ui-feedback": true, role: "group", "aria-label": "Share feedback on this UI", style: styles.root, children: [phase === 'sent' ? (_jsx("span", { "data-ggui-ui-feedback-thanks": true, children: "Thanks for the feedback" })) : (['up', 'down'].map((verdict) => (_jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": verdict, "aria-label": VERDICT_LABELS[verdict], title: VERDICT_LABELS[verdict], onClick: () => send(verdict), children: _jsx(ThumbIcon, { direction: verdict }) }, verdict)))), _jsx("button", { type: "button", style: styles.dismissButton, "aria-label": "Dismiss feedback", "data-ggui-ui-feedback-dismiss": true, onClick: () => setPhase('dismissed'), children: '×' })] }));
|
|
133
124
|
}
|
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import type { ObservabilityEmitter } from './observability.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stacking contract between the two body-appended runtime layers
|
|
4
|
+
* (ggui#603): the feedback chrome must sit strictly ABOVE the action
|
|
5
|
+
* toast. The persistent `action_required` toast parks
|
|
6
|
+
* `pointer-events:auto` at fixed bottom-center — the same region this
|
|
7
|
+
* chrome occupies — and is re-armed per gesture; without this order an
|
|
8
|
+
* armed toast eats every tap aimed at the feedback controls. The toast
|
|
9
|
+
* remains dismissable on its own uncovered area (it accepts the click
|
|
10
|
+
* anywhere on itself), so partial occlusion costs it nothing.
|
|
11
|
+
*
|
|
12
|
+
* Single source: `runtime.ts`'s toast reads {@link ACTION_TOAST_Z_INDEX};
|
|
13
|
+
* this module applies {@link UI_FEEDBACK_CHROME_Z_INDEX} to the chrome
|
|
14
|
+
* container. The test suite pins the relationship, not the numbers.
|
|
15
|
+
*/
|
|
16
|
+
export declare const UI_FEEDBACK_CHROME_Z_INDEX = 2147483647;
|
|
17
|
+
export declare const ACTION_TOAST_Z_INDEX = 2147483646;
|
|
18
|
+
/** Inputs to the boot path's mount decision — see
|
|
19
|
+
* {@link shouldMountUiFeedbackChrome}. */
|
|
20
|
+
export interface UiFeedbackMountDecisionInput {
|
|
21
|
+
/**
|
|
22
|
+
* The opt-in knob from the boot options (#653). Feedback chrome is
|
|
23
|
+
* DEFAULT OFF: only an explicit `true` can mount it.
|
|
24
|
+
*/
|
|
25
|
+
readonly uiFeedback: boolean | undefined;
|
|
26
|
+
/** Whether the boot bound an `onObserve` emitter. */
|
|
27
|
+
readonly onObserveBound: boolean;
|
|
28
|
+
/** The MCP-Apps host's announced name, when known. */
|
|
29
|
+
readonly hostName: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pure mount decision the boot path consults before touching the DOM.
|
|
33
|
+
* Two independent gates compose:
|
|
34
|
+
*
|
|
35
|
+
* 1. **The #653 opt-in knob** — the founder's default-off ruling:
|
|
36
|
+
* generated-component cards carry NO feedback chrome unless the
|
|
37
|
+
* embedding deployment explicitly opts in (`uiFeedback: true` on
|
|
38
|
+
* the boot options). Absent or `false` ⇒ never mount, regardless
|
|
39
|
+
* of sink state.
|
|
40
|
+
* 2. **The #471 live-sink gate** — even opted in, only a parent that
|
|
41
|
+
* actually consumes the `ggui:observe` envelope gets the
|
|
42
|
+
* affordance: a bound `onObserve` emitter, or the first-party
|
|
43
|
+
* embed host. Third-party MCP-Apps hosts drop the envelope, and a
|
|
44
|
+
* feedback control that can never deliver feedback is worse than
|
|
45
|
+
* none.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shouldMountUiFeedbackChrome(input: UiFeedbackMountDecisionInput): boolean;
|
|
2
48
|
export interface UiFeedbackChromeOptions {
|
|
3
49
|
/**
|
|
4
50
|
* Observability sink the card emits `ui-feedback` events into.
|