@ggui-ai/iframe-runtime 0.12.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.
@@ -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` — `'love'` / `'dislike'`, or `'other'` for the
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: 'love' | 'dislike' | 'other';
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/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
@@ -43,13 +43,13 @@ import { createChannelErrorHandler, createChannelPayloadHandler, createDataHandl
43
43
  import { connectViaRegistry, } from './registry-subscribe.js';
44
44
  import { buildBridgePolling, buildEventsPolling, createSequenceCursor, } from './events-polling.js';
45
45
  import { unwrapCallToolResult } from './call-tool-unwrap.js';
46
- import { ensureStatusDom, setStatus, setConnectedStatus, } from './status-dom.js';
46
+ import { ensureStatusDom, setStatus, setConnectedStatus, retireShellLoadingIndicator, } from './status-dom.js';
47
47
  import { mergeReservedValidators, setActiveValidatorSet, } from './validation.js';
48
48
  import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
49
49
  import { mountRender, } from './render-item.js';
50
50
  import { fromBootstrapFailure, } from './protocol-error.js';
51
51
  import { postObservabilityToParent, } from './observability.js';
52
- import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome } from './ui-feedback-chrome.js';
52
+ import { ACTION_TOAST_Z_INDEX, mountUiFeedbackChrome, shouldMountUiFeedbackChrome, } from './ui-feedback-chrome.js';
53
53
  import { hostCanReceiveMessages, hostCanRelayToolCalls, hostCapabilitiesCaptured, setHostCapabilities, } from './host-capabilities.js';
54
54
  import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
55
55
  import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
@@ -335,7 +335,7 @@ function postBootFailure(reason, message) {
335
335
  }
336
336
  }
337
337
  export async function bootSequence(opts) {
338
- const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle } = opts;
338
+ const { doc, app, transport, notifyParent, renderer: rendererHooks, onProtocolError, onObserve, onLifecycle, uiFeedback } = opts;
339
339
  const connectFn = opts.connectFn ?? connectViaRegistry;
340
340
  const toolResultTimeoutMs = opts.toolResultTimeoutMs ?? POSTMESSAGE_BOOT_TIMEOUT_MS;
341
341
  // Emit typed {@link ProtocolError} for every bootstrap-failure site
@@ -415,6 +415,7 @@ export async function bootSequence(opts) {
415
415
  const initResult = await connectApp(app, transport);
416
416
  if (!initResult.ok) {
417
417
  setStatus(refs, `ui/initialize failed: ${initResult.message}`, 'error');
418
+ retireShellLoadingIndicator(doc);
418
419
  emitBootFailure('UI_INITIALIZE_FAILED', initResult.message);
419
420
  return { ok: false, mountedRender };
420
421
  }
@@ -511,6 +512,7 @@ export async function bootSequence(opts) {
511
512
  if (!parsed.ok) {
512
513
  const message = `slice-meta parse failed: ${parsed.reason}`;
513
514
  setStatus(refs, message, 'error');
515
+ retireShellLoadingIndicator(doc);
514
516
  emitBootFailure(parsed.reason, message);
515
517
  return { ok: false, mountedRender };
516
518
  }
@@ -547,10 +549,12 @@ export async function bootSequence(opts) {
547
549
  ...(onObserve !== undefined ? { onObserve } : {}),
548
550
  })
549
551
  : null;
550
- // In-iframe UI-feedback affordance (ggui#244) — mounted adjacent to
551
- // the session root, gated inside the helper on `window.parent !==
552
- // window` (top-level `/r/<shortCode>` tabs have no `ggui:observe`
553
- // egress, so they get NO affordance) AND here on a LIVE SINK: an
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
554
558
  // injected `onObserve` emitter, or a first-party embed host — the
555
559
  // only parent that consumes the `ggui:observe` envelope
556
560
  // (mcp-app-iframe-host answers `ui/initialize` with exactly this
@@ -560,9 +564,12 @@ export async function bootSequence(opts) {
560
564
  // retest surfaced it as unstyled clutter that could never deliver
561
565
  // feedback anywhere. Fire-and-forget: chrome never blocks or fails
562
566
  // the boot.
563
- const feedbackSinkLive = onObserve !== undefined ||
564
- app.getHostVersion()?.name === 'ggui-iframe-runtime-embed-host';
565
- if (feedbackSinkLive) {
567
+ const mountFeedback = shouldMountUiFeedbackChrome({
568
+ uiFeedback,
569
+ onObserveBound: onObserve !== undefined,
570
+ hostName: app.getHostVersion()?.name,
571
+ });
572
+ if (mountFeedback) {
566
573
  void mountUiFeedbackChrome(doc, {
567
574
  emit: onObserve ?? postObservabilityToParent,
568
575
  sessionId: meta.sessionId,
@@ -630,6 +637,9 @@ export async function bootSequence(opts) {
630
637
  if (codeReadyEmitted)
631
638
  return;
632
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);
633
643
  onLifecycle?.(makeLifecycleEvent('code-ready', { sessionId: meta.sessionId }));
634
644
  };
635
645
  // Mode discriminators. The mount surface is DECOUPLED from the live
@@ -721,6 +731,7 @@ export async function bootSequence(opts) {
721
731
  if (!hasLiveTrio && mountedRender === null) {
722
732
  const message = 'bootstrap carries neither static content (codeUrl/codeB64/kind) nor a live trio (wsUrl/wsToken)';
723
733
  setStatus(refs, message, 'error');
734
+ retireShellLoadingIndicator(doc);
724
735
  emitBootFailure('MISSING_META_GGUI_BOOTSTRAP', message);
725
736
  return { ok: false, mountedRender };
726
737
  }
@@ -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;
@@ -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 three verdicts — Love / Dislike / Other
7
- * (free-text comment) — same acknowledgement after submit, same
8
- * dismiss control, same payload semantics (trimmed comment only on
9
- * `verdict: 'other'`, context stamps present exactly when known).
10
- * Where the host-chrome twin hands a payload to an `onUiFeedback`
11
- * callback, this card builds a {@link UiFeedbackEvent} and hands it to
12
- * the injected {@link ObservabilityEmitter} — production binds the
13
- * `ggui:observe` postMessage-to-parent default, tests record.
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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 three verdicts — Love / Dislike / Other
8
- * (free-text comment) — same acknowledgement after submit, same
9
- * dismiss control, same payload semantics (trimmed comment only on
10
- * `verdict: 'other'`, context stamps present exactly when known).
11
- * Where the host-chrome twin hands a payload to an `onUiFeedback`
12
- * callback, this card builds a {@link UiFeedbackEvent} and hands it to
13
- * the injected {@link ObservabilityEmitter} — production binds the
14
- * `ggui:observe` postMessage-to-parent default, tests record.
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: '2px 8px',
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, commentText) => {
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" })) : phase === 'comment' ? (_jsxs("form", { onSubmit: onCommentSubmit, style: styles.commentForm, "data-ggui-ui-feedback-comment-form": true, children: [_jsx("input", { type: "text", value: comment, onChange: (e) => setComment(e.target.value), placeholder: "What happened?", "aria-label": "Feedback comment", "data-ggui-ui-feedback-comment": true, style: styles.commentInput, autoFocus: true }), _jsx("button", { type: "submit", style: styles.verdictButton, "data-ggui-ui-feedback-send": true, children: "Send" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "love", onClick: () => send('love'), children: "Love" }), _jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "dislike", onClick: () => send('dislike'), children: "Dislike" }), _jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "other", onClick: () => setPhase('comment'), children: "Other\u2026" })] })), _jsx("button", { type: "button", style: styles.dismissButton, "aria-label": "Dismiss feedback", "data-ggui-ui-feedback-dismiss": true, onClick: () => setPhase('dismissed'), children: '×' })] }));
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
  }
@@ -15,6 +15,36 @@ import type { ObservabilityEmitter } from './observability.js';
15
15
  */
16
16
  export declare const UI_FEEDBACK_CHROME_Z_INDEX = 2147483647;
17
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;
18
48
  export interface UiFeedbackChromeOptions {
19
49
  /**
20
50
  * Observability sink the card emits `ui-feedback` events into.
@@ -52,6 +52,28 @@ import { createElement } from 'react';
52
52
  */
53
53
  export const UI_FEEDBACK_CHROME_Z_INDEX = 2147483647;
54
54
  export const ACTION_TOAST_Z_INDEX = 2147483646;
55
+ /**
56
+ * Pure mount decision the boot path consults before touching the DOM.
57
+ * Two independent gates compose:
58
+ *
59
+ * 1. **The #653 opt-in knob** — the founder's default-off ruling:
60
+ * generated-component cards carry NO feedback chrome unless the
61
+ * embedding deployment explicitly opts in (`uiFeedback: true` on
62
+ * the boot options). Absent or `false` ⇒ never mount, regardless
63
+ * of sink state.
64
+ * 2. **The #471 live-sink gate** — even opted in, only a parent that
65
+ * actually consumes the `ggui:observe` envelope gets the
66
+ * affordance: a bound `onObserve` emitter, or the first-party
67
+ * embed host. Third-party MCP-Apps hosts drop the envelope, and a
68
+ * feedback control that can never deliver feedback is worse than
69
+ * none.
70
+ */
71
+ export function shouldMountUiFeedbackChrome(input) {
72
+ if (input.uiFeedback !== true)
73
+ return false;
74
+ return (input.onObserveBound ||
75
+ input.hostName === 'ggui-iframe-runtime-embed-host');
76
+ }
55
77
  /**
56
78
  * Mount the in-iframe UI-feedback affordance, or do nothing.
57
79
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/iframe-runtime",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -36,12 +36,12 @@
36
36
  "react": "^19.0.0",
37
37
  "react-dom": "^19.0.0",
38
38
  "zod": "^4.3.6",
39
- "@ggui-ai/design": "0.12.0",
40
- "@ggui-ai/live-channel": "0.12.0",
41
- "@ggui-ai/gadgets": "0.12.0",
42
- "@ggui-ai/preview-a2ui": "0.12.0",
43
- "@ggui-ai/protocol": "0.12.0",
44
- "@ggui-ai/wire": "0.12.0"
39
+ "@ggui-ai/design": "0.13.0",
40
+ "@ggui-ai/preview-a2ui": "0.13.0",
41
+ "@ggui-ai/gadgets": "0.13.0",
42
+ "@ggui-ai/protocol": "0.13.0",
43
+ "@ggui-ai/wire": "0.13.0",
44
+ "@ggui-ai/live-channel": "0.13.0"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.27.0"
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "author": "ggui contributors <hello@ggui.ai>",
77
77
  "scripts": {
78
- "build": "rm -rf dist && node esbuild.config.mjs && tsc -p tsconfig.types.json && pnpm run check:bundle-size",
78
+ "build": "rm -rf dist.staging && tsc -p tsconfig.types.json --outDir dist.staging && GGUI_ESBUILD_OUT_DIR=dist.staging node esbuild.config.mjs && node ../scripts/atomic-swap.mjs dist.staging dist && pnpm run check:bundle-size",
79
79
  "check:bundle-size": "tsx scripts/check-bundle-size.ts",
80
80
  "check:bundle-size:self-test": "tsx scripts/check-bundle-size.ts --self-test",
81
81
  "typecheck": "tsc --noEmit",