@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.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/bootstrap.d.ts +72 -0
- package/dist/bootstrap.js +500 -0
- package/dist/canvas/animator/animator-host.d.ts +63 -0
- package/dist/canvas/animator/animator-host.js +105 -0
- package/dist/canvas/animator/animator-pill.d.ts +42 -0
- package/dist/canvas/animator/animator-pill.js +119 -0
- package/dist/canvas/animator/state-machine.d.ts +106 -0
- package/dist/canvas/animator/state-machine.js +150 -0
- package/dist/canvas/canvas-shell.d.ts +77 -0
- package/dist/canvas/canvas-shell.js +117 -0
- package/dist/canvas/display-mode-policy.d.ts +104 -0
- package/dist/canvas/display-mode-policy.js +98 -0
- package/dist/canvas/mount.d.ts +150 -0
- package/dist/canvas/mount.js +202 -0
- package/dist/canvas/nav-stack.d.ts +133 -0
- package/dist/canvas/nav-stack.js +172 -0
- package/dist/channel-transport.d.ts +182 -0
- package/dist/channel-transport.js +344 -0
- package/dist/channels/channel-payload.d.ts +20 -0
- package/dist/channels/channel-payload.js +34 -0
- package/dist/channels/data.d.ts +48 -0
- package/dist/channels/data.js +103 -0
- package/dist/channels/drain-ack.d.ts +13 -0
- package/dist/channels/drain-ack.js +15 -0
- package/dist/channels/feedback.d.ts +14 -0
- package/dist/channels/feedback.js +19 -0
- package/dist/channels/index.d.ts +7 -0
- package/dist/channels/index.js +7 -0
- package/dist/channels/props-update.d.ts +53 -0
- package/dist/channels/props-update.js +130 -0
- package/dist/channels/push.d.ts +47 -0
- package/dist/channels/push.js +54 -0
- package/dist/channels/system.d.ts +26 -0
- package/dist/channels/system.js +44 -0
- package/dist/compiled-validators.d.ts +40 -0
- package/dist/compiled-validators.js +74 -0
- package/dist/context-observer.d.ts +205 -0
- package/dist/context-observer.js +312 -0
- package/dist/gadget-loader.d.ts +118 -0
- package/dist/gadget-loader.js +136 -0
- package/dist/globals.d.ts +234 -0
- package/dist/globals.js +101 -0
- package/dist/host-context-emitter.d.ts +91 -0
- package/dist/host-context-emitter.js +213 -0
- package/dist/iframe-runtime.js +261 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +4 -0
- package/dist/lifecycle.d.ts +66 -0
- package/dist/lifecycle.js +49 -0
- package/dist/mcp-app-iframe-host.d.ts +69 -0
- package/dist/mcp-app-iframe-host.js +214 -0
- package/dist/observability.d.ts +225 -0
- package/dist/observability.js +28 -0
- package/dist/protocol-error.d.ts +199 -0
- package/dist/protocol-error.js +116 -0
- package/dist/provisional-renderer.d.ts +62 -0
- package/dist/provisional-renderer.js +323 -0
- package/dist/react-renderer.d.ts +95 -0
- package/dist/react-renderer.js +343 -0
- package/dist/registry-subscribe.d.ts +102 -0
- package/dist/registry-subscribe.js +319 -0
- package/dist/runtime.d.ts +531 -0
- package/dist/runtime.js +2623 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.js +72 -0
- package/dist/stack-item-renderer.d.ts +103 -0
- package/dist/stack-item-renderer.js +270 -0
- package/dist/stack.d.ts +90 -0
- package/dist/stack.js +118 -0
- package/dist/status-dom.d.ts +72 -0
- package/dist/status-dom.js +51 -0
- package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
- package/dist/system-cards/NoCredentialsCard.js +189 -0
- package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
- package/dist/system-cards/ProtocolProbeCard.js +463 -0
- package/dist/system-cards/QrCode.d.ts +46 -0
- package/dist/system-cards/QrCode.js +55 -0
- package/dist/system-cards/host-detect.d.ts +31 -0
- package/dist/system-cards/host-detect.js +108 -0
- package/dist/system-cards/host-intents.d.ts +85 -0
- package/dist/system-cards/host-intents.js +170 -0
- package/dist/system-cards/index.d.ts +70 -0
- package/dist/system-cards/index.js +134 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +83 -0
- package/dist/validation.js +106 -0
- package/dist/wire-config.d.ts +214 -0
- package/dist/wire-config.js +290 -0
- 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 };
|
package/dist/types.d.ts
ADDED
|
@@ -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