@ggui-ai/iframe-runtime 0.5.0-rc.1 → 0.6.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/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, AuthRequiredEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, UnknownObservabilityEvent, } from './observability.js';
40
+ export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, AuthRequiredEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, UiFeedbackEvent, UnknownObservabilityEvent, } from './observability.js';
41
41
  export { postObservabilityToParent } from './observability.js';
42
42
  export type { LifecycleEmitter } from './lifecycle.js';
43
43
  export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
@@ -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 | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UnknownObservabilityEvent;
36
+ export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UiFeedbackEvent | 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
@@ -152,6 +152,44 @@ export interface ChannelTransportResubscribedEvent {
152
152
  readonly sessionId: string;
153
153
  readonly channelName: string;
154
154
  }
155
+ /**
156
+ * Fired when the end user submits the runtime's in-iframe UI-feedback
157
+ * affordance ("did this generated UI work for you?"). Field semantics
158
+ * mirror `UiFeedbackPayload` in `@ggui-ai/react` / `@ggui-ai/react-native`
159
+ * (the host-chrome twin of this affordance):
160
+ *
161
+ * - `verdict` — `'love'` / `'dislike'`, or `'other'` for the
162
+ * free-text flow.
163
+ * - `comment` — present only for `verdict: 'other'` with a non-empty
164
+ * trimmed comment.
165
+ * - `sessionId` / `toolName` — present exactly when the runtime knew
166
+ * them at mount time.
167
+ *
168
+ * Observability-only — feedback is host-app chrome with ZERO wire
169
+ * surface: the agent cannot observe it (it is neither an action nor
170
+ * context), so it rides this renderer ↔ host seam instead of the
171
+ * agent ↔ UI contract.
172
+ *
173
+ * The affordance mounts only when the runtime document has a parent
174
+ * window (`window.parent !== window`) — a top-level tab has no
175
+ * `ggui:observe` egress, and a dead affordance must never render.
176
+ * Hosts that also own DOM chrome around the iframe MUST wire exactly
177
+ * ONE feedback surface: either their own chrome (the `onUiFeedback`
178
+ * host-callback component) or this event arm — never both, or the
179
+ * user sees two affordances for one render.
180
+ *
181
+ * @public
182
+ */
183
+ export interface UiFeedbackEvent {
184
+ readonly kind: 'ui-feedback';
185
+ readonly verdict: 'love' | 'dislike' | 'other';
186
+ /** Trimmed free-text comment; only on `verdict: 'other'`, never empty. */
187
+ readonly comment?: string;
188
+ /** GguiSession id of the render the feedback is about. */
189
+ readonly sessionId?: string;
190
+ /** Tool that produced the render (e.g. `ggui_render`). */
191
+ readonly toolName?: string;
192
+ }
155
193
  /**
156
194
  * Catch-all branch for event kinds the host's typings don't recognize.
157
195
  * Lets the union stay extensible without forcing protocol-version
@@ -48,6 +48,12 @@ const PREVIEW_CHANNEL = '_ggui:preview';
48
48
  import { parseServerMessage, } from '@ggui-ai/preview-a2ui';
49
49
  import { Button, Card, Checkbox, Divider, Heading, Icon, Image, Input, Row, Select, Spinner, Stack, Text, } from '@ggui-ai/design/primitives';
50
50
  import { PreviewFragmentEnter, PreviewSurface, StreamingText, } from '@ggui-ai/design/preview';
51
+ // A2UI Text carries INLINE markdown (the block level comes from
52
+ // `variant`) — rendered through the shared @silverprotocol/richtext
53
+ // subset via the design system's inline renderer. Streaming-safe:
54
+ // prefixes parse to stable ASTs, so re-emitting a longer `text`
55
+ // only extends the trailing construct.
56
+ import { MarkdownInline } from '@ggui-ai/design';
51
57
  const A2UI_ROOT_ID = 'root';
52
58
  const EMPTY_STATE = {
53
59
  surfaceId: null,
@@ -152,7 +158,7 @@ function renderComponent(component, all) {
152
158
  const headingMatch = /^h([1-6])$/.exec(variant);
153
159
  if (headingMatch) {
154
160
  const level = Number(headingMatch[1]);
155
- return wrap(createElement(Heading, { level }, createElement(StreamingText, null, component.text)));
161
+ return wrap(createElement(Heading, { level }, createElement(StreamingText, null, createElement(MarkdownInline, { text: component.text }))));
156
162
  }
157
163
  // A2UI 'caption' / 'label' map onto Text's size/weight/tone
158
164
  // axes; anything else degrades to default body text.
@@ -161,7 +167,7 @@ function renderComponent(component, all) {
161
167
  : variant === 'label'
162
168
  ? { size: 'sm', weight: 'medium' }
163
169
  : {};
164
- return wrap(createElement(Text, textProps, createElement(StreamingText, null, component.text)));
170
+ return wrap(createElement(Text, textProps, createElement(StreamingText, null, createElement(MarkdownInline, { text: component.text }))));
165
171
  }
166
172
  case 'Image':
167
173
  return wrap(createElement(Image, { src: component.src, alt: component.alt ?? '' }));
package/dist/runtime.js CHANGED
@@ -15,6 +15,7 @@ import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
15
15
  import { mountRender, } from './render-item.js';
16
16
  import { fromBootstrapFailure, } from './protocol-error.js';
17
17
  import { postObservabilityToParent, } from './observability.js';
18
+ import { mountUiFeedbackChrome } from './ui-feedback-chrome.js';
18
19
  import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
19
20
  import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
20
21
  const RENDERER_VERSION = typeof __GGUI_RUNTIME_VERSION__ === 'string' ? __GGUI_RUNTIME_VERSION__ : 'dev';
@@ -367,6 +368,19 @@ export async function bootSequence(opts) {
367
368
  ...(onObserve !== undefined ? { onObserve } : {}),
368
369
  })
369
370
  : null;
371
+ // In-iframe UI-feedback affordance (ggui#244) — mounted adjacent to
372
+ // the session root, gated inside the helper on `window.parent !==
373
+ // window` (top-level `/r/<shortCode>` tabs have no `ggui:observe`
374
+ // egress, so they get NO affordance). Interaction emits a
375
+ // `ui-feedback` ObservabilityEvent through the boot path's emitter;
376
+ // when no emitter was injected the postMessage-to-parent default
377
+ // still gives the mounted affordance a live sink (the gate already
378
+ // proved a parent exists). Fire-and-forget: chrome never blocks or
379
+ // fails the boot.
380
+ void mountUiFeedbackChrome(doc, {
381
+ emit: onObserve ?? postObservabilityToParent,
382
+ sessionId: meta.sessionId,
383
+ });
370
384
  setStatus(refs, `Connecting to ${meta.sessionId}…`, 'connecting');
371
385
  // Boot-without-renderer path: we still need a ChannelRegistry to
372
386
  // receive frames, because the registry is the only dispatch
@@ -0,0 +1,46 @@
1
+ /**
2
+ * UiFeedbackCard — the runtime's in-iframe UI-feedback affordance
3
+ * ("did this generated UI work for you?").
4
+ *
5
+ * The in-iframe twin of the host-chrome `UiFeedback` component in
6
+ * `@ggui-ai/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.
14
+ *
15
+ * This is CHROME the runtime mounts adjacent to the session root
16
+ * (see `ui-feedback-chrome.ts`), NOT a system card: it has no
17
+ * `SystemGguiSession.kind`, no registry entry, and no server-side
18
+ * producer. It lives in this directory because it follows the
19
+ * system-card authoring idiom — inline styles with `--ggui-*`
20
+ * CSS-variable hooks and {@link useColorScheme} adaptation — and
21
+ * because the dynamic-import posture keeps it out of the runtime's
22
+ * base module graph.
23
+ *
24
+ * No ThemeProvider on purpose: the adjacent user render owns the
25
+ * document's `--ggui-*` variable layer (its own theme injection wrote
26
+ * `:root`), and wrapping this card in a ThemeProvider would overwrite
27
+ * those variables last-writer-wins. Inline `var(--ggui-*, fallback)`
28
+ * styles pick up the render's theme when present; the
29
+ * scheme-conditional fallbacks keep the card readable when no theme
30
+ * variables were injected at all.
31
+ */
32
+ import * as React from 'react';
33
+ import type { ObservabilityEmitter } from '../observability.js';
34
+ export interface UiFeedbackCardProps {
35
+ /**
36
+ * Sink for the built {@link UiFeedbackEvent}. Required — the mount
37
+ * gate (`ui-feedback-chrome.ts`) guarantees a reachable parent
38
+ * before this card ever renders, so there is no unsunk state.
39
+ */
40
+ readonly emit: ObservabilityEmitter;
41
+ /** GguiSession id stamped onto every emitted event, when known. */
42
+ readonly sessionId?: string;
43
+ /** Producing tool name stamped onto every emitted event, when known. */
44
+ readonly toolName?: string;
45
+ }
46
+ export declare function UiFeedbackCard({ emit, sessionId, toolName, }: UiFeedbackCardProps): React.JSX.Element | null;
@@ -0,0 +1,133 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * UiFeedbackCard — the runtime's in-iframe UI-feedback affordance
4
+ * ("did this generated UI work for you?").
5
+ *
6
+ * The in-iframe twin of the host-chrome `UiFeedback` component in
7
+ * `@ggui-ai/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.
15
+ *
16
+ * This is CHROME the runtime mounts adjacent to the session root
17
+ * (see `ui-feedback-chrome.ts`), NOT a system card: it has no
18
+ * `SystemGguiSession.kind`, no registry entry, and no server-side
19
+ * producer. It lives in this directory because it follows the
20
+ * system-card authoring idiom — inline styles with `--ggui-*`
21
+ * CSS-variable hooks and {@link useColorScheme} adaptation — and
22
+ * because the dynamic-import posture keeps it out of the runtime's
23
+ * base module graph.
24
+ *
25
+ * No ThemeProvider on purpose: the adjacent user render owns the
26
+ * document's `--ggui-*` variable layer (its own theme injection wrote
27
+ * `:root`), and wrapping this card in a ThemeProvider would overwrite
28
+ * those variables last-writer-wins. Inline `var(--ggui-*, fallback)`
29
+ * styles pick up the render's theme when present; the
30
+ * scheme-conditional fallbacks keep the card readable when no theme
31
+ * variables were injected at all.
32
+ */
33
+ import * as React from 'react';
34
+ import { useColorScheme } from './host-detect.js';
35
+ const FONT_FAMILY = "var(--ggui-font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif)";
36
+ /**
37
+ * Scheme-conditional style palette. The `var(--ggui-*, …)` fallbacks
38
+ * flip between the neutral light values (mirroring the host-chrome
39
+ * twin) and dark-readable equivalents so the card stays legible on
40
+ * hosts that render the iframe over a dark surface without injecting
41
+ * theme variables.
42
+ */
43
+ function buildStyles(scheme) {
44
+ const dark = scheme === 'dark';
45
+ return {
46
+ root: {
47
+ display: 'inline-flex',
48
+ alignItems: 'center',
49
+ gap: 4,
50
+ fontFamily: FONT_FAMILY,
51
+ fontSize: 12,
52
+ lineHeight: 1.2,
53
+ color: dark
54
+ ? 'var(--ggui-color-neutral-500, #9ca3af)'
55
+ : 'var(--ggui-color-neutral-500, #6b7280)',
56
+ },
57
+ verdictButton: {
58
+ appearance: 'none',
59
+ background: dark
60
+ ? 'var(--ggui-color-neutral-50, #1f2937)'
61
+ : 'var(--ggui-color-neutral-50, #f9fafb)',
62
+ border: dark
63
+ ? '1px solid var(--ggui-color-neutral-200, #374151)'
64
+ : '1px solid var(--ggui-color-neutral-200, #e5e7eb)',
65
+ borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
66
+ padding: '2px 8px',
67
+ fontFamily: 'inherit',
68
+ fontSize: 'inherit',
69
+ color: dark
70
+ ? 'var(--ggui-color-neutral-600, #d1d5db)'
71
+ : 'var(--ggui-color-neutral-600, #4b5563)',
72
+ cursor: 'pointer',
73
+ },
74
+ dismissButton: {
75
+ appearance: 'none',
76
+ background: 'transparent',
77
+ border: 'none',
78
+ padding: '2px 4px',
79
+ fontFamily: 'inherit',
80
+ fontSize: 'inherit',
81
+ color: dark
82
+ ? 'var(--ggui-color-neutral-400, #6b7280)'
83
+ : 'var(--ggui-color-neutral-400, #9ca3af)',
84
+ cursor: 'pointer',
85
+ },
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
+ };
109
+ }
110
+ export function UiFeedbackCard({ emit, sessionId, toolName, }) {
111
+ const [phase, setPhase] = React.useState('idle');
112
+ const [comment, setComment] = React.useState('');
113
+ const scheme = useColorScheme();
114
+ const styles = buildStyles(scheme);
115
+ const send = React.useCallback((verdict, commentText) => {
116
+ const trimmed = commentText?.trim();
117
+ emit({
118
+ kind: 'ui-feedback',
119
+ verdict,
120
+ ...(trimmed !== undefined && trimmed.length > 0 ? { comment: trimmed } : {}),
121
+ ...(sessionId !== undefined ? { sessionId } : {}),
122
+ ...(toolName !== undefined ? { toolName } : {}),
123
+ });
124
+ setPhase('sent');
125
+ }, [emit, sessionId, toolName]);
126
+ const onCommentSubmit = React.useCallback((e) => {
127
+ e.preventDefault();
128
+ send('other', comment);
129
+ }, [send, comment]);
130
+ if (phase === 'dismissed')
131
+ 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: '×' })] }));
133
+ }
@@ -0,0 +1,40 @@
1
+ import type { ObservabilityEmitter } from './observability.js';
2
+ export interface UiFeedbackChromeOptions {
3
+ /**
4
+ * Observability sink the card emits `ui-feedback` events into.
5
+ * Production passes the boot path's emitter (default
6
+ * `postObservabilityToParent`); tests inject a recorder.
7
+ */
8
+ readonly emit: ObservabilityEmitter;
9
+ /** GguiSession id stamped onto every emitted event, when known. */
10
+ readonly sessionId?: string;
11
+ /** Producing tool name stamped onto every emitted event, when known. */
12
+ readonly toolName?: string;
13
+ /**
14
+ * Window whose `parent` is consulted for the sink-reachability
15
+ * gate. Defaults to the global `window`; tests inject fakes so the
16
+ * gate is exercisable without mutating jsdom's window graph.
17
+ */
18
+ readonly win?: Window;
19
+ }
20
+ export interface UiFeedbackChromeHandle {
21
+ /** The appended `[data-ggui-ui-feedback-chrome]` container. */
22
+ readonly container: HTMLElement;
23
+ /** Tear down the React root + remove the container. */
24
+ unmount(): void;
25
+ }
26
+ /**
27
+ * Mount the in-iframe UI-feedback affordance, or do nothing.
28
+ *
29
+ * Returns `null` without touching the DOM when the gate fails (no
30
+ * parent window — top-level tab / non-browser environment). Otherwise
31
+ * appends a `<div data-ggui-ui-feedback-chrome>` to `doc.body` (after
32
+ * the session root, which the boot path appended first) and renders
33
+ * the card into it. Idempotent per document: a re-entrant mount
34
+ * replaces any previous chrome container — one affordance per
35
+ * document, never two.
36
+ *
37
+ * Fire-and-forget from the boot path's perspective: chrome must never
38
+ * block or fail the boot, so callers `void` the promise.
39
+ */
40
+ export declare function mountUiFeedbackChrome(doc: Document, opts: UiFeedbackChromeOptions): Promise<UiFeedbackChromeHandle | null>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * In-iframe UI-feedback chrome mount (ggui#244).
3
+ *
4
+ * Mounts {@link import('./system-cards/UiFeedbackCard.js').UiFeedbackCard}
5
+ * in its own container ADJACENT to the session root (appended to
6
+ * `document.body`, so it sits below the rendered UI) — the same
7
+ * append-my-own-root posture as `status-dom.ts#ensureStatusDom`.
8
+ * Interaction egress is the existing `ggui:observe` renderer → host
9
+ * postMessage envelope: the card builds a `ui-feedback`
10
+ * {@link import('./observability.js').ObservabilityEvent} arm and hands
11
+ * it to the injected emitter (production binds
12
+ * `postObservabilityToParent`).
13
+ *
14
+ * Sink-reachability gate — never render a dead affordance:
15
+ *
16
+ * - `window.parent !== window` → a parent window exists, the
17
+ * `ggui:observe` envelope has somewhere to go → mount. Parents
18
+ * that don't understand the envelope ignore it (best-effort emit;
19
+ * one postMessage of cost).
20
+ * - `window.parent === window` → top-level tab (the `/r/<shortCode>`
21
+ * share-link viewer) → NO mount. There is no parent to receive
22
+ * the envelope, so a rendered affordance would drop every click
23
+ * on the floor.
24
+ *
25
+ * Hosts that own DOM chrome around the iframe wire exactly ONE
26
+ * feedback surface: either their own chrome (`onUiFeedback` on the
27
+ * host-side SDK components) or this in-iframe affordance's
28
+ * `ui-feedback` arm via `onObserve` — never both.
29
+ *
30
+ * The card + react-dom are dynamic-imported so this module stays
31
+ * feather-weight in the runtime's base graph (mirrors the system-card
32
+ * branch in `render-item.ts`); the import cost is only paid when the
33
+ * gate passes.
34
+ */
35
+ import { createElement } from 'react';
36
+ /**
37
+ * Mount the in-iframe UI-feedback affordance, or do nothing.
38
+ *
39
+ * Returns `null` without touching the DOM when the gate fails (no
40
+ * parent window — top-level tab / non-browser environment). Otherwise
41
+ * appends a `<div data-ggui-ui-feedback-chrome>` to `doc.body` (after
42
+ * the session root, which the boot path appended first) and renders
43
+ * the card into it. Idempotent per document: a re-entrant mount
44
+ * replaces any previous chrome container — one affordance per
45
+ * document, never two.
46
+ *
47
+ * Fire-and-forget from the boot path's perspective: chrome must never
48
+ * block or fail the boot, so callers `void` the promise.
49
+ */
50
+ export async function mountUiFeedbackChrome(doc, opts) {
51
+ const win = opts.win ?? (typeof window !== 'undefined' ? window : undefined);
52
+ // Gate: mount only when a parent window exists to receive the
53
+ // `ggui:observe` envelope. `win.parent === win` is the top-level-tab
54
+ // signature; a nullish parent means a detached/nonstandard context.
55
+ if (win === undefined || win.parent === null || win.parent === win) {
56
+ return null;
57
+ }
58
+ const [reactDomClient, cardMod] = await Promise.all([
59
+ import('react-dom/client'),
60
+ import('./system-cards/UiFeedbackCard.js'),
61
+ ]);
62
+ // Idempotency: a re-entrant mount replaces the previous chrome
63
+ // container wholesale (remove + fresh element) rather than calling
64
+ // `createRoot` twice on one node — React warns on double-rooting a
65
+ // container, and chrome carries no state worth preserving.
66
+ doc.querySelector('[data-ggui-ui-feedback-chrome]')?.remove();
67
+ const container = doc.createElement('div');
68
+ container.setAttribute('data-ggui-ui-feedback-chrome', '');
69
+ doc.body.appendChild(container);
70
+ const root = reactDomClient.createRoot(container);
71
+ root.render(createElement(cardMod.UiFeedbackCard, {
72
+ emit: opts.emit,
73
+ ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
74
+ ...(opts.toolName !== undefined ? { toolName: opts.toolName } : {}),
75
+ }));
76
+ return {
77
+ container,
78
+ unmount() {
79
+ root.unmount();
80
+ container.remove();
81
+ },
82
+ };
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/iframe-runtime",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.6.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",
@@ -35,12 +35,12 @@
35
35
  "qrcode-generator": "^2.0.4",
36
36
  "react": "^19.0.0",
37
37
  "react-dom": "^19.0.0",
38
- "@ggui-ai/live-channel": "0.5.0-rc.1",
39
- "@ggui-ai/design": "0.5.0-rc.1",
40
- "@ggui-ai/gadgets": "0.5.0-rc.1",
41
- "@ggui-ai/preview-a2ui": "0.5.0-rc.1",
42
- "@ggui-ai/protocol": "0.5.0-rc.1",
43
- "@ggui-ai/wire": "0.5.0-rc.1"
38
+ "@ggui-ai/live-channel": "0.6.0",
39
+ "@ggui-ai/preview-a2ui": "0.6.0",
40
+ "@ggui-ai/gadgets": "0.6.0",
41
+ "@ggui-ai/protocol": "0.6.0",
42
+ "@ggui-ai/design": "0.6.0",
43
+ "@ggui-ai/wire": "0.6.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@modelcontextprotocol/sdk": "^1.27.0"