@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,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';
|