@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,463 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * MCP Apps protocol probe system card.
4
+ *
5
+ * Diagnostic + regression fixture exercising every gesture surface
6
+ * the iframe runtime routes:
7
+ *
8
+ * 1. **Pattern β bridge** — `tools/call ggui_runtime_submit_action` (audit) +
9
+ * `ui/update-model-context` (silent context) + `ui/message`
10
+ * (consent prompt). Fires for cross-server / non-app-visible
11
+ * `actionSpec[name].dispatch.tool` targets, OR any
12
+ * `dispatch.kind === 'agent'` entry. Empirical-first reproduction
13
+ * of the production `dispatchWiredAction` path.
14
+ * 2. **Pattern α direct fire** — `tools/call ggui_runtime_submit_action`
15
+ * (audit) + `tools/call <T>` (direct, no consent prompt). Fires
16
+ * for same-server, app-visible target tools. Probes against
17
+ * `ggui_runtime_submit_action` itself (the only universally-app-visible
18
+ * tool the iframe can be sure exists).
19
+ * 3. **Native-idiom: anchor click** — `ui/open-link` paired with
20
+ * `kind: 'openLink'` audit. Triggered by the runtime's
21
+ * capture-phase `<a href>` interceptor (cross-origin or
22
+ * `target="_blank"`). The probe row simulates the postMessage
23
+ * directly so the operator sees the envelope without needing to
24
+ * click an anchor.
25
+ * 4. **Native-idiom: fullscreen** — `ui/request-display-mode` +
26
+ * `kind: 'requestDisplayMode'` audit. Triggered by the runtime's
27
+ * `Element.prototype.requestFullscreen` /
28
+ * `Document.prototype.exitFullscreen` overrides. The probe row
29
+ * simulates the postMessage; the row labelled "exitFullscreen"
30
+ * sends `mode: 'inline'` per the spec'd inverse mapping.
31
+ *
32
+ * Each button transitions through `idle → pending → success | error`
33
+ * so the operator can see exactly which methods the embedding host
34
+ * (claude.ai web, Claude Desktop, …) actually honors today. The
35
+ * persistent receive-log captures every `ui/notifications/*` and
36
+ * host-issued request seen while the card is mounted.
37
+ *
38
+ * Why this lives outside the wire-context: system cards (`kind:
39
+ * 'mcp-apps-probe'`, `'no-credentials'`, …) mount in a render-only
40
+ * branch of `bootSelfContained` that does NOT install
41
+ * `<GguiWireProvider>` — they exist precisely to surface protocol
42
+ * state when the wire-context might not be live (auth missing,
43
+ * compile failed, etc.). The probe card mirrors the postMessage
44
+ * envelopes the runtime's gesture surfaces would emit, derived from
45
+ * the canonical `SubmitActionEnvelope` shape in
46
+ * `@ggui-ai/protocol/integrations/mcp-apps`.
47
+ *
48
+ * Originating context: an earlier runtime posted its outbound
49
+ * action dispatch only as `{method:'tools/call',
50
+ * name:'ggui_runtime_submit_action', ...}`. Per MCP Apps spec §401, hosts MUST
51
+ * reject `tools/call` from views for tools without `'app'` in
52
+ * visibility — claude.ai silently dropped the call. The original
53
+ * probe diagnosed the gap; the routing simplification work landed
54
+ * Pattern α / Pattern β / native idioms in response.
55
+ */
56
+ import * as React from 'react';
57
+ import { Card, Stack, Heading, Text, Button, Badge, } from '@ggui-ai/design/primitives';
58
+ const PROBE_TIMEOUT_MS = 5_000;
59
+ /**
60
+ * Lifetime-scoped JSON-RPC caller. One instance per card mount —
61
+ * mirrors the runtime's own `makeJsonRpcCaller` (runtime.ts:105) so
62
+ * the card behaves like a real View per spec semantics. Pending
63
+ * requests are tracked by id; the persistent `message` listener
64
+ * matches by id and resolves.
65
+ */
66
+ function useJsonRpcCaller() {
67
+ const nextIdRef = React.useRef(10_000);
68
+ const pendingRef = React.useRef(new Map());
69
+ React.useEffect(() => {
70
+ const onMessage = (event) => {
71
+ const data = event.data;
72
+ if (data === null || typeof data !== 'object')
73
+ return;
74
+ const id = data.id;
75
+ if (typeof id !== 'number')
76
+ return;
77
+ const resolver = pendingRef.current.get(id);
78
+ if (resolver === undefined)
79
+ return;
80
+ pendingRef.current.delete(id);
81
+ resolver(data);
82
+ };
83
+ window.addEventListener('message', onMessage);
84
+ return () => window.removeEventListener('message', onMessage);
85
+ }, []);
86
+ return React.useCallback((method, params) => {
87
+ return new Promise((resolve) => {
88
+ const id = nextIdRef.current;
89
+ nextIdRef.current += 1;
90
+ pendingRef.current.set(id, resolve);
91
+ try {
92
+ window.parent.postMessage({ jsonrpc: '2.0', id, method, params: params ?? {} }, '*');
93
+ }
94
+ catch (err) {
95
+ pendingRef.current.delete(id);
96
+ resolve({
97
+ error: {
98
+ code: -32099,
99
+ message: err instanceof Error ? err.message : String(err),
100
+ },
101
+ });
102
+ }
103
+ window.setTimeout(() => {
104
+ if (pendingRef.current.delete(id)) {
105
+ resolve({ error: { code: -32001, message: 'TIMEOUT' } });
106
+ }
107
+ }, PROBE_TIMEOUT_MS);
108
+ });
109
+ }, []);
110
+ }
111
+ /**
112
+ * Persistent listener for host-emitted notifications. Captures every
113
+ * `jsonrpc:'2.0'` message that arrives without an `id` (= notification)
114
+ * AND has a `method` starting with `ui/notifications/` or matching one
115
+ * of the spec's other host→view methods. Bounded to the most recent
116
+ * 50 entries to keep the DOM small even on chatty hosts.
117
+ */
118
+ function useNotificationLog() {
119
+ const [entries, setEntries] = React.useState([]);
120
+ const seqRef = React.useRef(0);
121
+ React.useEffect(() => {
122
+ const onMessage = (event) => {
123
+ const data = event.data;
124
+ if (data === null || typeof data !== 'object')
125
+ return;
126
+ const obj = data;
127
+ if (obj.jsonrpc !== '2.0')
128
+ return;
129
+ if (typeof obj.method !== 'string')
130
+ return;
131
+ // Capture notifications (no id) AND host-issued requests
132
+ // (e.g. `ui/resource-teardown` carries an id). Skip echoes of
133
+ // OUR own outbound calls — those have ids in the 10k range we
134
+ // assigned.
135
+ const isOurEcho = typeof obj.id === 'number' &&
136
+ obj.id >= 10_000 &&
137
+ (obj.method === 'ui/message' ||
138
+ obj.method === 'ui/update-model-context' ||
139
+ obj.method === 'ui/open-link' ||
140
+ obj.method === 'ui/request-display-mode' ||
141
+ obj.method === 'tools/call');
142
+ if (isOurEcho)
143
+ return;
144
+ seqRef.current += 1;
145
+ const entry = {
146
+ id: seqRef.current,
147
+ tsIso: new Date().toISOString(),
148
+ method: obj.method,
149
+ params: obj.params,
150
+ };
151
+ setEntries((prev) => {
152
+ const next = [entry, ...prev];
153
+ return next.slice(0, 50);
154
+ });
155
+ };
156
+ window.addEventListener('message', onMessage);
157
+ return () => window.removeEventListener('message', onMessage);
158
+ }, []);
159
+ return entries;
160
+ }
161
+ /**
162
+ * Build a default param payload for each canonical method. The shapes
163
+ * mirror the spec examples (apps.mdx:963-1102) exactly so any host
164
+ * with a strict validator accepts them.
165
+ */
166
+ function defaultParams(method) {
167
+ switch (method) {
168
+ case 'ui/message':
169
+ // Empirical 2026-05-03: claude.ai's validator wants `content`
170
+ // as a ContentBlock[] array, NOT the single-object shape the
171
+ // spec example (apps.mdx:1006-1011) shows. Other hosts may
172
+ // accept either; an array satisfies both.
173
+ return {
174
+ role: 'user',
175
+ content: [
176
+ { type: 'text', text: '[ggui:probe] hello from the protocol probe card' },
177
+ ],
178
+ };
179
+ case 'ui/update-model-context':
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text',
184
+ text: '[ggui:probe] this user prefers concise replies',
185
+ },
186
+ ],
187
+ };
188
+ case 'ui/open-link':
189
+ return { url: 'https://ggui.ai' };
190
+ case 'ui/request-display-mode':
191
+ return { mode: 'fullscreen' };
192
+ case 'tools/call':
193
+ // Mirror `runtime.ts::emitAudit` + `runtime.ts::dispatchWiredAction`
194
+ // so this probe empirically validates the canonical
195
+ // `GguiUserActionInput` envelope (SPEC §4.6, see
196
+ // `@ggui-ai/protocol/integrations/mcp-apps`). `ggui_runtime_submit_action`
197
+ // MUST be server-registered with `_meta.ui.visibility: ['app']`
198
+ // for claude.ai to honor the call (spec §401). With registration
199
+ // in place, the server echoes the discriminated envelope back as
200
+ // a tool result; the host forwards that result to this iframe
201
+ // via the JSON-RPC response matching our outbound id.
202
+ //
203
+ // The probe uses `kind: 'dispatch'` so the chained Pattern β
204
+ // 3-message follow-up (`ui/update-model-context` + `ui/message`)
205
+ // semantically matches a real wired-action click.
206
+ return {
207
+ name: 'ggui_runtime_submit_action',
208
+ arguments: {
209
+ kind: 'dispatch',
210
+ payload: {
211
+ intent: 'probe-click',
212
+ data: { note: 'hello from the protocol probe' },
213
+ },
214
+ sessionId: 'probe-session',
215
+ appId: 'probe-app',
216
+ actionId: '00000000', // overwritten in onClick with real FNV-1a
217
+ firedAt: new Date().toISOString(),
218
+ },
219
+ };
220
+ }
221
+ }
222
+ /**
223
+ * Compute a short deterministic action-id from an action payload.
224
+ * FNV-1a 32-bit, 8 hex chars — not cryptographically strong, just
225
+ * collision-resistant enough for in-flight wired-action correlation.
226
+ *
227
+ * Why hash (vs. random UUID): the id reproduces from
228
+ * `{intent, data, firedAt}` so both the silent context-update AND
229
+ * the loud consent message arrive at the SAME id without
230
+ * cross-state coordination. The LLM can then cross-check
231
+ * `actionId` in message ↔ `actionId` in pending-action context
232
+ * before acting — catches the case where a second click overwrote
233
+ * the context between message-fire and user-confirm.
234
+ */
235
+ function hashAction(payload) {
236
+ let hash = 0x811c9dc5;
237
+ for (let i = 0; i < payload.length; i += 1) {
238
+ hash ^= payload.charCodeAt(i);
239
+ hash = Math.imul(hash, 0x01000193);
240
+ }
241
+ return (hash >>> 0).toString(16).padStart(8, '0');
242
+ }
243
+ /**
244
+ * Render an action's `data` payload as a short inline string for
245
+ * embedding in a chat message. Goal: readable by humans, not a JSON
246
+ * dump. Falls back to truncated JSON for nested / non-scalar values
247
+ * so the consent prompt doesn't drop information silently.
248
+ */
249
+ function formatActionDataInline(data) {
250
+ if (data === null || data === undefined)
251
+ return '';
252
+ if (typeof data !== 'object' || Array.isArray(data))
253
+ return '';
254
+ const entries = Object.entries(data);
255
+ if (entries.length === 0)
256
+ return '';
257
+ const parts = entries.map(([k, v]) => {
258
+ if (v === null)
259
+ return `${k}: null`;
260
+ if (typeof v === 'string')
261
+ return `${k}: ${v}`;
262
+ if (typeof v === 'number' || typeof v === 'boolean')
263
+ return `${k}: ${v}`;
264
+ // Fallback for nested objects / arrays — keep it terse.
265
+ const json = JSON.stringify(v);
266
+ return `${k}: ${json.length > 40 ? `${json.slice(0, 37)}…` : json}`;
267
+ });
268
+ return parts.join(', ');
269
+ }
270
+ function ProbeButtonRow({ method, description, call, }) {
271
+ const [outcome, setOutcome] = React.useState({ state: 'idle' });
272
+ const onClick = React.useCallback(() => {
273
+ const startedAtMs = Date.now();
274
+ setOutcome({ state: 'pending', startedAtMs });
275
+ const params = defaultParams(method);
276
+ void call(method, params).then((resp) => {
277
+ if (resp.error) {
278
+ setOutcome({
279
+ state: 'error',
280
+ code: resp.error.code ?? '?',
281
+ message: resp.error.message ?? '(no message)',
282
+ });
283
+ return;
284
+ }
285
+ setOutcome({ state: 'success', result: resp.result ?? {} });
286
+ // Tools/call chain. After the gateway tool acknowledges, fire
287
+ // TWO follow-ups so the LLM gets both human-readable AND
288
+ // machine-readable signals about the action:
289
+ //
290
+ // 1. `ui/update-model-context` (silent) — structured payload
291
+ // describing the pending action. Persists in context so
292
+ // the LLM has unambiguous tool-args data instead of
293
+ // having to parse "tomorrow 3pm" out of natural language.
294
+ // Spec §1099: each call overwrites the previous; we DON'T
295
+ // attempt to accumulate (latest click wins, which matches
296
+ // most UX flows).
297
+ //
298
+ // 2. `ui/message` (consent prompt) — natural-language
299
+ // surface of the same action. claude.ai pre-fills the
300
+ // input with this text and asks the user to confirm
301
+ // before sending. The consent IS the prompt-injection
302
+ // firewall (visibility:['app'] keeps the LLM from
303
+ // seeing view-issued tool calls directly).
304
+ //
305
+ // LLM next turn sees: the user-confirmed message + the
306
+ // structured pending-action context. Combination reduces
307
+ // tool-arg ambiguity dramatically — production actionSpec
308
+ // routing would also include `tool: actionSpec.<intent>.tool`
309
+ // in the structured context so the LLM knows the exact
310
+ // downstream tool to call.
311
+ if (method === 'tools/call') {
312
+ const args = params
313
+ .arguments ?? {};
314
+ // New gesture-envelope shape (SPEC §4.6): `{kind, payload,
315
+ // sessionId, appId, actionId, firedAt}`. For Pattern β the
316
+ // `payload` carries `{intent, data}`.
317
+ const payload = args['payload'] ?? {};
318
+ const intent = String(payload['intent'] ?? '?');
319
+ const data = payload['data'];
320
+ const inlineData = formatActionDataInline(data);
321
+ const dataPart = inlineData === '' ? '' : ` (${inlineData})`;
322
+ const firedAt = new Date().toISOString();
323
+ // Action id binds the silent context update and the loud
324
+ // consent message together. LLM verifies `actionId` in the
325
+ // message matches `actionId` in pending-action context
326
+ // before acting — guarantees the args it uses are the args
327
+ // we authorized (not a stale or overwritten set).
328
+ const actionId = hashAction(`${intent}|${JSON.stringify(data ?? null)}|${firedAt}`);
329
+ // Silent — structured hint for the LLM. `actionId` is the
330
+ // verification token; rest carries the exact args.
331
+ void call('ui/update-model-context', {
332
+ content: [
333
+ {
334
+ type: 'text',
335
+ text: `[ggui:pending-action] ${JSON.stringify({
336
+ actionId,
337
+ intent,
338
+ data: data ?? null,
339
+ firedAt,
340
+ })}`,
341
+ },
342
+ ],
343
+ });
344
+ // Loud — user-confirmable surface. Three layers:
345
+ // - Intent in **bold** so the user sees what they're
346
+ // authorizing at a glance.
347
+ // - Inline data so the consent prompt is human-readable
348
+ // (no "approve a black box" UX).
349
+ // - Trailing `[id: <hash>]` stamp the LLM cross-checks
350
+ // against the pending-action context. Mismatch → LLM
351
+ // should refuse to act (production prompt-engineering).
352
+ void call('ui/message', {
353
+ role: 'user',
354
+ content: [
355
+ {
356
+ type: 'text',
357
+ text: `Please proceed with **${intent}**${dataPart}. [id: \`${actionId}\`]`,
358
+ },
359
+ ],
360
+ });
361
+ }
362
+ });
363
+ }, [call, method]);
364
+ const buttonLabel = outcome.state === 'pending'
365
+ ? 'Sending…'
366
+ : outcome.state === 'success'
367
+ ? 'Send again ✓'
368
+ : outcome.state === 'error'
369
+ ? 'Retry'
370
+ : 'Send';
371
+ const statusBadge = outcome.state === 'idle' ? (_jsx(Badge, { variant: "default", children: "idle" })) : outcome.state === 'pending' ? (_jsx(Badge, { variant: "info", children: "pending" })) : outcome.state === 'success' ? (_jsx(Badge, { variant: "success", children: "success" })) : (_jsx(Badge, { variant: "warning", children: "error" }));
372
+ return (_jsxs("div", { style: {
373
+ display: 'flex',
374
+ flexDirection: 'column',
375
+ gap: 'var(--ggui-spacing-2, 8px)',
376
+ padding: 'var(--ggui-spacing-3, 12px)',
377
+ borderRadius: 'var(--ggui-shape-radius-md, 10px)',
378
+ border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
379
+ background: 'var(--ggui-color-surface, #fff)',
380
+ }, children: [_jsxs("div", { style: {
381
+ display: 'flex',
382
+ alignItems: 'center',
383
+ justifyContent: 'space-between',
384
+ gap: 'var(--ggui-spacing-3, 12px)',
385
+ flexWrap: 'wrap',
386
+ }, children: [_jsx("code", { style: {
387
+ fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
388
+ fontSize: 'var(--ggui-font-size-sm, 13px)',
389
+ color: 'var(--ggui-color-onSurface, #111)',
390
+ fontWeight: 'var(--ggui-font-weight-semibold, 600)',
391
+ }, children: method }), statusBadge] }), _jsx(Text, { variant: "caption", style: {
392
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
393
+ fontSize: 'var(--ggui-font-size-xs, 11px)',
394
+ }, children: description }), _jsx("div", { style: { display: 'flex', gap: 'var(--ggui-spacing-2, 8px)' }, children: _jsx(Button, { variant: "primary", size: "sm", onClick: onClick, disabled: outcome.state === 'pending', children: buttonLabel }) }), outcome.state === 'success' || outcome.state === 'error' ? (_jsx("pre", { style: {
395
+ margin: 0,
396
+ fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
397
+ fontSize: 'var(--ggui-font-size-xs, 11px)',
398
+ color: outcome.state === 'error'
399
+ ? 'var(--ggui-color-warning, #b45309)'
400
+ : 'var(--ggui-color-onSurfaceVariant, #52525b)',
401
+ background: 'var(--ggui-color-neutral-100, #f4f4f5)',
402
+ padding: 'var(--ggui-spacing-2, 8px)',
403
+ borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
404
+ overflow: 'auto',
405
+ maxHeight: '120px',
406
+ whiteSpace: 'pre-wrap',
407
+ wordBreak: 'break-word',
408
+ }, children: outcome.state === 'error'
409
+ ? `${outcome.code}: ${outcome.message}`
410
+ : JSON.stringify(outcome.result, null, 2) })) : null] }));
411
+ }
412
+ export function ProtocolProbeCard({ intent, }) {
413
+ const call = useJsonRpcCaller();
414
+ const log = useNotificationLog();
415
+ return (_jsx(Card, { padding: "lg", border: false, shadow: "none", style: { backgroundColor: 'transparent' }, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "sm", children: [_jsxs("div", { style: {
416
+ display: 'flex',
417
+ alignItems: 'center',
418
+ gap: 'var(--ggui-spacing-3, 12px)',
419
+ flexWrap: 'wrap',
420
+ }, children: [_jsx(Heading, { level: 3, children: "MCP Apps protocol probe" }), _jsx(Badge, { variant: "info", children: "diagnostic" })] }), _jsxs(Text, { style: {
421
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
422
+ lineHeight: '1.55',
423
+ }, children: ["One button per primary host effect emitted by the iframe runtime's gesture surfaces (Pattern \u03B1 / Pattern \u03B2 / native- idiom interceptors). Click each to verify which methods this host honors. The", _jsx("code", { children: "tools/call" }), " row chains the full Pattern \u03B2 bridge (audit + context + consent), reproducing the production", _jsx("code", { children: "dispatchWiredAction" }), " envelope shape verbatim. The receive log below captures every notification the host emits while this card is mounted."] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(ProbeButtonRow, { method: "ui/message", description: "Pattern \u03B2 step 3 \u2014 consent-gated user authorization. Host pre-fills + waits for user confirm before the LLM acts on the paired pending-action context.", call: call }), _jsx(ProbeButtonRow, { method: "ui/update-model-context", description: "Pattern \u03B2 step 2 \u2014 silent structured `[ggui:pending-action]` context drop. LLM honors on next turn. No immediate UI.", call: call }), _jsx(ProbeButtonRow, { method: "ui/open-link", description: "Native-idiom anchor click interceptor primary effect. Opens ggui.ai in the host's browser / new tab. Real iframe code triggers via `<a href target='_blank'>` or cross-origin href.", call: call }), _jsx(ProbeButtonRow, { method: "ui/request-display-mode", description: "Native-idiom fullscreen interceptor primary effect. Request fullscreen \u2014 real iframe code calls `el.requestFullscreen()`; the runtime overrides the prototype to route through here.", call: call }), _jsx(ProbeButtonRow, { method: "tools/call", description: "Pattern \u03B2 full bridge: `tools/call ggui_runtime_submit_action` (audit, kind:'dispatch') + `ui/update-model-context` + `ui/message`. Spec \u00A7401 requires `_meta.ui.visibility:['app']` on the receiving tool. Pattern \u03B1 (direct fire to a same-server app-visible target) would skip steps 2+3 and add a second `tools/call` to the target tool instead.", call: call })] }), _jsxs(Stack, { gap: "sm", children: [_jsxs("div", { style: {
424
+ display: 'flex',
425
+ alignItems: 'baseline',
426
+ justifyContent: 'space-between',
427
+ gap: 'var(--ggui-spacing-3, 12px)',
428
+ }, children: [_jsx(Heading, { level: 4, children: "Receive log" }), _jsxs(Text, { variant: "caption", style: {
429
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
430
+ fontSize: 'var(--ggui-font-size-xs, 11px)',
431
+ }, children: [log.length, " ", log.length === 1 ? 'event' : 'events'] })] }), log.length === 0 ? (_jsx(Text, { variant: "caption", style: {
432
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
433
+ fontStyle: 'italic',
434
+ }, children: "No host notifications observed yet. Click a probe button or wait for the host to emit one." })) : (_jsx("div", { style: {
435
+ display: 'flex',
436
+ flexDirection: 'column',
437
+ gap: 'var(--ggui-spacing-1, 4px)',
438
+ maxHeight: '320px',
439
+ overflow: 'auto',
440
+ padding: 'var(--ggui-spacing-2, 8px)',
441
+ background: 'var(--ggui-color-neutral-100, #f4f4f5)',
442
+ border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
443
+ borderRadius: 'var(--ggui-shape-radius-md, 10px)',
444
+ }, children: log.map((entry) => (_jsxs("details", { style: {
445
+ fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
446
+ fontSize: 'var(--ggui-font-size-xs, 11px)',
447
+ }, children: [_jsxs("summary", { style: {
448
+ cursor: 'pointer',
449
+ padding: 'var(--ggui-spacing-1, 4px) 0',
450
+ color: 'var(--ggui-color-onSurface, #111)',
451
+ }, children: [_jsx("span", { style: {
452
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
453
+ marginRight: '8px',
454
+ }, children: entry.tsIso.slice(11, 19) }), _jsx("span", { style: { fontWeight: 600 }, children: entry.method })] }), _jsx("pre", { style: {
455
+ margin: '4px 0 0 16px',
456
+ whiteSpace: 'pre-wrap',
457
+ wordBreak: 'break-word',
458
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
459
+ }, children: JSON.stringify(entry.params, null, 2) })] }, entry.id))) }))] }), intent ? (_jsxs(Text, { variant: "caption", style: {
460
+ color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
461
+ opacity: 0.7,
462
+ }, children: ["Triggered by: ", intent] })) : null] }) }));
463
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tiny QR code component — renders a URL as a scannable SVG QR code.
3
+ *
4
+ * The mobile-best fallback for system cards that need to send the
5
+ * user out to a real browser. From inside an MCP App iframe sandbox
6
+ * (claudemcpcontent.com etc.), no JS API can spawn a browser tab —
7
+ * `window.open` is blocked, `target="_blank"` silently fails, and
8
+ * postMessage host-intents (`ui/request/open-url`) are speculative.
9
+ * A QR code bypasses every sandbox limit: phone camera scans →
10
+ * opens the URL in real Safari/Chrome with no host cooperation
11
+ * required.
12
+ *
13
+ * Renders inline SVG (no canvas, no images, no network fetch). Bundle
14
+ * cost: `qrcode-generator` is ~6 KB unminified, ~3 KB gzipped — tiny
15
+ * compared to the alternative of "tell the user to scan a clipboard".
16
+ */
17
+ import * as React from 'react';
18
+ export interface QrCodeProps {
19
+ /** URL to encode. Truncated implicitly by the QR encoder if too long. */
20
+ readonly value: string;
21
+ /**
22
+ * Side length in CSS pixels. Default 128 — readable on a phone
23
+ * camera at arm's length without being visually dominant on the
24
+ * card.
25
+ */
26
+ readonly size?: number;
27
+ /**
28
+ * Error correction level. `M` (medium, ~15% recoverable) is the
29
+ * default sweet spot — robust to dust / glare without bloating
30
+ * module count for short URLs. Bump to `Q` or `H` only when
31
+ * encoding into a high-traffic surface.
32
+ */
33
+ readonly errorCorrection?: 'L' | 'M' | 'Q' | 'H';
34
+ /** Foreground color (the dots). Defaults to theme onSurface. */
35
+ readonly foreground?: string;
36
+ /** Background color. Defaults to theme surface. */
37
+ readonly background?: string;
38
+ /** Optional aria-label override. */
39
+ readonly ariaLabel?: string;
40
+ }
41
+ /**
42
+ * Render a QR code as inline SVG. Returns `null` when the encoder
43
+ * fails (e.g., URL too long for any error-correction level) so the
44
+ * caller can render a fallback.
45
+ */
46
+ export declare function QrCode({ value, size, errorCorrection, foreground, background, ariaLabel, }: QrCodeProps): React.JSX.Element | null;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Tiny QR code component — renders a URL as a scannable SVG QR code.
4
+ *
5
+ * The mobile-best fallback for system cards that need to send the
6
+ * user out to a real browser. From inside an MCP App iframe sandbox
7
+ * (claudemcpcontent.com etc.), no JS API can spawn a browser tab —
8
+ * `window.open` is blocked, `target="_blank"` silently fails, and
9
+ * postMessage host-intents (`ui/request/open-url`) are speculative.
10
+ * A QR code bypasses every sandbox limit: phone camera scans →
11
+ * opens the URL in real Safari/Chrome with no host cooperation
12
+ * required.
13
+ *
14
+ * Renders inline SVG (no canvas, no images, no network fetch). Bundle
15
+ * cost: `qrcode-generator` is ~6 KB unminified, ~3 KB gzipped — tiny
16
+ * compared to the alternative of "tell the user to scan a clipboard".
17
+ */
18
+ import * as React from 'react';
19
+ import qrcodeFactory from 'qrcode-generator';
20
+ /**
21
+ * Render a QR code as inline SVG. Returns `null` when the encoder
22
+ * fails (e.g., URL too long for any error-correction level) so the
23
+ * caller can render a fallback.
24
+ */
25
+ export function QrCode({ value, size = 128, errorCorrection = 'M', foreground = 'var(--ggui-color-onSurface, #18181b)', background = 'var(--ggui-color-surface, #ffffff)', ariaLabel, }) {
26
+ const moduleMatrix = React.useMemo(() => {
27
+ try {
28
+ // typeNumber=0 lets the encoder pick the smallest fit. The
29
+ // qrcode-generator default-export is a factory function —
30
+ // calling it returns a `qr` object with `.addData()` /
31
+ // `.make()` / `.getModuleCount()` / `.isDark(row, col)`.
32
+ const qr = qrcodeFactory(0, errorCorrection);
33
+ qr.addData(value);
34
+ qr.make();
35
+ const count = qr.getModuleCount();
36
+ const matrix = [];
37
+ for (let row = 0; row < count; row += 1) {
38
+ const r = [];
39
+ for (let col = 0; col < count; col += 1) {
40
+ r.push(qr.isDark(row, col));
41
+ }
42
+ matrix.push(r);
43
+ }
44
+ return matrix;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }, [value, errorCorrection]);
50
+ if (moduleMatrix === null)
51
+ return null;
52
+ const moduleCount = moduleMatrix.length;
53
+ // 1px viewBox per module — let CSS handle final size scaling.
54
+ return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: `0 0 ${moduleCount} ${moduleCount}`, width: size, height: size, role: "img", "aria-label": ariaLabel ?? `QR code for ${value}`, shapeRendering: "crispEdges", style: { display: 'block', flexShrink: 0 }, children: [_jsx("rect", { width: moduleCount, height: moduleCount, fill: background }), moduleMatrix.flatMap((row, y) => row.map((isDark, x) => isDark ? (_jsx("rect", { x: x, y: y, width: 1, height: 1, fill: foreground }, `${x}-${y}`)) : null))] }));
55
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Best-effort detection that we're embedded inside Claude (web or
3
+ * desktop). Heuristics:
4
+ *
5
+ * 1. Hostname endsWith `claudemcpcontent.com` → claude.ai web.
6
+ * claude.ai loads the runtime through this sandbox proxy
7
+ * origin, so the iframe's own location lands here.
8
+ * 2. Document referrer contains `claude.ai` → loaded from the
9
+ * claude.ai chat surface (covers cases where the runtime
10
+ * iframe inherits the parent origin via srcdoc).
11
+ * 3. Hostname/UA contains `claude` (case-insensitive) → covers
12
+ * Claude Desktop's `desktop.claude.ai` / similar.
13
+ *
14
+ * Returns `false` when `window` is undefined (SSR / Node test
15
+ * environments) — the caller can default to a non-Claude theme.
16
+ *
17
+ * @public
18
+ */
19
+ export declare function isInsideClaude(): boolean;
20
+ /**
21
+ * React hook returning the user's preferred color scheme. Listens to
22
+ * the `prefers-color-scheme` media query so the card re-themes live
23
+ * when the user toggles between light and dark in their OS / browser
24
+ * settings.
25
+ *
26
+ * Defaults to `'light'` when `window.matchMedia` is unavailable
27
+ * (older runtimes, SSR pre-paint).
28
+ *
29
+ * @public
30
+ */
31
+ export declare function useColorScheme(): 'light' | 'dark';