@ggui-ai/iframe-runtime 0.12.0 → 0.14.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.
@@ -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
  *
@@ -1,9 +1,12 @@
1
1
  import { buildWireConfig, StreamBus, } from '@ggui-ai/wire';
2
2
  import { validateOutboundActionEnvelope } from './validation.js';
3
3
  import { defaultProtocolErrorEmitter, fromClientContractViolation, } from './protocol-error.js';
4
- // `StreamBus` (and its bounded reserved-channel replay ring) was
5
- // hoisted into `@ggui-ai/wire` so `<GguiRender>` and this runtime share
6
- // one implementation. Re-exported for the runtime's internal modules.
4
+ // `StreamBus` (and its bounded reserved-channel replay ring) lives in
5
+ // `@ggui-ai/wire` (one implementation for every first-party renderer).
6
+ // It was originally hoisted to share with `<GguiRender>` — that
7
+ // component and its WS action path are DELETED (react host-helper
8
+ // ruling; ggui#599 cycle-2 caught the stale reference here) — and the
9
+ // hoist stands on its own merits. Re-exported for internal modules.
7
10
  export { StreamBus };
8
11
  /**
9
12
  * Build the per-render `WireConfig` for the iframe runtime.
@@ -44,6 +47,9 @@ export function buildRootWireConfig(opts) {
44
47
  }
45
48
  return buildWireConfig({
46
49
  app: { appId: opts.appId, appName: opts.appId },
50
+ // `isConnected` here is the static config field; `useRender()`
51
+ // reads the LIVE value from wire's document connection store,
52
+ // which the runtime writes at the relay latch's edges (ggui#670).
47
53
  render: { sessionId: opts.sessionId, isConnected: true },
48
54
  auth: { isAuthenticated: false },
49
55
  // Resolve the active render's actionSpec on every dispatch.
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.14.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",
@@ -28,6 +28,11 @@
28
28
  "types": "./dist/server.d.ts",
29
29
  "import": "./dist/server.js",
30
30
  "default": "./dist/server.js"
31
+ },
32
+ "./observability": {
33
+ "types": "./dist/observability.d.ts",
34
+ "import": "./dist/observability.js",
35
+ "default": "./dist/observability.js"
31
36
  }
32
37
  },
33
38
  "dependencies": {
@@ -36,12 +41,12 @@
36
41
  "react": "^19.0.0",
37
42
  "react-dom": "^19.0.0",
38
43
  "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"
44
+ "@ggui-ai/gadgets": "0.14.0",
45
+ "@ggui-ai/preview-a2ui": "0.14.0",
46
+ "@ggui-ai/live-channel": "0.14.0",
47
+ "@ggui-ai/protocol": "0.14.0",
48
+ "@ggui-ai/design": "0.14.0",
49
+ "@ggui-ai/wire": "0.14.0"
45
50
  },
46
51
  "peerDependencies": {
47
52
  "@modelcontextprotocol/sdk": "^1.27.0"
@@ -52,7 +57,7 @@
52
57
  "@types/node": "^24.0.0",
53
58
  "@types/react": "^19.0.0",
54
59
  "@types/react-dom": "^19.0.0",
55
- "esbuild": "^0.25.12",
60
+ "esbuild": "^0.28.2",
56
61
  "jsdom": "^24.0.0",
57
62
  "tsx": "^4.21.0",
58
63
  "typescript": "^5.0.0",
@@ -71,11 +76,11 @@
71
76
  "access": "public"
72
77
  },
73
78
  "engines": {
74
- "node": ">=20.0.0"
79
+ "node": ">=22.0.0"
75
80
  },
76
81
  "author": "ggui contributors <hello@ggui.ai>",
77
82
  "scripts": {
78
- "build": "rm -rf dist && node esbuild.config.mjs && tsc -p tsconfig.types.json && pnpm run check:bundle-size",
83
+ "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
84
  "check:bundle-size": "tsx scripts/check-bundle-size.ts",
80
85
  "check:bundle-size:self-test": "tsx scripts/check-bundle-size.ts --self-test",
81
86
  "typecheck": "tsc --noEmit",