@ggui-ai/iframe-runtime 0.1.0-rc.1

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.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/bootstrap.d.ts +72 -0
  4. package/dist/bootstrap.js +500 -0
  5. package/dist/canvas/animator/animator-host.d.ts +63 -0
  6. package/dist/canvas/animator/animator-host.js +105 -0
  7. package/dist/canvas/animator/animator-pill.d.ts +42 -0
  8. package/dist/canvas/animator/animator-pill.js +119 -0
  9. package/dist/canvas/animator/state-machine.d.ts +106 -0
  10. package/dist/canvas/animator/state-machine.js +150 -0
  11. package/dist/canvas/canvas-shell.d.ts +77 -0
  12. package/dist/canvas/canvas-shell.js +117 -0
  13. package/dist/canvas/display-mode-policy.d.ts +104 -0
  14. package/dist/canvas/display-mode-policy.js +98 -0
  15. package/dist/canvas/mount.d.ts +150 -0
  16. package/dist/canvas/mount.js +202 -0
  17. package/dist/canvas/nav-stack.d.ts +133 -0
  18. package/dist/canvas/nav-stack.js +172 -0
  19. package/dist/channel-transport.d.ts +182 -0
  20. package/dist/channel-transport.js +344 -0
  21. package/dist/channels/channel-payload.d.ts +20 -0
  22. package/dist/channels/channel-payload.js +34 -0
  23. package/dist/channels/data.d.ts +48 -0
  24. package/dist/channels/data.js +103 -0
  25. package/dist/channels/drain-ack.d.ts +13 -0
  26. package/dist/channels/drain-ack.js +15 -0
  27. package/dist/channels/feedback.d.ts +14 -0
  28. package/dist/channels/feedback.js +19 -0
  29. package/dist/channels/index.d.ts +7 -0
  30. package/dist/channels/index.js +7 -0
  31. package/dist/channels/props-update.d.ts +53 -0
  32. package/dist/channels/props-update.js +130 -0
  33. package/dist/channels/push.d.ts +47 -0
  34. package/dist/channels/push.js +54 -0
  35. package/dist/channels/system.d.ts +26 -0
  36. package/dist/channels/system.js +44 -0
  37. package/dist/compiled-validators.d.ts +40 -0
  38. package/dist/compiled-validators.js +74 -0
  39. package/dist/context-observer.d.ts +205 -0
  40. package/dist/context-observer.js +312 -0
  41. package/dist/gadget-loader.d.ts +118 -0
  42. package/dist/gadget-loader.js +136 -0
  43. package/dist/globals.d.ts +234 -0
  44. package/dist/globals.js +101 -0
  45. package/dist/host-context-emitter.d.ts +91 -0
  46. package/dist/host-context-emitter.js +213 -0
  47. package/dist/iframe-runtime.js +261 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +4 -0
  50. package/dist/lifecycle.d.ts +66 -0
  51. package/dist/lifecycle.js +49 -0
  52. package/dist/mcp-app-iframe-host.d.ts +69 -0
  53. package/dist/mcp-app-iframe-host.js +214 -0
  54. package/dist/observability.d.ts +225 -0
  55. package/dist/observability.js +28 -0
  56. package/dist/protocol-error.d.ts +199 -0
  57. package/dist/protocol-error.js +116 -0
  58. package/dist/provisional-renderer.d.ts +62 -0
  59. package/dist/provisional-renderer.js +323 -0
  60. package/dist/react-renderer.d.ts +95 -0
  61. package/dist/react-renderer.js +343 -0
  62. package/dist/registry-subscribe.d.ts +102 -0
  63. package/dist/registry-subscribe.js +319 -0
  64. package/dist/runtime.d.ts +531 -0
  65. package/dist/runtime.js +2623 -0
  66. package/dist/server.d.ts +36 -0
  67. package/dist/server.js +72 -0
  68. package/dist/stack-item-renderer.d.ts +103 -0
  69. package/dist/stack-item-renderer.js +270 -0
  70. package/dist/stack.d.ts +90 -0
  71. package/dist/stack.js +118 -0
  72. package/dist/status-dom.d.ts +72 -0
  73. package/dist/status-dom.js +51 -0
  74. package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
  75. package/dist/system-cards/NoCredentialsCard.js +189 -0
  76. package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
  77. package/dist/system-cards/ProtocolProbeCard.js +463 -0
  78. package/dist/system-cards/QrCode.d.ts +46 -0
  79. package/dist/system-cards/QrCode.js +55 -0
  80. package/dist/system-cards/host-detect.d.ts +31 -0
  81. package/dist/system-cards/host-detect.js +108 -0
  82. package/dist/system-cards/host-intents.d.ts +85 -0
  83. package/dist/system-cards/host-intents.js +170 -0
  84. package/dist/system-cards/index.d.ts +70 -0
  85. package/dist/system-cards/index.js +134 -0
  86. package/dist/types.d.ts +69 -0
  87. package/dist/types.js +3 -0
  88. package/dist/validation.d.ts +83 -0
  89. package/dist/validation.js +106 -0
  90. package/dist/wire-config.d.ts +214 -0
  91. package/dist/wire-config.js +290 -0
  92. package/package.json +81 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Host environment detection — figure out which embedder is hosting
3
+ * the iframe-runtime so system cards can adapt their theming.
4
+ *
5
+ * Two signals matter:
6
+ *
7
+ * - **Is this Claude?** (claude.ai web's `claudemcpcontent.com`
8
+ * sandbox proxy, or Claude Desktop's local iframe). When yes,
9
+ * system cards should drop their own background so the host's
10
+ * chat-bubble surface shows through, and use Claude-friendly
11
+ * accents.
12
+ * - **What color scheme does the user prefer?** Read from
13
+ * `prefers-color-scheme`. Subscribed via `matchMedia` so the card
14
+ * re-themes live when the user toggles their OS appearance.
15
+ *
16
+ * Both helpers are SSR-safe (return sensible defaults when `window`
17
+ * is undefined).
18
+ */
19
+ import * as React from 'react';
20
+ /**
21
+ * Best-effort detection that we're embedded inside Claude (web or
22
+ * desktop). Heuristics:
23
+ *
24
+ * 1. Hostname endsWith `claudemcpcontent.com` → claude.ai web.
25
+ * claude.ai loads the runtime through this sandbox proxy
26
+ * origin, so the iframe's own location lands here.
27
+ * 2. Document referrer contains `claude.ai` → loaded from the
28
+ * claude.ai chat surface (covers cases where the runtime
29
+ * iframe inherits the parent origin via srcdoc).
30
+ * 3. Hostname/UA contains `claude` (case-insensitive) → covers
31
+ * Claude Desktop's `desktop.claude.ai` / similar.
32
+ *
33
+ * Returns `false` when `window` is undefined (SSR / Node test
34
+ * environments) — the caller can default to a non-Claude theme.
35
+ *
36
+ * @public
37
+ */
38
+ export function isInsideClaude() {
39
+ if (typeof window === 'undefined')
40
+ return false;
41
+ try {
42
+ const host = window.location?.hostname ?? '';
43
+ if (host.endsWith('claudemcpcontent.com'))
44
+ return true;
45
+ if (/(^|\.)claude\.ai$/i.test(host))
46
+ return true;
47
+ if (/claude/i.test(host))
48
+ return true;
49
+ const referrer = typeof document !== 'undefined' ? document.referrer : '';
50
+ if (/claude\.ai/i.test(referrer))
51
+ return true;
52
+ const ua = typeof navigator !== 'undefined' ? navigator.userAgent : '';
53
+ if (/Claude/.test(ua))
54
+ return true;
55
+ }
56
+ catch {
57
+ /* cross-origin frame access denied / malformed location — fall through */
58
+ }
59
+ return false;
60
+ }
61
+ /**
62
+ * React hook returning the user's preferred color scheme. Listens to
63
+ * the `prefers-color-scheme` media query so the card re-themes live
64
+ * when the user toggles between light and dark in their OS / browser
65
+ * settings.
66
+ *
67
+ * Defaults to `'light'` when `window.matchMedia` is unavailable
68
+ * (older runtimes, SSR pre-paint).
69
+ *
70
+ * @public
71
+ */
72
+ export function useColorScheme() {
73
+ const getInitial = React.useCallback(() => {
74
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
75
+ return 'light';
76
+ }
77
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
78
+ ? 'dark'
79
+ : 'light';
80
+ }, []);
81
+ const [scheme, setScheme] = React.useState(getInitial);
82
+ React.useEffect(() => {
83
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
84
+ return undefined;
85
+ }
86
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
87
+ const handler = (e) => {
88
+ setScheme(e.matches ? 'dark' : 'light');
89
+ };
90
+ // Both `addEventListener` (modern) and `addListener` (Safari ≤13)
91
+ // — defensive in case the runtime ships in a webview that hasn't
92
+ // updated its MediaQueryList implementation.
93
+ if (typeof mql.addEventListener === 'function') {
94
+ mql.addEventListener('change', handler);
95
+ return () => {
96
+ mql.removeEventListener('change', handler);
97
+ };
98
+ }
99
+ // Legacy fallback intentionally cast through unknown — older lib.dom
100
+ // typings expose `addListener` only on the deprecated branch.
101
+ const legacy = mql;
102
+ legacy.addListener(handler);
103
+ return () => {
104
+ legacy.removeListener(handler);
105
+ };
106
+ }, []);
107
+ return scheme;
108
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Host-intents — request actions from the embedding host (parent
3
+ * window) that the sandboxed iframe cannot perform itself.
4
+ *
5
+ * The MCP Apps spec (SEP-1865) defines `ui/notifications/*` and
6
+ * `ui/request/*` namespaces for iframe→host communication. This
7
+ * module sends well-formed requests for the actions the no-
8
+ * credentials card and other system cards need (open external URL,
9
+ * etc.), and falls back gracefully when the host doesn't implement
10
+ * the request.
11
+ *
12
+ * Design posture:
13
+ *
14
+ * - **All-strategies-fire-in-parallel.** Most MCP hosts ignore
15
+ * methods they don't implement. Sending three speculative
16
+ * requests costs three postMessages — all of which are no-ops
17
+ * to a host that doesn't recognize them. Whichever the host
18
+ * honors first wins.
19
+ *
20
+ * - **Always include a clipboard fallback.** Clipboard write is
21
+ * the only mechanism that works in every sandbox without any
22
+ * host cooperation. Strategies are layered: try popup, try
23
+ * anchor, try postMessage; clipboard is the floor.
24
+ *
25
+ * - **Observable outcome.** Caller gets a discriminated result
26
+ * `{outcome, mechanism}` so the UI can show "opened" vs
27
+ * "copied" vs "user-select-only" with confident copy.
28
+ */
29
+ /**
30
+ * Request the host open a URL in the user's real browser tab.
31
+ *
32
+ * In claudemcpcontent.com (claude.ai's MCP Apps sandbox), the iframe
33
+ * is sandboxed without `allow-popups` AND `connect-src 'self'` blocks
34
+ * fetches to other origins, so neither `window.open(url)` nor
35
+ * `<a target="_blank">` succeed. This helper:
36
+ *
37
+ * 1. Tries `window.open(url, '_blank', 'noopener,noreferrer')` —
38
+ * works on hosts without sandbox restrictions (console, web
39
+ * embed, desktop with permissive iframe, etc.).
40
+ * 2. Programmatic anchor click — some hosts trap this differently
41
+ * from `window.open`; cheap to try.
42
+ * 3. postMessage `ui/request/open-url` to parent — speculative but
43
+ * canonical. If/when MCP hosts implement an open-url request
44
+ * method (likely a v2 of the spec), this is the form they'll
45
+ * use.
46
+ * 4. postMessage `tools/call` requesting an `open_url` tool — if
47
+ * the host exposes `open_url` as a registered tool, the iframe
48
+ * can request its invocation. Stretch path; cheap to attempt.
49
+ * 5. `navigator.clipboard.writeText(url)` — guaranteed-to-work
50
+ * fallback. Even if every other mechanism is blocked, the
51
+ * clipboard write executes synchronously in the user-gesture
52
+ * context and is permitted in every sandbox we've tested.
53
+ *
54
+ * The return value reflects what we *think* happened — strategy 1
55
+ * is observable (returns null when blocked); strategies 2–4 we
56
+ * fire and forget (no host has documented a reply contract). The
57
+ * clipboard outcome is the most reliable signal because it
58
+ * resolves a Promise we can `await`.
59
+ *
60
+ * @public
61
+ */
62
+ export declare function requestOpenUrl(url: string): Promise<RequestOpenUrlResult>;
63
+ /**
64
+ * Outcome of {@link requestOpenUrl}.
65
+ *
66
+ * - `opened` — `window.open` returned a non-null window. Best-
67
+ * effort signal that a tab/popup was created. UI should say
68
+ * "Opened in browser ✓" or similar.
69
+ * - `copied` — popup was blocked, but clipboard write succeeded.
70
+ * UI should say "URL copied — paste in your browser address bar"
71
+ * and keep the URL block visible for confirmation.
72
+ * - `unsupported` — every mechanism failed. UI must surface the
73
+ * URL prominently with `user-select: all` so the user can
74
+ * long-press / triple-click + copy manually.
75
+ */
76
+ export type RequestOpenUrlResult = {
77
+ readonly outcome: 'opened';
78
+ readonly mechanism: 'window.open';
79
+ } | {
80
+ readonly outcome: 'copied';
81
+ readonly mechanism: 'clipboard';
82
+ } | {
83
+ readonly outcome: 'unsupported';
84
+ readonly mechanism: null;
85
+ };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Host-intents — request actions from the embedding host (parent
3
+ * window) that the sandboxed iframe cannot perform itself.
4
+ *
5
+ * The MCP Apps spec (SEP-1865) defines `ui/notifications/*` and
6
+ * `ui/request/*` namespaces for iframe→host communication. This
7
+ * module sends well-formed requests for the actions the no-
8
+ * credentials card and other system cards need (open external URL,
9
+ * etc.), and falls back gracefully when the host doesn't implement
10
+ * the request.
11
+ *
12
+ * Design posture:
13
+ *
14
+ * - **All-strategies-fire-in-parallel.** Most MCP hosts ignore
15
+ * methods they don't implement. Sending three speculative
16
+ * requests costs three postMessages — all of which are no-ops
17
+ * to a host that doesn't recognize them. Whichever the host
18
+ * honors first wins.
19
+ *
20
+ * - **Always include a clipboard fallback.** Clipboard write is
21
+ * the only mechanism that works in every sandbox without any
22
+ * host cooperation. Strategies are layered: try popup, try
23
+ * anchor, try postMessage; clipboard is the floor.
24
+ *
25
+ * - **Observable outcome.** Caller gets a discriminated result
26
+ * `{outcome, mechanism}` so the UI can show "opened" vs
27
+ * "copied" vs "user-select-only" with confident copy.
28
+ */
29
+ /**
30
+ * Request the host open a URL in the user's real browser tab.
31
+ *
32
+ * In claudemcpcontent.com (claude.ai's MCP Apps sandbox), the iframe
33
+ * is sandboxed without `allow-popups` AND `connect-src 'self'` blocks
34
+ * fetches to other origins, so neither `window.open(url)` nor
35
+ * `<a target="_blank">` succeed. This helper:
36
+ *
37
+ * 1. Tries `window.open(url, '_blank', 'noopener,noreferrer')` —
38
+ * works on hosts without sandbox restrictions (console, web
39
+ * embed, desktop with permissive iframe, etc.).
40
+ * 2. Programmatic anchor click — some hosts trap this differently
41
+ * from `window.open`; cheap to try.
42
+ * 3. postMessage `ui/request/open-url` to parent — speculative but
43
+ * canonical. If/when MCP hosts implement an open-url request
44
+ * method (likely a v2 of the spec), this is the form they'll
45
+ * use.
46
+ * 4. postMessage `tools/call` requesting an `open_url` tool — if
47
+ * the host exposes `open_url` as a registered tool, the iframe
48
+ * can request its invocation. Stretch path; cheap to attempt.
49
+ * 5. `navigator.clipboard.writeText(url)` — guaranteed-to-work
50
+ * fallback. Even if every other mechanism is blocked, the
51
+ * clipboard write executes synchronously in the user-gesture
52
+ * context and is permitted in every sandbox we've tested.
53
+ *
54
+ * The return value reflects what we *think* happened — strategy 1
55
+ * is observable (returns null when blocked); strategies 2–4 we
56
+ * fire and forget (no host has documented a reply contract). The
57
+ * clipboard outcome is the most reliable signal because it
58
+ * resolves a Promise we can `await`.
59
+ *
60
+ * @public
61
+ */
62
+ export async function requestOpenUrl(url) {
63
+ if (typeof window === 'undefined') {
64
+ return { outcome: 'unsupported', mechanism: null };
65
+ }
66
+ // 1. window.open — observable. `null` return == popup-blocked /
67
+ // sandbox-blocked. A non-null window object means the browser
68
+ // initiated a tab/window (we can't actually verify the user
69
+ // sees it, but absence of null is a strong positive signal).
70
+ let openedWindow = null;
71
+ try {
72
+ openedWindow = window.open(url, '_blank', 'noopener,noreferrer');
73
+ }
74
+ catch {
75
+ openedWindow = null;
76
+ }
77
+ // 2. Programmatic anchor click. Same iframe-sandbox limits apply,
78
+ // but some mobile WebViews (notably iOS Safari embedded as
79
+ // SFSafariViewController) trap anchor.click() differently from
80
+ // window.open. Cheap to try; no observable signal.
81
+ try {
82
+ const anchor = document.createElement('a');
83
+ anchor.href = url;
84
+ anchor.target = '_blank';
85
+ anchor.rel = 'noopener noreferrer';
86
+ anchor.style.display = 'none';
87
+ document.body.appendChild(anchor);
88
+ anchor.click();
89
+ document.body.removeChild(anchor);
90
+ }
91
+ catch {
92
+ // Anchor strategy failed — fall through.
93
+ }
94
+ // 3. postMessage `ui/request/open-url`. Speculative — no MCP host
95
+ // has shipped this yet, but it's the canonical request shape
96
+ // a future host should implement. Sending it is harmless: a
97
+ // host that doesn't recognize the method ignores the message.
98
+ // We don't await a reply because there's no documented
99
+ // response contract; if a host ships one, future versions of
100
+ // this helper can listen for `ui/responses/open-url`.
101
+ try {
102
+ window.parent.postMessage({
103
+ jsonrpc: '2.0',
104
+ method: 'ui/request/open-url',
105
+ params: { url },
106
+ }, '*');
107
+ }
108
+ catch {
109
+ /* parent detached / sandboxed */
110
+ }
111
+ // 4. postMessage `tools/call` → `open_url`. Some hosts model
112
+ // "request the user's browser to open a URL" as a registered
113
+ // tool the iframe can invoke. Cheap probe; harmless when the
114
+ // tool isn't registered.
115
+ try {
116
+ window.parent.postMessage({
117
+ jsonrpc: '2.0',
118
+ id: Math.floor(Math.random() * 1e9),
119
+ method: 'tools/call',
120
+ params: { name: 'open_url', arguments: { url } },
121
+ }, '*');
122
+ }
123
+ catch {
124
+ /* parent detached / sandboxed */
125
+ }
126
+ // 5. Clipboard fallback — guaranteed to work in every host that
127
+ // grants `clipboard-write` (which the MCP Apps default
128
+ // permissions-policy includes). The user-gesture context the
129
+ // click came in on is exactly what `writeText` requires.
130
+ //
131
+ // Timeout via Promise.race: in some sandbox environments
132
+ // (headless browsers without a real clipboard, MCP hosts that
133
+ // don't grant clipboard-write but never reject — they just
134
+ // hang the Promise), `writeText` returns a Promise that
135
+ // NEVER resolves or rejects. Without a timeout the whole
136
+ // helper hangs forever and the CTA gets stuck on "Opening…".
137
+ // 1.5s is generous enough for a real clipboard write
138
+ // (microsecond-fast in normal browsers) yet short enough
139
+ // that the user doesn't perceive a hang.
140
+ let clipboardCopied = false;
141
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
142
+ const CLIPBOARD_TIMEOUT_MS = 1500;
143
+ try {
144
+ await Promise.race([
145
+ navigator.clipboard.writeText(url).then(() => {
146
+ clipboardCopied = true;
147
+ }, () => {
148
+ clipboardCopied = false;
149
+ }),
150
+ new Promise((resolve) => setTimeout(resolve, CLIPBOARD_TIMEOUT_MS)),
151
+ ]);
152
+ }
153
+ catch {
154
+ clipboardCopied = false;
155
+ }
156
+ }
157
+ // Decide the outcome. Priority:
158
+ // - If window.open returned a non-null window, claim 'opened'
159
+ // (highest-confidence observable signal).
160
+ // - Else if clipboard succeeded, 'copied' (the fallback worked).
161
+ // - Else 'unsupported' (user must long-press / triple-click the
162
+ // URL block manually).
163
+ if (openedWindow !== null) {
164
+ return { outcome: 'opened', mechanism: 'window.open' };
165
+ }
166
+ if (clipboardCopied) {
167
+ return { outcome: 'copied', mechanism: 'clipboard' };
168
+ }
169
+ return { outcome: 'unsupported', mechanism: null };
170
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * System-card registry.
3
+ *
4
+ * Central mapping from `SystemStackItem.kind` (a stable wire string)
5
+ * to a real React component bundled inside the iframe-runtime. The
6
+ * runtime's self-contained boot path looks up the kind here when it
7
+ * sees a stack item with `type: 'system'`. Unknown kinds fall through
8
+ * to {@link UnknownSystemCard} so an old runtime + new server still
9
+ * surfaces something visible (vs. a blank iframe).
10
+ *
11
+ * Adding a new system card:
12
+ *
13
+ * 1. Author `MyNewCard.tsx` in this directory.
14
+ * 2. Add an entry to {@link SYSTEM_CARD_REGISTRY} below.
15
+ * 3. (Optional) Add the new kind string to
16
+ * `SystemStackItemKind` in `@ggui-ai/protocol` for IDE autocomplete.
17
+ */
18
+ import * as React from 'react';
19
+ import { NoCredentialsCard, type NoCredentialsCardProps } from './NoCredentialsCard.js';
20
+ import { ProtocolProbeCard, type ProtocolProbeCardProps } from './ProtocolProbeCard.js';
21
+ /**
22
+ * Generic typed-component shape for the registry. Each entry takes a
23
+ * loosely-typed `Record<string, unknown>` (the wire `props`) and is
24
+ * responsible for narrowing its own props internally — the runtime
25
+ * does NOT validate against the JSX prop shape because the wire is
26
+ * versioned independently of the bundle.
27
+ */
28
+ export type SystemCardComponent = React.ComponentType<Record<string, unknown>>;
29
+ /**
30
+ * Registered system-card kinds. Keep keys aligned with
31
+ * `SystemStackItemKind` in `@ggui-ai/protocol` so wire + bundle stay
32
+ * in sync. New kinds added without a registry entry render as
33
+ * {@link UnknownSystemCard}.
34
+ */
35
+ export declare const SYSTEM_CARD_REGISTRY: Record<string, SystemCardComponent>;
36
+ /**
37
+ * Resolve a registered component by kind, or the fallback when the
38
+ * kind is unknown. Always returns a renderable React element.
39
+ */
40
+ export declare function renderSystemCard(kind: string, props: Record<string, unknown>): React.ReactElement;
41
+ /**
42
+ * Top-level mount component the runtime renders for system stack
43
+ * items. Wraps the resolved card in a {@link ThemeProvider} so design
44
+ * tokens (`var(--ggui-*)`) resolve at `:root` even though the host's
45
+ * shell HTML doesn't pre-stamp them. Idempotent across remounts —
46
+ * shares the `ggui-theme-vars` style id with the rest of the design
47
+ * system, last writer wins.
48
+ *
49
+ * Adapts to the embedding host:
50
+ *
51
+ * - **Inside Claude** (claude.ai web's `claudemcpcontent.com` or
52
+ * Claude Desktop): drops the body background to `transparent` so
53
+ * the chat-bubble surface shows through, and follows the user's
54
+ * OS-level `prefers-color-scheme`. The card's own surfaces stay
55
+ * opaque so the content remains readable on either bubble color.
56
+ * - **Anywhere else**: lightTheme + opaque white background — the
57
+ * historical default, suitable for stand-alone preview iframes
58
+ * and console inspectors.
59
+ *
60
+ * Theme choice re-runs live: if the user toggles dark mode in their
61
+ * OS while the card is mounted, `useColorScheme` updates and we
62
+ * re-emit the matching theme's CSS variables.
63
+ */
64
+ export declare function SystemCardHost({ kind, props, themeId: _themeId, }: {
65
+ kind: string;
66
+ props: Record<string, unknown>;
67
+ themeId?: string | undefined;
68
+ }): React.ReactElement;
69
+ export { NoCredentialsCard, ProtocolProbeCard };
70
+ export type { NoCredentialsCardProps, ProtocolProbeCardProps };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * System-card registry.
3
+ *
4
+ * Central mapping from `SystemStackItem.kind` (a stable wire string)
5
+ * to a real React component bundled inside the iframe-runtime. The
6
+ * runtime's self-contained boot path looks up the kind here when it
7
+ * sees a stack item with `type: 'system'`. Unknown kinds fall through
8
+ * to {@link UnknownSystemCard} so an old runtime + new server still
9
+ * surfaces something visible (vs. a blank iframe).
10
+ *
11
+ * Adding a new system card:
12
+ *
13
+ * 1. Author `MyNewCard.tsx` in this directory.
14
+ * 2. Add an entry to {@link SYSTEM_CARD_REGISTRY} below.
15
+ * 3. (Optional) Add the new kind string to
16
+ * `SystemStackItemKind` in `@ggui-ai/protocol` for IDE autocomplete.
17
+ */
18
+ import * as React from 'react';
19
+ import { ThemeProvider, lightTheme, darkTheme } from '@ggui-ai/design/themes';
20
+ import { NoCredentialsCard, } from './NoCredentialsCard.js';
21
+ import { ProtocolProbeCard, } from './ProtocolProbeCard.js';
22
+ import { isInsideClaude, useColorScheme } from './host-detect.js';
23
+ /**
24
+ * Wrap a strongly-typed card component in a loosely-typed entry the
25
+ * registry can store uniformly. Each card narrows its own props out of
26
+ * the bag — runtime cost is negligible (a property read + typeof
27
+ * checks).
28
+ */
29
+ function entry(Component, narrow) {
30
+ return function SystemCardEntry(props) {
31
+ // The narrowed shape is the component's actual prop type — cast
32
+ // through `unknown` because React's typed `createElement` overloads
33
+ // can't see through the registry's loose `Record<string, unknown>`
34
+ // surface.
35
+ const Comp = Component;
36
+ const narrowed = narrow(props);
37
+ return React.createElement(Comp, narrowed);
38
+ };
39
+ }
40
+ /**
41
+ * Fallback card rendered when the runtime sees a `kind` it doesn't
42
+ * have a registered component for — typically because the server is
43
+ * running a newer protocol version. Surfaces the kind + raw props so
44
+ * the user can at least file a useful bug report.
45
+ */
46
+ function UnknownSystemCard({ kind, props, }) {
47
+ return React.createElement('div', {
48
+ style: {
49
+ padding: '16px',
50
+ border: '1px solid var(--ggui-color-outlineVariant, #e4e4e7)',
51
+ borderRadius: 'var(--ggui-shape-radius-md, 10px)',
52
+ fontFamily: 'var(--ggui-font-family-sans, -apple-system, BlinkMacSystemFont, sans-serif)',
53
+ background: 'var(--ggui-color-surfaceVariant, #f4f4f5)',
54
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
55
+ },
56
+ }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, `Unknown system card: ${kind}`), React.createElement('div', {
57
+ style: {
58
+ fontSize: 12,
59
+ fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, monospace)',
60
+ opacity: 0.7,
61
+ wordBreak: 'break-all',
62
+ },
63
+ }, JSON.stringify(props)));
64
+ }
65
+ /**
66
+ * Registered system-card kinds. Keep keys aligned with
67
+ * `SystemStackItemKind` in `@ggui-ai/protocol` so wire + bundle stay
68
+ * in sync. New kinds added without a registry entry render as
69
+ * {@link UnknownSystemCard}.
70
+ */
71
+ export const SYSTEM_CARD_REGISTRY = {
72
+ 'no-credentials': entry(NoCredentialsCard, (raw) => {
73
+ const settingsUrl = typeof raw['settingsUrl'] === 'string' ? raw['settingsUrl'] : '';
74
+ const intent = typeof raw['intent'] === 'string' ? raw['intent'] : undefined;
75
+ return {
76
+ settingsUrl,
77
+ ...(intent !== undefined ? { intent } : {}),
78
+ };
79
+ }),
80
+ 'mcp-apps-probe': entry(ProtocolProbeCard, (raw) => {
81
+ const intent = typeof raw['intent'] === 'string' ? raw['intent'] : undefined;
82
+ return {
83
+ ...(intent !== undefined ? { intent } : {}),
84
+ };
85
+ }),
86
+ };
87
+ /**
88
+ * Resolve a registered component by kind, or the fallback when the
89
+ * kind is unknown. Always returns a renderable React element.
90
+ */
91
+ export function renderSystemCard(kind, props) {
92
+ const Comp = SYSTEM_CARD_REGISTRY[kind];
93
+ if (Comp) {
94
+ return React.createElement(Comp, props);
95
+ }
96
+ return React.createElement(UnknownSystemCard, { kind, props });
97
+ }
98
+ /**
99
+ * Top-level mount component the runtime renders for system stack
100
+ * items. Wraps the resolved card in a {@link ThemeProvider} so design
101
+ * tokens (`var(--ggui-*)`) resolve at `:root` even though the host's
102
+ * shell HTML doesn't pre-stamp them. Idempotent across remounts —
103
+ * shares the `ggui-theme-vars` style id with the rest of the design
104
+ * system, last writer wins.
105
+ *
106
+ * Adapts to the embedding host:
107
+ *
108
+ * - **Inside Claude** (claude.ai web's `claudemcpcontent.com` or
109
+ * Claude Desktop): drops the body background to `transparent` so
110
+ * the chat-bubble surface shows through, and follows the user's
111
+ * OS-level `prefers-color-scheme`. The card's own surfaces stay
112
+ * opaque so the content remains readable on either bubble color.
113
+ * - **Anywhere else**: lightTheme + opaque white background — the
114
+ * historical default, suitable for stand-alone preview iframes
115
+ * and console inspectors.
116
+ *
117
+ * Theme choice re-runs live: if the user toggles dark mode in their
118
+ * OS while the card is mounted, `useColorScheme` updates and we
119
+ * re-emit the matching theme's CSS variables.
120
+ */
121
+ export function SystemCardHost({ kind, props, themeId: _themeId, }) {
122
+ // `themeId` reserved for future per-card theme override. Today the
123
+ // host environment + system color scheme drive the choice.
124
+ const inClaude = isInsideClaude();
125
+ const scheme = useColorScheme();
126
+ const theme = scheme === 'dark' ? darkTheme : lightTheme;
127
+ return React.createElement(ThemeProvider, {
128
+ theme,
129
+ mode: scheme,
130
+ transparent: inClaude,
131
+ children: renderSystemCard(kind, props),
132
+ });
133
+ }
134
+ export { NoCredentialsCard, ProtocolProbeCard };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Internal types shared across the `@ggui-ai/iframe-runtime` runtime.
3
+ *
4
+ * These shapes describe the renderer's INTERNAL contracts — the
5
+ * bootstrap-parse outcome union + the renderer-host postMessage
6
+ * frames. The wire shapes consumed FROM the network
7
+ * (`SubscribePayload`, `AckPayload`, `WebSocketMessage`,
8
+ * `GguiBootstrapMeta`) come from `@ggui-ai/protocol` and are imported
9
+ * as `type` only so esbuild emits no runtime require for the protocol
10
+ * package — the bundle stays self-contained.
11
+ *
12
+ * Post-B3b: the WS lifecycle lives inside `@ggui-ai/channel-client`'s
13
+ * `WSTransport`; the renderer no longer maintains its own manager
14
+ * class. The `RendererWebSocketManagerOptions` type that pre-B3b
15
+ * declared the manager's constructor bag has been retired.
16
+ */
17
+ import type { GguiBootstrapMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
18
+ import type { HostContextProjection } from '@ggui-ai/protocol';
19
+ /**
20
+ * Why a bootstrap parse can fail. Closed union — every value is a
21
+ * canonical reason the renderer surfaces to the host via postMessage.
22
+ *
23
+ * This set is intentionally narrow — it covers only parse-time
24
+ * outcomes. Host-observable failures (`BUNDLE_FETCH_FAILED`,
25
+ * `CSP_VIOLATION`, etc) live on the broader `BootstrapFailureReason`
26
+ * union in `protocol-error.ts`. The four parse-time outcomes:
27
+ *
28
+ * - `MISSING_TOOL_OUTPUT` — host echoed `ui/initialize` without a
29
+ * `result.toolOutput` object at all.
30
+ * - `MISSING_META_GGUI_BOOTSTRAP` — `result.toolOutput` exists but
31
+ * `_meta.ggui.bootstrap` is absent. The on-wire synonym is
32
+ * `BOOTSTRAP_META_MISSING`.
33
+ * - `MALFORMED_BOOTSTRAP` — `_meta.ggui.bootstrap` is present but
34
+ * missing one of the four required string fields (`wsUrl`,
35
+ * `token`, `sessionId`, `appId`) — i.e. it doesn't satisfy
36
+ * `GguiBootstrapMeta`.
37
+ * - `EXPIRED_BOOTSTRAP` — `expiresAt` is set and parses to a
38
+ * timestamp in the past. UX sugar; servers reject expired tokens
39
+ * anyway, but we skip the round-trip when the proof is on hand.
40
+ */
41
+ export type BootstrapParseFailureReason = 'MISSING_TOOL_OUTPUT' | 'MISSING_META_GGUI_BOOTSTRAP' | 'MALFORMED_BOOTSTRAP' | 'EXPIRED_BOOTSTRAP';
42
+ /**
43
+ * Result of a bootstrap-meta extraction attempt. Discriminated on
44
+ * `ok` so call sites get exhaustive narrowing without a typed-error
45
+ * class — keeps the renderer's runtime cost tiny (no class metadata
46
+ * in the bundle).
47
+ *
48
+ * widening: the `ok: true` arm now also carries
49
+ * an optional `hostContext: HostContextProjection`. When the bootstrap
50
+ * is extracted from the `ui/initialize` response (Reading-A path), the
51
+ * parser opportunistically projects the surrounding `McpUiHostContext`
52
+ * into this field so the runtime can:
53
+ * - drive canvas-mode display-mode escalation
54
+ * - echo to the server (live-channel `host_context_observed` envelope)
55
+ * - feed the agent on next handshake/consume
56
+ *
57
+ * Capture is best-effort: a malformed `HostContext` never blocks the
58
+ * bootstrap parse. Absent for the other extraction paths
59
+ * (`__GGUI_BOOTSTRAP__` global, `ui/notifications/tool-result`) — those
60
+ * envelopes don't carry HostContext.
61
+ */
62
+ export type BootstrapParseResult = {
63
+ readonly ok: true;
64
+ readonly bootstrap: GguiBootstrapMeta;
65
+ readonly hostContext?: HostContextProjection;
66
+ } | {
67
+ readonly ok: false;
68
+ readonly reason: BootstrapParseFailureReason;
69
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export {};
2
+ // `RendererWebSocketManagerOptions` retired in B3b — the WS lifecycle
3
+ // moved to `@ggui-ai/channel-client`'s `WSTransport`.