@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
package/dist/stack.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory model of the visible stack. Owned by the runtime; mutated
|
|
3
|
+
* via the methods (no setter — keeps the "model is the truth" doctrine
|
|
4
|
+
* obvious in code reviews).
|
|
5
|
+
*/
|
|
6
|
+
export class StackModel {
|
|
7
|
+
items = [];
|
|
8
|
+
filterToItemId;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.filterToItemId = options.filterToItemId;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Replace the entire stack — used after the subscribe ack lands.
|
|
14
|
+
*
|
|
15
|
+
* Under `filterToItemId` (single-item mode), the incoming list is
|
|
16
|
+
* filtered to just the matching entry. If no entry matches, the
|
|
17
|
+
* model ends up empty — the server no longer includes the pinned
|
|
18
|
+
* item and the renderer's empty-state is the correct surface.
|
|
19
|
+
*/
|
|
20
|
+
setAll(items) {
|
|
21
|
+
if (this.filterToItemId !== undefined) {
|
|
22
|
+
const target = this.filterToItemId;
|
|
23
|
+
this.items = items.filter((item) => item.id === target);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.items = [...items];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Upsert a stack item. If an entry with the same id already exists
|
|
30
|
+
* its slot is replaced in place (preserves ordering); otherwise the
|
|
31
|
+
* item is appended at the tail. Mirrors `GguiSession.tsx`'s push
|
|
32
|
+
* reducer.
|
|
33
|
+
*
|
|
34
|
+
* Under `filterToItemId` (single-item mode): entries whose id does
|
|
35
|
+
* not match the filter are ignored. The renderer's live-channel
|
|
36
|
+
* subscription still delivers every push for the session (subscribe
|
|
37
|
+
* remains session-scoped); this filter is the client-side pin that
|
|
38
|
+
* keeps the per-card iframe bound to its one stack entry.
|
|
39
|
+
*/
|
|
40
|
+
upsert(item) {
|
|
41
|
+
if (this.filterToItemId !== undefined && item.id !== this.filterToItemId) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const idx = this.items.findIndex((existing) => existing.id === item.id);
|
|
45
|
+
if (idx >= 0) {
|
|
46
|
+
const next = [...this.items];
|
|
47
|
+
next[idx] = item;
|
|
48
|
+
this.items = next;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.items = [...this.items, item];
|
|
52
|
+
}
|
|
53
|
+
/** Read-only view — callers MUST NOT mutate. */
|
|
54
|
+
snapshot() {
|
|
55
|
+
return this.items;
|
|
56
|
+
}
|
|
57
|
+
/** Item count — convenience for status-line rendering. */
|
|
58
|
+
size() {
|
|
59
|
+
return this.items.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Best-effort textual label for a stack item — title-ish for the
|
|
64
|
+
* placeholder UI. Tries `description` → `prompt` → a generic fallback.
|
|
65
|
+
*
|
|
66
|
+
* The wire shape keeps these fields optional on every entry kind
|
|
67
|
+
* (generated, McpApps), so the absence-handling has to be honest.
|
|
68
|
+
*/
|
|
69
|
+
function labelFor(item) {
|
|
70
|
+
if (typeof item.description === 'string' && item.description.length > 0) {
|
|
71
|
+
return item.description;
|
|
72
|
+
}
|
|
73
|
+
// Only generated component items carry `prompt`. McpApps + system
|
|
74
|
+
// variants surface their own description (or a generic fallback).
|
|
75
|
+
if (item.type !== 'mcpApps' && item.type !== 'system') {
|
|
76
|
+
if (typeof item.prompt === 'string' && item.prompt.length > 0) {
|
|
77
|
+
return item.prompt;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return '(untitled stack item)';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Render the stack model into the target list element as structural
|
|
84
|
+
* `<li data-ggui-stack-item="<id>">` placeholders. Idempotent — every
|
|
85
|
+
* call replaces the element's children based on the current model.
|
|
86
|
+
*
|
|
87
|
+
* The DOM shape (data attributes + classnames) is intentionally
|
|
88
|
+
* stable: E2E tests and the React-mount path both need to address
|
|
89
|
+
* stack-item rows by `data-ggui-stack-item` selector.
|
|
90
|
+
*/
|
|
91
|
+
export function renderStackInto(target, model) {
|
|
92
|
+
// Replace children rather than mutate-in-place — the placeholder
|
|
93
|
+
// doesn't need diffing, and the wire-frame burst rate is low enough
|
|
94
|
+
// (push events arrive roughly per agent turn) that full-replace is
|
|
95
|
+
// cheap.
|
|
96
|
+
target.replaceChildren();
|
|
97
|
+
const items = model.snapshot();
|
|
98
|
+
if (items.length === 0) {
|
|
99
|
+
const empty = target.ownerDocument.createElement('li');
|
|
100
|
+
empty.textContent = '(no stack items yet)';
|
|
101
|
+
empty.setAttribute('data-ggui-empty', 'true');
|
|
102
|
+
target.appendChild(empty);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const li = target.ownerDocument.createElement('li');
|
|
107
|
+
li.setAttribute('data-ggui-stack-item', item.id);
|
|
108
|
+
const idEl = target.ownerDocument.createElement('span');
|
|
109
|
+
idEl.className = 'id';
|
|
110
|
+
idEl.textContent = item.id;
|
|
111
|
+
li.appendChild(idEl);
|
|
112
|
+
const labelEl = target.ownerDocument.createElement('span');
|
|
113
|
+
labelEl.className = 'label';
|
|
114
|
+
labelEl.textContent = labelFor(item);
|
|
115
|
+
li.appendChild(labelEl);
|
|
116
|
+
target.appendChild(li);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-iframe status DOM. Extracted from `runtime.ts` in B3b so the
|
|
3
|
+
* channel-client handlers can import the `setStatus` + status-refs
|
|
4
|
+
* helpers without a circular dep on the runtime module (which imports
|
|
5
|
+
* the handlers for registration at boot).
|
|
6
|
+
*
|
|
7
|
+
* Operators see the boot state at a glance; failures land here with a
|
|
8
|
+
* prefix that makes them grep-able in dev consoles.
|
|
9
|
+
*
|
|
10
|
+
* The status DOM is a `<div data-ggui-status>` + `<ul data-ggui-stack>`
|
|
11
|
+
* pair built once at boot and reused for the iframe's lifetime. The
|
|
12
|
+
* `data-ggui-status` attribute carries a {@link StatusKind} value so
|
|
13
|
+
* operator stylesheets + e2e specs can pin assertions against the
|
|
14
|
+
* narrow union rather than reading the text content.
|
|
15
|
+
*/
|
|
16
|
+
import type { StackModel } from './stack.js';
|
|
17
|
+
/**
|
|
18
|
+
* Closed union of values the `data-ggui-status` attribute carries.
|
|
19
|
+
*
|
|
20
|
+
* - `idle` — initial state before any boot IO. Default text reads
|
|
21
|
+
* "Initializing renderer…"; set by {@link ensureStatusDom}.
|
|
22
|
+
* - `initializing` — same as `idle` (legacy alias kept for the rare
|
|
23
|
+
* external CSS hook in `e2e/`).
|
|
24
|
+
* - `connecting` — ui/initialize fired or WS handshake in flight.
|
|
25
|
+
* - `connected` — first ack landed + every subsequent successful
|
|
26
|
+
* push. Text shows the live stack count.
|
|
27
|
+
* - `reconnecting` — WS dropped; the reconnect ladder is running.
|
|
28
|
+
* - `disconnected` — terminal close + ladder exhausted, or a graceful
|
|
29
|
+
* stop. Authors may layer reload UX here.
|
|
30
|
+
* - `upgrade-required` — version handshake failed; operator should
|
|
31
|
+
* upgrade the client.
|
|
32
|
+
* - `error` — every other failure (ui/initialize, bootstrap parse,
|
|
33
|
+
* WS handshake non-version).
|
|
34
|
+
*
|
|
35
|
+
* Closed at this layer; new values require a code change here PLUS
|
|
36
|
+
* any e2e specs / CSS that pattern-match against the attribute.
|
|
37
|
+
*/
|
|
38
|
+
export type StatusKind = 'idle' | 'initializing' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'upgrade-required' | 'error';
|
|
39
|
+
/**
|
|
40
|
+
* Refs handed back from {@link ensureStatusDom}. Stays narrow (two
|
|
41
|
+
* elements) so the channel handlers can close over them without
|
|
42
|
+
* pulling other runtime state.
|
|
43
|
+
*/
|
|
44
|
+
export interface StatusRefs {
|
|
45
|
+
readonly status: HTMLElement;
|
|
46
|
+
readonly stack: HTMLElement;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the minimum viable status DOM the runtime owns. Adopters can
|
|
50
|
+
* style via the `data-ggui-status` / `data-ggui-stack` attributes; the
|
|
51
|
+
* bundle ships zero CSS so hosts see only the structure.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ensureStatusDom(doc: Document): StatusRefs;
|
|
54
|
+
/**
|
|
55
|
+
* Set the status text + the `data-ggui-status` attribute. Centralised
|
|
56
|
+
* so every caller stays consistent across the runtime + the channel
|
|
57
|
+
* handlers (push handler keeps the count fresh on every fold).
|
|
58
|
+
*/
|
|
59
|
+
export declare function setStatus(refs: StatusRefs, text: string, state: StatusKind): void;
|
|
60
|
+
/**
|
|
61
|
+
* Convenience — render the `connected (N items)` status line based on
|
|
62
|
+
* the current stack model size. Used by the push handler on every
|
|
63
|
+
* successful upsert + by the boot sequence on first ack.
|
|
64
|
+
*/
|
|
65
|
+
export declare function setConnectedStatus(refs: StatusRefs, model: StackModel): void;
|
|
66
|
+
/**
|
|
67
|
+
* Re-render the stack DOM into the placeholder element. Wraps the
|
|
68
|
+
* standalone {@link renderStackInto} so callers can pass refs + model
|
|
69
|
+
* without unpacking; the standalone export stays for the legacy
|
|
70
|
+
* callers that have only the bare element in scope.
|
|
71
|
+
*/
|
|
72
|
+
export declare function refreshStackDom(refs: StatusRefs, model: StackModel): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { renderStackInto } from './stack.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the minimum viable status DOM the runtime owns. Adopters can
|
|
4
|
+
* style via the `data-ggui-status` / `data-ggui-stack` attributes; the
|
|
5
|
+
* bundle ships zero CSS so hosts see only the structure.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureStatusDom(doc) {
|
|
8
|
+
// Reuse pre-existing nodes if the thin shell wrote them — lets the
|
|
9
|
+
// shell inline a status line if it wants to.
|
|
10
|
+
const existingStatus = doc.querySelector('[data-ggui-status]');
|
|
11
|
+
const existingStack = doc.querySelector('[data-ggui-stack]');
|
|
12
|
+
if (existingStatus !== null && existingStack !== null) {
|
|
13
|
+
return { status: existingStatus, stack: existingStack };
|
|
14
|
+
}
|
|
15
|
+
const root = doc.body;
|
|
16
|
+
const status = doc.createElement('div');
|
|
17
|
+
status.setAttribute('data-ggui-status', 'idle');
|
|
18
|
+
status.textContent = 'Initializing renderer…';
|
|
19
|
+
const stack = doc.createElement('ul');
|
|
20
|
+
stack.setAttribute('data-ggui-stack', '');
|
|
21
|
+
root.appendChild(status);
|
|
22
|
+
root.appendChild(stack);
|
|
23
|
+
return { status, stack };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Set the status text + the `data-ggui-status` attribute. Centralised
|
|
27
|
+
* so every caller stays consistent across the runtime + the channel
|
|
28
|
+
* handlers (push handler keeps the count fresh on every fold).
|
|
29
|
+
*/
|
|
30
|
+
export function setStatus(refs, text, state) {
|
|
31
|
+
refs.status.textContent = text;
|
|
32
|
+
refs.status.setAttribute('data-ggui-status', state);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convenience — render the `connected (N items)` status line based on
|
|
36
|
+
* the current stack model size. Used by the push handler on every
|
|
37
|
+
* successful upsert + by the boot sequence on first ack.
|
|
38
|
+
*/
|
|
39
|
+
export function setConnectedStatus(refs, model) {
|
|
40
|
+
const size = model.size();
|
|
41
|
+
setStatus(refs, `Connected (${size} item${size === 1 ? '' : 's'}).`, 'connected');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Re-render the stack DOM into the placeholder element. Wraps the
|
|
45
|
+
* standalone {@link renderStackInto} so callers can pass refs + model
|
|
46
|
+
* without unpacking; the standalone export stays for the legacy
|
|
47
|
+
* callers that have only the bare element in scope.
|
|
48
|
+
*/
|
|
49
|
+
export function refreshStackDom(refs, model) {
|
|
50
|
+
renderStackInto(refs.stack, model);
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-credentials system card.
|
|
3
|
+
*
|
|
4
|
+
* Rendered when the server processed a `ggui_push` for a session whose
|
|
5
|
+
* LLM credentials are missing or unresolvable. The user needs to open
|
|
6
|
+
* the server's `/settings` page in a real browser tab to paste an API
|
|
7
|
+
* key.
|
|
8
|
+
*
|
|
9
|
+
* Open-url strategy is delegated to {@link requestOpenUrl} — fires
|
|
10
|
+
* `window.open`, programmatic anchor click, `ui/request/open-url`
|
|
11
|
+
* postMessage, and `tools/call:open_url` postMessage in parallel,
|
|
12
|
+
* with `clipboard.writeText` as the guaranteed-floor fallback. The
|
|
13
|
+
* card observes the `{outcome, mechanism}` result and shows distinct
|
|
14
|
+
* copy for opened / copied / unsupported so the user understands
|
|
15
|
+
* what happened. Hosts that block popups (claude.ai's
|
|
16
|
+
* `claudemcpcontent.com` iframe sandbox) gracefully degrade to the
|
|
17
|
+
* clipboard path; the URL block's `user-select: all` covers the
|
|
18
|
+
* worst case.
|
|
19
|
+
*/
|
|
20
|
+
import * as React from 'react';
|
|
21
|
+
export interface NoCredentialsCardProps {
|
|
22
|
+
/**
|
|
23
|
+
* Absolute URL of the server's settings page. Constructed at boot
|
|
24
|
+
* from `--public-base-url` (or falls back to `host:port`). MUST be
|
|
25
|
+
* absolute — host iframes resolve relative URLs against their own
|
|
26
|
+
* sandbox origin (e.g. `claudemcpcontent.com`), where `/settings`
|
|
27
|
+
* goes to a 404.
|
|
28
|
+
*/
|
|
29
|
+
readonly settingsUrl: string;
|
|
30
|
+
/** Optional: the original prompt that triggered the gen, surfaced as a footnote. */
|
|
31
|
+
readonly intent?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function NoCredentialsCard({ settingsUrl, intent, }: NoCredentialsCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* No-credentials system card.
|
|
4
|
+
*
|
|
5
|
+
* Rendered when the server processed a `ggui_push` for a session whose
|
|
6
|
+
* LLM credentials are missing or unresolvable. The user needs to open
|
|
7
|
+
* the server's `/settings` page in a real browser tab to paste an API
|
|
8
|
+
* key.
|
|
9
|
+
*
|
|
10
|
+
* Open-url strategy is delegated to {@link requestOpenUrl} — fires
|
|
11
|
+
* `window.open`, programmatic anchor click, `ui/request/open-url`
|
|
12
|
+
* postMessage, and `tools/call:open_url` postMessage in parallel,
|
|
13
|
+
* with `clipboard.writeText` as the guaranteed-floor fallback. The
|
|
14
|
+
* card observes the `{outcome, mechanism}` result and shows distinct
|
|
15
|
+
* copy for opened / copied / unsupported so the user understands
|
|
16
|
+
* what happened. Hosts that block popups (claude.ai's
|
|
17
|
+
* `claudemcpcontent.com` iframe sandbox) gracefully degrade to the
|
|
18
|
+
* clipboard path; the URL block's `user-select: all` covers the
|
|
19
|
+
* worst case.
|
|
20
|
+
*/
|
|
21
|
+
import * as React from 'react';
|
|
22
|
+
import { Card, Stack, Heading, Text, Button, Badge, } from '@ggui-ai/design/primitives';
|
|
23
|
+
import { requestOpenUrl } from './host-intents.js';
|
|
24
|
+
import { QrCode } from './QrCode.js';
|
|
25
|
+
const BUILD_ID = typeof __GGUI_RUNTIME_BUILD_ID__ === 'string'
|
|
26
|
+
? __GGUI_RUNTIME_BUILD_ID__
|
|
27
|
+
: 'dev';
|
|
28
|
+
/**
|
|
29
|
+
* ggui wordmark — `g g u i` in the brand's two-tone alternation.
|
|
30
|
+
* Inlined SVG (24px tall) so the bundle picks up no asset dependency.
|
|
31
|
+
* Colors track theme tokens: `chrome` ≈ surface, `ink` ≈ onSurface.
|
|
32
|
+
* Each glyph sits in a 50×50 box with 8px gaps.
|
|
33
|
+
*/
|
|
34
|
+
function GguiWordmark() {
|
|
35
|
+
// `text-primary` flips dark↔light per theme mode (charcoal in light,
|
|
36
|
+
// off-white in dark). `gray-300` flips too — the gray scale in
|
|
37
|
+
// `darkTheme` is intentionally inverted, so the wordmark's
|
|
38
|
+
// contrasting "chrome" tone reads correctly under either theme.
|
|
39
|
+
const ink = 'var(--ggui-color-onSurface, #292929)';
|
|
40
|
+
const chrome = 'var(--ggui-color-neutral-300, #d1d5db)';
|
|
41
|
+
return (_jsxs("svg", { viewBox: "0 0 224 50", width: "72", height: "16", role: "img", "aria-label": "ggui", style: { display: 'block', flexShrink: 0 }, children: [_jsx("path", { d: "M 0 0 H 50 V 25 H 25 V 50 H 0 Z", fill: chrome }), _jsx("rect", { x: "33", y: "33", width: "17", height: "17", fill: ink }), _jsx("path", { d: "M 58 0 H 108 V 25 H 83 V 50 H 58 Z", fill: ink }), _jsx("rect", { x: "91", y: "33", width: "17", height: "17", fill: chrome }), _jsx("path", { d: "M 141 50 C 154.807 50 166 38.8071 166 25 V 0 H 116 V 25 C 116 38.8071 127.193 50 141 50 Z", fill: ink }), _jsx("rect", { x: "174", y: "0", width: "50", height: "50", fill: chrome })] }));
|
|
42
|
+
}
|
|
43
|
+
const OUTCOME_TTL_MS = 4000;
|
|
44
|
+
export function NoCredentialsCard({ settingsUrl, intent, }) {
|
|
45
|
+
const [ctaState, setCtaState] = React.useState('idle');
|
|
46
|
+
const onPrimary = React.useCallback(() => {
|
|
47
|
+
// Diagnostic logging — surfaces in the iframe's devtools console
|
|
48
|
+
// (claude.ai web: "Open frame in new tab" then DevTools; desktop:
|
|
49
|
+
// sandboxed and console-less, so we ALSO surface state in the
|
|
50
|
+
// CTA label). The `[ggui:no-creds-card]` prefix makes these
|
|
51
|
+
// greppable.
|
|
52
|
+
if (typeof console !== 'undefined') {
|
|
53
|
+
// eslint-disable-next-line no-console -- diagnostic trace, see helper docstring
|
|
54
|
+
console.log('[ggui:no-creds-card] click → settingsUrl =', settingsUrl);
|
|
55
|
+
}
|
|
56
|
+
setCtaState('working');
|
|
57
|
+
void requestOpenUrl(settingsUrl).then((result) => {
|
|
58
|
+
if (typeof console !== 'undefined') {
|
|
59
|
+
// eslint-disable-next-line no-console -- diagnostic trace, see helper docstring
|
|
60
|
+
console.log('[ggui:no-creds-card] requestOpenUrl resolved →', result);
|
|
61
|
+
}
|
|
62
|
+
const next = result.outcome === 'opened'
|
|
63
|
+
? 'opened'
|
|
64
|
+
: result.outcome === 'copied'
|
|
65
|
+
? 'copied'
|
|
66
|
+
: 'manual';
|
|
67
|
+
setCtaState(next);
|
|
68
|
+
if (typeof window !== 'undefined') {
|
|
69
|
+
window.setTimeout(() => setCtaState('idle'), OUTCOME_TTL_MS);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}, [settingsUrl]);
|
|
73
|
+
const ctaLabel = ctaState === 'working'
|
|
74
|
+
? 'Opening…'
|
|
75
|
+
: ctaState === 'opened'
|
|
76
|
+
? 'Opened in browser ✓'
|
|
77
|
+
: ctaState === 'copied'
|
|
78
|
+
? 'URL copied — paste in your browser'
|
|
79
|
+
: ctaState === 'manual'
|
|
80
|
+
? 'Tap the URL above to copy'
|
|
81
|
+
: 'Open settings';
|
|
82
|
+
const ctaDisabled = ctaState === 'working';
|
|
83
|
+
// Color tokens that auto-flip between light + dark mode. Only the
|
|
84
|
+
// tokens defined in BOTH `lightTheme` and `darkTheme` are safe to
|
|
85
|
+
// use without fallback divergence — `surface`, `text-primary`,
|
|
86
|
+
// `text-secondary`, and the gray scale (which is intentionally
|
|
87
|
+
// INVERTED in darkTheme so `gray-100` is light-on-dark when in
|
|
88
|
+
// dark mode and dark-on-light when in light mode).
|
|
89
|
+
//
|
|
90
|
+
// - `--ggui-color-surface` — opaque card surface (white in light,
|
|
91
|
+
// slate-800 in dark). Used for the URL block + QR backdrop.
|
|
92
|
+
// - `--ggui-color-onSurface` — high-contrast body text.
|
|
93
|
+
// - `--ggui-color-onSurfaceVariant` — muted captions/headings.
|
|
94
|
+
// - `--ggui-color-neutral-100` — subtle tinted background that
|
|
95
|
+
// auto-flips. Used for the inner "details" panel — slightly
|
|
96
|
+
// differentiated from outer transparent canvas without being
|
|
97
|
+
// a hard surface.
|
|
98
|
+
// - `--ggui-color-neutral-200` — subtle outline that auto-flips.
|
|
99
|
+
return (
|
|
100
|
+
// Outer Card is forced transparent — when this card renders inside
|
|
101
|
+
// an MCP App iframe (claude.ai web, Claude Desktop), the host
|
|
102
|
+
// already draws its own chat-bubble surface. A second opaque
|
|
103
|
+
// background creates a card-on-card look. Padding stays so content
|
|
104
|
+
// doesn't crowd the iframe edges. Outside Claude (preview iframe,
|
|
105
|
+
// console inspector), the ThemeProvider's body background is the
|
|
106
|
+
// theme's `--ggui-color-surface`, so a transparent card just
|
|
107
|
+
// shows the theme's canvas behind it — also fine.
|
|
108
|
+
_jsx(Card, { padding: "lg", border: false, shadow: "none", style: { backgroundColor: 'transparent' }, children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "md", children: [_jsxs("div", { style: {
|
|
109
|
+
display: 'flex',
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
justifyContent: 'space-between',
|
|
112
|
+
gap: 'var(--ggui-spacing-3, 12px)',
|
|
113
|
+
}, children: [_jsx(GguiWordmark, {}), _jsx(Badge, { variant: "info", children: "Setup needed" })] }), _jsxs("div", { style: {
|
|
114
|
+
display: 'flex',
|
|
115
|
+
alignItems: 'baseline',
|
|
116
|
+
gap: 'var(--ggui-spacing-3, 12px)',
|
|
117
|
+
flexWrap: 'wrap',
|
|
118
|
+
}, children: [_jsx(Heading, { level: 3, children: "Connect an LLM provider" }), _jsx("code", { style: {
|
|
119
|
+
fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
120
|
+
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
121
|
+
color: 'var(--ggui-color-onSurfaceVariant, #71717a)',
|
|
122
|
+
opacity: 0.7,
|
|
123
|
+
userSelect: 'all',
|
|
124
|
+
}, title: "iframe-runtime build identifier \u2014 useful for diagnosing stale caches", children: BUILD_ID })] }), _jsx(Text, { style: {
|
|
125
|
+
color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
|
|
126
|
+
lineHeight: '1.55',
|
|
127
|
+
}, children: "ggui needs an LLM key to render UIs. Open this server\u2019s settings page to paste an Anthropic, OpenAI, Google, or OpenRouter API key." })] }), _jsx("div", { style: {
|
|
128
|
+
display: 'flex',
|
|
129
|
+
flexDirection: 'column',
|
|
130
|
+
gap: 'var(--ggui-spacing-3, 12px)',
|
|
131
|
+
padding: 'var(--ggui-spacing-4, 16px)',
|
|
132
|
+
background: 'var(--ggui-color-neutral-100, #f4f4f5)',
|
|
133
|
+
border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
|
|
134
|
+
borderRadius: 'var(--ggui-shape-radius-md, 10px)',
|
|
135
|
+
}, children: _jsxs("div", { style: {
|
|
136
|
+
display: 'flex',
|
|
137
|
+
gap: 'var(--ggui-spacing-4, 16px)',
|
|
138
|
+
alignItems: 'flex-start',
|
|
139
|
+
flexWrap: 'wrap',
|
|
140
|
+
}, children: [_jsxs("div", { style: {
|
|
141
|
+
display: 'flex',
|
|
142
|
+
flexDirection: 'column',
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
gap: 'var(--ggui-spacing-2, 8px)',
|
|
145
|
+
}, children: [_jsx("div", { style: {
|
|
146
|
+
padding: 'var(--ggui-spacing-2, 8px)',
|
|
147
|
+
background: '#ffffff',
|
|
148
|
+
borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
|
|
149
|
+
lineHeight: 0,
|
|
150
|
+
}, children: _jsx(QrCode, { value: settingsUrl, size: 120, foreground: "#000000", background: "#ffffff" }) }), _jsx(Text, { variant: "caption", style: {
|
|
151
|
+
color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
|
|
152
|
+
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
153
|
+
textAlign: 'center',
|
|
154
|
+
}, children: "Scan with your phone" })] }), _jsxs("div", { style: {
|
|
155
|
+
display: 'flex',
|
|
156
|
+
flexDirection: 'column',
|
|
157
|
+
gap: 'var(--ggui-spacing-2, 8px)',
|
|
158
|
+
flex: '1 1 240px',
|
|
159
|
+
minWidth: '200px',
|
|
160
|
+
}, children: [_jsx(Text, { variant: "caption", style: {
|
|
161
|
+
color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
|
|
162
|
+
fontWeight: 'var(--ggui-font-weight-semibold, 600)',
|
|
163
|
+
textTransform: 'uppercase',
|
|
164
|
+
letterSpacing: '0.06em',
|
|
165
|
+
fontSize: 'var(--ggui-font-size-xs, 11px)',
|
|
166
|
+
}, children: "Or open this URL in your browser" }), _jsx("div", { style: {
|
|
167
|
+
fontFamily: 'var(--ggui-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
168
|
+
fontSize: 'var(--ggui-font-size-sm, 13px)',
|
|
169
|
+
color: 'var(--ggui-color-onSurface, #111)',
|
|
170
|
+
wordBreak: 'break-all',
|
|
171
|
+
userSelect: 'all',
|
|
172
|
+
padding: 'var(--ggui-spacing-3, 12px)',
|
|
173
|
+
background: 'var(--ggui-color-surface, #fff)',
|
|
174
|
+
border: '1px solid var(--ggui-color-neutral-200, #e4e4e7)',
|
|
175
|
+
borderRadius: 'var(--ggui-shape-radius-sm, 6px)',
|
|
176
|
+
lineHeight: '1.45',
|
|
177
|
+
}, children: settingsUrl }), _jsxs("div", { style: {
|
|
178
|
+
display: 'flex',
|
|
179
|
+
alignItems: 'center',
|
|
180
|
+
gap: 'var(--ggui-spacing-3, 12px)',
|
|
181
|
+
flexWrap: 'wrap',
|
|
182
|
+
}, children: [_jsx(Button, { variant: "primary", size: "md", onClick: onPrimary, disabled: ctaDisabled, children: ctaLabel }), ctaState === 'idle' || ctaState === 'working' ? (_jsx(Text, { variant: "caption", style: {
|
|
183
|
+
color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
|
|
184
|
+
opacity: 0.85,
|
|
185
|
+
}, children: "or tap-and-hold the URL above to copy" })) : null] })] })] }) }), intent ? (_jsxs(Text, { variant: "caption", style: {
|
|
186
|
+
color: 'var(--ggui-color-onSurfaceVariant, #52525b)',
|
|
187
|
+
opacity: 0.7,
|
|
188
|
+
}, children: ["Pending prompt: ", intent] })) : null] }) }));
|
|
189
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Apps protocol probe system card.
|
|
3
|
+
*
|
|
4
|
+
* Diagnostic + regression fixture exercising every gesture surface
|
|
5
|
+
* the iframe runtime routes:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Pattern β bridge** — `tools/call ggui_runtime_submit_action` (audit) +
|
|
8
|
+
* `ui/update-model-context` (silent context) + `ui/message`
|
|
9
|
+
* (consent prompt). Fires for cross-server / non-app-visible
|
|
10
|
+
* `actionSpec[name].dispatch.tool` targets, OR any
|
|
11
|
+
* `dispatch.kind === 'agent'` entry. Empirical-first reproduction
|
|
12
|
+
* of the production `dispatchWiredAction` path.
|
|
13
|
+
* 2. **Pattern α direct fire** — `tools/call ggui_runtime_submit_action`
|
|
14
|
+
* (audit) + `tools/call <T>` (direct, no consent prompt). Fires
|
|
15
|
+
* for same-server, app-visible target tools. Probes against
|
|
16
|
+
* `ggui_runtime_submit_action` itself (the only universally-app-visible
|
|
17
|
+
* tool the iframe can be sure exists).
|
|
18
|
+
* 3. **Native-idiom: anchor click** — `ui/open-link` paired with
|
|
19
|
+
* `kind: 'openLink'` audit. Triggered by the runtime's
|
|
20
|
+
* capture-phase `<a href>` interceptor (cross-origin or
|
|
21
|
+
* `target="_blank"`). The probe row simulates the postMessage
|
|
22
|
+
* directly so the operator sees the envelope without needing to
|
|
23
|
+
* click an anchor.
|
|
24
|
+
* 4. **Native-idiom: fullscreen** — `ui/request-display-mode` +
|
|
25
|
+
* `kind: 'requestDisplayMode'` audit. Triggered by the runtime's
|
|
26
|
+
* `Element.prototype.requestFullscreen` /
|
|
27
|
+
* `Document.prototype.exitFullscreen` overrides. The probe row
|
|
28
|
+
* simulates the postMessage; the row labelled "exitFullscreen"
|
|
29
|
+
* sends `mode: 'inline'` per the spec'd inverse mapping.
|
|
30
|
+
*
|
|
31
|
+
* Each button transitions through `idle → pending → success | error`
|
|
32
|
+
* so the operator can see exactly which methods the embedding host
|
|
33
|
+
* (claude.ai web, Claude Desktop, …) actually honors today. The
|
|
34
|
+
* persistent receive-log captures every `ui/notifications/*` and
|
|
35
|
+
* host-issued request seen while the card is mounted.
|
|
36
|
+
*
|
|
37
|
+
* Why this lives outside the wire-context: system cards (`kind:
|
|
38
|
+
* 'mcp-apps-probe'`, `'no-credentials'`, …) mount in a render-only
|
|
39
|
+
* branch of `bootSelfContained` that does NOT install
|
|
40
|
+
* `<GguiWireProvider>` — they exist precisely to surface protocol
|
|
41
|
+
* state when the wire-context might not be live (auth missing,
|
|
42
|
+
* compile failed, etc.). The probe card mirrors the postMessage
|
|
43
|
+
* envelopes the runtime's gesture surfaces would emit, derived from
|
|
44
|
+
* the canonical `SubmitActionEnvelope` shape in
|
|
45
|
+
* `@ggui-ai/protocol/integrations/mcp-apps`.
|
|
46
|
+
*
|
|
47
|
+
* Originating context: an earlier runtime posted its outbound
|
|
48
|
+
* action dispatch only as `{method:'tools/call',
|
|
49
|
+
* name:'ggui_runtime_submit_action', ...}`. Per MCP Apps spec §401, hosts MUST
|
|
50
|
+
* reject `tools/call` from views for tools without `'app'` in
|
|
51
|
+
* visibility — claude.ai silently dropped the call. The original
|
|
52
|
+
* probe diagnosed the gap; the routing simplification work landed
|
|
53
|
+
* Pattern α / Pattern β / native idioms in response.
|
|
54
|
+
*/
|
|
55
|
+
import * as React from 'react';
|
|
56
|
+
export interface ProtocolProbeCardProps {
|
|
57
|
+
/**
|
|
58
|
+
* Optional: the original prompt that triggered the probe push.
|
|
59
|
+
* Surfaced as a footnote so the card matches the framing of other
|
|
60
|
+
* system cards (`NoCredentialsCard`).
|
|
61
|
+
*/
|
|
62
|
+
readonly intent?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function ProtocolProbeCard({ intent, }: ProtocolProbeCardProps): React.JSX.Element;
|