@ggui-ai/iframe-runtime 0.5.0-rc.1 → 0.6.0
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/dist/iframe-runtime.js +104 -74
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/observability.d.ts +39 -1
- package/dist/provisional-renderer.js +8 -2
- package/dist/runtime.js +14 -0
- package/dist/system-cards/UiFeedbackCard.d.ts +46 -0
- package/dist/system-cards/UiFeedbackCard.js +133 -0
- package/dist/ui-feedback-chrome.d.ts +40 -0
- package/dist/ui-feedback-chrome.js +83 -0
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export { parseMetaFromGlobal, parseMetaFromToolResult, validateMeta, } from './m
|
|
|
37
37
|
export type { RendererBootFailureReason, RendererBootFailedMessage, } from './runtime.js';
|
|
38
38
|
export type { ProtocolError, ProtocolErrorEmitter, BootstrapFailureReason, } from './protocol-error.js';
|
|
39
39
|
export { defaultProtocolErrorEmitter, fromBootstrapFailure, fromClientContractViolation, fromTransportFailure, fromAuthFailure, fromUpgradeRequired, fromUnknown, } from './protocol-error.js';
|
|
40
|
-
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, AuthRequiredEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, UnknownObservabilityEvent, } from './observability.js';
|
|
40
|
+
export type { ObservabilityEvent, ObservabilityEmitter, ObservabilityMessage, SchemaVersionMismatchEvent, SubscribeFailedEvent, AuthRequiredEvent, ChannelTransportPickedEvent, ChannelTransportFallbackEvent, ChannelTransportResubscribedEvent, UiFeedbackEvent, UnknownObservabilityEvent, } from './observability.js';
|
|
41
41
|
export { postObservabilityToParent } from './observability.js';
|
|
42
42
|
export type { LifecycleEmitter } from './lifecycle.js';
|
|
43
43
|
export { postLifecycleToParent, makeLifecycleEvent } from './lifecycle.js';
|
package/dist/observability.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { MCP_APP_OBSERVE_TYPE } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
|
33
33
|
*
|
|
34
34
|
* @public
|
|
35
35
|
*/
|
|
36
|
-
export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UnknownObservabilityEvent;
|
|
36
|
+
export type ObservabilityEvent = SchemaVersionMismatchEvent | SubscribeFailedEvent | AuthRequiredEvent | ChannelTransportPickedEvent | ChannelTransportFallbackEvent | ChannelTransportResubscribedEvent | UiFeedbackEvent | UnknownObservabilityEvent;
|
|
37
37
|
/**
|
|
38
38
|
* Fired when the protocol-version handshake rejects the connection.
|
|
39
39
|
* Parallel to `UpgradeRequiredError` — the host can choose to render
|
|
@@ -152,6 +152,44 @@ export interface ChannelTransportResubscribedEvent {
|
|
|
152
152
|
readonly sessionId: string;
|
|
153
153
|
readonly channelName: string;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Fired when the end user submits the runtime's in-iframe UI-feedback
|
|
157
|
+
* affordance ("did this generated UI work for you?"). Field semantics
|
|
158
|
+
* mirror `UiFeedbackPayload` in `@ggui-ai/react` / `@ggui-ai/react-native`
|
|
159
|
+
* (the host-chrome twin of this affordance):
|
|
160
|
+
*
|
|
161
|
+
* - `verdict` — `'love'` / `'dislike'`, or `'other'` for the
|
|
162
|
+
* free-text flow.
|
|
163
|
+
* - `comment` — present only for `verdict: 'other'` with a non-empty
|
|
164
|
+
* trimmed comment.
|
|
165
|
+
* - `sessionId` / `toolName` — present exactly when the runtime knew
|
|
166
|
+
* them at mount time.
|
|
167
|
+
*
|
|
168
|
+
* Observability-only — feedback is host-app chrome with ZERO wire
|
|
169
|
+
* surface: the agent cannot observe it (it is neither an action nor
|
|
170
|
+
* context), so it rides this renderer ↔ host seam instead of the
|
|
171
|
+
* agent ↔ UI contract.
|
|
172
|
+
*
|
|
173
|
+
* The affordance mounts only when the runtime document has a parent
|
|
174
|
+
* window (`window.parent !== window`) — a top-level tab has no
|
|
175
|
+
* `ggui:observe` egress, and a dead affordance must never render.
|
|
176
|
+
* Hosts that also own DOM chrome around the iframe MUST wire exactly
|
|
177
|
+
* ONE feedback surface: either their own chrome (the `onUiFeedback`
|
|
178
|
+
* host-callback component) or this event arm — never both, or the
|
|
179
|
+
* user sees two affordances for one render.
|
|
180
|
+
*
|
|
181
|
+
* @public
|
|
182
|
+
*/
|
|
183
|
+
export interface UiFeedbackEvent {
|
|
184
|
+
readonly kind: 'ui-feedback';
|
|
185
|
+
readonly verdict: 'love' | 'dislike' | 'other';
|
|
186
|
+
/** Trimmed free-text comment; only on `verdict: 'other'`, never empty. */
|
|
187
|
+
readonly comment?: string;
|
|
188
|
+
/** GguiSession id of the render the feedback is about. */
|
|
189
|
+
readonly sessionId?: string;
|
|
190
|
+
/** Tool that produced the render (e.g. `ggui_render`). */
|
|
191
|
+
readonly toolName?: string;
|
|
192
|
+
}
|
|
155
193
|
/**
|
|
156
194
|
* Catch-all branch for event kinds the host's typings don't recognize.
|
|
157
195
|
* Lets the union stay extensible without forcing protocol-version
|
|
@@ -48,6 +48,12 @@ const PREVIEW_CHANNEL = '_ggui:preview';
|
|
|
48
48
|
import { parseServerMessage, } from '@ggui-ai/preview-a2ui';
|
|
49
49
|
import { Button, Card, Checkbox, Divider, Heading, Icon, Image, Input, Row, Select, Spinner, Stack, Text, } from '@ggui-ai/design/primitives';
|
|
50
50
|
import { PreviewFragmentEnter, PreviewSurface, StreamingText, } from '@ggui-ai/design/preview';
|
|
51
|
+
// A2UI Text carries INLINE markdown (the block level comes from
|
|
52
|
+
// `variant`) — rendered through the shared @silverprotocol/richtext
|
|
53
|
+
// subset via the design system's inline renderer. Streaming-safe:
|
|
54
|
+
// prefixes parse to stable ASTs, so re-emitting a longer `text`
|
|
55
|
+
// only extends the trailing construct.
|
|
56
|
+
import { MarkdownInline } from '@ggui-ai/design';
|
|
51
57
|
const A2UI_ROOT_ID = 'root';
|
|
52
58
|
const EMPTY_STATE = {
|
|
53
59
|
surfaceId: null,
|
|
@@ -152,7 +158,7 @@ function renderComponent(component, all) {
|
|
|
152
158
|
const headingMatch = /^h([1-6])$/.exec(variant);
|
|
153
159
|
if (headingMatch) {
|
|
154
160
|
const level = Number(headingMatch[1]);
|
|
155
|
-
return wrap(createElement(Heading, { level }, createElement(StreamingText, null, component.text)));
|
|
161
|
+
return wrap(createElement(Heading, { level }, createElement(StreamingText, null, createElement(MarkdownInline, { text: component.text }))));
|
|
156
162
|
}
|
|
157
163
|
// A2UI 'caption' / 'label' map onto Text's size/weight/tone
|
|
158
164
|
// axes; anything else degrades to default body text.
|
|
@@ -161,7 +167,7 @@ function renderComponent(component, all) {
|
|
|
161
167
|
: variant === 'label'
|
|
162
168
|
? { size: 'sm', weight: 'medium' }
|
|
163
169
|
: {};
|
|
164
|
-
return wrap(createElement(Text, textProps, createElement(StreamingText, null, component.text)));
|
|
170
|
+
return wrap(createElement(Text, textProps, createElement(StreamingText, null, createElement(MarkdownInline, { text: component.text }))));
|
|
165
171
|
}
|
|
166
172
|
case 'Image':
|
|
167
173
|
return wrap(createElement(Image, { src: component.src, alt: component.alt ?? '' }));
|
package/dist/runtime.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadCompiledValidatorsFromUrl } from './compiled-validators.js';
|
|
|
15
15
|
import { mountRender, } from './render-item.js';
|
|
16
16
|
import { fromBootstrapFailure, } from './protocol-error.js';
|
|
17
17
|
import { postObservabilityToParent, } from './observability.js';
|
|
18
|
+
import { mountUiFeedbackChrome } from './ui-feedback-chrome.js';
|
|
18
19
|
import { makeLifecycleEvent, postLifecycleToParent, } from './lifecycle.js';
|
|
19
20
|
import { contextSlotLastValues, createContextStateHost, installContextRegistry, reemitLastContextValues, } from './context-observer.js';
|
|
20
21
|
const RENDERER_VERSION = typeof __GGUI_RUNTIME_VERSION__ === 'string' ? __GGUI_RUNTIME_VERSION__ : 'dev';
|
|
@@ -367,6 +368,19 @@ export async function bootSequence(opts) {
|
|
|
367
368
|
...(onObserve !== undefined ? { onObserve } : {}),
|
|
368
369
|
})
|
|
369
370
|
: null;
|
|
371
|
+
// In-iframe UI-feedback affordance (ggui#244) — mounted adjacent to
|
|
372
|
+
// the session root, gated inside the helper on `window.parent !==
|
|
373
|
+
// window` (top-level `/r/<shortCode>` tabs have no `ggui:observe`
|
|
374
|
+
// egress, so they get NO affordance). Interaction emits a
|
|
375
|
+
// `ui-feedback` ObservabilityEvent through the boot path's emitter;
|
|
376
|
+
// when no emitter was injected the postMessage-to-parent default
|
|
377
|
+
// still gives the mounted affordance a live sink (the gate already
|
|
378
|
+
// proved a parent exists). Fire-and-forget: chrome never blocks or
|
|
379
|
+
// fails the boot.
|
|
380
|
+
void mountUiFeedbackChrome(doc, {
|
|
381
|
+
emit: onObserve ?? postObservabilityToParent,
|
|
382
|
+
sessionId: meta.sessionId,
|
|
383
|
+
});
|
|
370
384
|
setStatus(refs, `Connecting to ${meta.sessionId}…`, 'connecting');
|
|
371
385
|
// Boot-without-renderer path: we still need a ChannelRegistry to
|
|
372
386
|
// receive frames, because the registry is the only dispatch
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UiFeedbackCard — the runtime's in-iframe UI-feedback affordance
|
|
3
|
+
* ("did this generated UI work for you?").
|
|
4
|
+
*
|
|
5
|
+
* The in-iframe twin of the host-chrome `UiFeedback` component in
|
|
6
|
+
* `@ggui-ai/react`: same three verdicts — Love / Dislike / Other
|
|
7
|
+
* (free-text comment) — same acknowledgement after submit, same
|
|
8
|
+
* dismiss control, same payload semantics (trimmed comment only on
|
|
9
|
+
* `verdict: 'other'`, context stamps present exactly when known).
|
|
10
|
+
* Where the host-chrome twin hands a payload to an `onUiFeedback`
|
|
11
|
+
* callback, this card builds a {@link UiFeedbackEvent} and hands it to
|
|
12
|
+
* the injected {@link ObservabilityEmitter} — production binds the
|
|
13
|
+
* `ggui:observe` postMessage-to-parent default, tests record.
|
|
14
|
+
*
|
|
15
|
+
* This is CHROME the runtime mounts adjacent to the session root
|
|
16
|
+
* (see `ui-feedback-chrome.ts`), NOT a system card: it has no
|
|
17
|
+
* `SystemGguiSession.kind`, no registry entry, and no server-side
|
|
18
|
+
* producer. It lives in this directory because it follows the
|
|
19
|
+
* system-card authoring idiom — inline styles with `--ggui-*`
|
|
20
|
+
* CSS-variable hooks and {@link useColorScheme} adaptation — and
|
|
21
|
+
* because the dynamic-import posture keeps it out of the runtime's
|
|
22
|
+
* base module graph.
|
|
23
|
+
*
|
|
24
|
+
* No ThemeProvider on purpose: the adjacent user render owns the
|
|
25
|
+
* document's `--ggui-*` variable layer (its own theme injection wrote
|
|
26
|
+
* `:root`), and wrapping this card in a ThemeProvider would overwrite
|
|
27
|
+
* those variables last-writer-wins. Inline `var(--ggui-*, fallback)`
|
|
28
|
+
* styles pick up the render's theme when present; the
|
|
29
|
+
* scheme-conditional fallbacks keep the card readable when no theme
|
|
30
|
+
* variables were injected at all.
|
|
31
|
+
*/
|
|
32
|
+
import * as React from 'react';
|
|
33
|
+
import type { ObservabilityEmitter } from '../observability.js';
|
|
34
|
+
export interface UiFeedbackCardProps {
|
|
35
|
+
/**
|
|
36
|
+
* Sink for the built {@link UiFeedbackEvent}. Required — the mount
|
|
37
|
+
* gate (`ui-feedback-chrome.ts`) guarantees a reachable parent
|
|
38
|
+
* before this card ever renders, so there is no unsunk state.
|
|
39
|
+
*/
|
|
40
|
+
readonly emit: ObservabilityEmitter;
|
|
41
|
+
/** GguiSession id stamped onto every emitted event, when known. */
|
|
42
|
+
readonly sessionId?: string;
|
|
43
|
+
/** Producing tool name stamped onto every emitted event, when known. */
|
|
44
|
+
readonly toolName?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function UiFeedbackCard({ emit, sessionId, toolName, }: UiFeedbackCardProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* UiFeedbackCard — the runtime's in-iframe UI-feedback affordance
|
|
4
|
+
* ("did this generated UI work for you?").
|
|
5
|
+
*
|
|
6
|
+
* The in-iframe twin of the host-chrome `UiFeedback` component in
|
|
7
|
+
* `@ggui-ai/react`: same three verdicts — Love / Dislike / Other
|
|
8
|
+
* (free-text comment) — same acknowledgement after submit, same
|
|
9
|
+
* dismiss control, same payload semantics (trimmed comment only on
|
|
10
|
+
* `verdict: 'other'`, context stamps present exactly when known).
|
|
11
|
+
* Where the host-chrome twin hands a payload to an `onUiFeedback`
|
|
12
|
+
* callback, this card builds a {@link UiFeedbackEvent} and hands it to
|
|
13
|
+
* the injected {@link ObservabilityEmitter} — production binds the
|
|
14
|
+
* `ggui:observe` postMessage-to-parent default, tests record.
|
|
15
|
+
*
|
|
16
|
+
* This is CHROME the runtime mounts adjacent to the session root
|
|
17
|
+
* (see `ui-feedback-chrome.ts`), NOT a system card: it has no
|
|
18
|
+
* `SystemGguiSession.kind`, no registry entry, and no server-side
|
|
19
|
+
* producer. It lives in this directory because it follows the
|
|
20
|
+
* system-card authoring idiom — inline styles with `--ggui-*`
|
|
21
|
+
* CSS-variable hooks and {@link useColorScheme} adaptation — and
|
|
22
|
+
* because the dynamic-import posture keeps it out of the runtime's
|
|
23
|
+
* base module graph.
|
|
24
|
+
*
|
|
25
|
+
* No ThemeProvider on purpose: the adjacent user render owns the
|
|
26
|
+
* document's `--ggui-*` variable layer (its own theme injection wrote
|
|
27
|
+
* `:root`), and wrapping this card in a ThemeProvider would overwrite
|
|
28
|
+
* those variables last-writer-wins. Inline `var(--ggui-*, fallback)`
|
|
29
|
+
* styles pick up the render's theme when present; the
|
|
30
|
+
* scheme-conditional fallbacks keep the card readable when no theme
|
|
31
|
+
* variables were injected at all.
|
|
32
|
+
*/
|
|
33
|
+
import * as React from 'react';
|
|
34
|
+
import { useColorScheme } from './host-detect.js';
|
|
35
|
+
const FONT_FAMILY = "var(--ggui-font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif)";
|
|
36
|
+
/**
|
|
37
|
+
* Scheme-conditional style palette. The `var(--ggui-*, …)` fallbacks
|
|
38
|
+
* flip between the neutral light values (mirroring the host-chrome
|
|
39
|
+
* twin) and dark-readable equivalents so the card stays legible on
|
|
40
|
+
* hosts that render the iframe over a dark surface without injecting
|
|
41
|
+
* theme variables.
|
|
42
|
+
*/
|
|
43
|
+
function buildStyles(scheme) {
|
|
44
|
+
const dark = scheme === 'dark';
|
|
45
|
+
return {
|
|
46
|
+
root: {
|
|
47
|
+
display: 'inline-flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
gap: 4,
|
|
50
|
+
fontFamily: FONT_FAMILY,
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
lineHeight: 1.2,
|
|
53
|
+
color: dark
|
|
54
|
+
? 'var(--ggui-color-neutral-500, #9ca3af)'
|
|
55
|
+
: 'var(--ggui-color-neutral-500, #6b7280)',
|
|
56
|
+
},
|
|
57
|
+
verdictButton: {
|
|
58
|
+
appearance: 'none',
|
|
59
|
+
background: dark
|
|
60
|
+
? 'var(--ggui-color-neutral-50, #1f2937)'
|
|
61
|
+
: 'var(--ggui-color-neutral-50, #f9fafb)',
|
|
62
|
+
border: dark
|
|
63
|
+
? '1px solid var(--ggui-color-neutral-200, #374151)'
|
|
64
|
+
: '1px solid var(--ggui-color-neutral-200, #e5e7eb)',
|
|
65
|
+
borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
|
|
66
|
+
padding: '2px 8px',
|
|
67
|
+
fontFamily: 'inherit',
|
|
68
|
+
fontSize: 'inherit',
|
|
69
|
+
color: dark
|
|
70
|
+
? 'var(--ggui-color-neutral-600, #d1d5db)'
|
|
71
|
+
: 'var(--ggui-color-neutral-600, #4b5563)',
|
|
72
|
+
cursor: 'pointer',
|
|
73
|
+
},
|
|
74
|
+
dismissButton: {
|
|
75
|
+
appearance: 'none',
|
|
76
|
+
background: 'transparent',
|
|
77
|
+
border: 'none',
|
|
78
|
+
padding: '2px 4px',
|
|
79
|
+
fontFamily: 'inherit',
|
|
80
|
+
fontSize: 'inherit',
|
|
81
|
+
color: dark
|
|
82
|
+
? 'var(--ggui-color-neutral-400, #6b7280)'
|
|
83
|
+
: 'var(--ggui-color-neutral-400, #9ca3af)',
|
|
84
|
+
cursor: 'pointer',
|
|
85
|
+
},
|
|
86
|
+
commentForm: {
|
|
87
|
+
display: 'inline-flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
gap: 4,
|
|
90
|
+
margin: 0,
|
|
91
|
+
},
|
|
92
|
+
commentInput: {
|
|
93
|
+
fontFamily: 'inherit',
|
|
94
|
+
fontSize: 'inherit',
|
|
95
|
+
padding: '2px 6px',
|
|
96
|
+
minWidth: 140,
|
|
97
|
+
border: dark
|
|
98
|
+
? '1px solid var(--ggui-color-neutral-200, #374151)'
|
|
99
|
+
: '1px solid var(--ggui-color-neutral-200, #e5e7eb)',
|
|
100
|
+
borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
|
|
101
|
+
background: dark
|
|
102
|
+
? 'var(--ggui-color-neutral-50, #111827)'
|
|
103
|
+
: 'var(--ggui-color-neutral-50, #ffffff)',
|
|
104
|
+
color: dark
|
|
105
|
+
? 'var(--ggui-color-neutral-800, #e5e7eb)'
|
|
106
|
+
: 'var(--ggui-color-neutral-800, #1f2937)',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function UiFeedbackCard({ emit, sessionId, toolName, }) {
|
|
111
|
+
const [phase, setPhase] = React.useState('idle');
|
|
112
|
+
const [comment, setComment] = React.useState('');
|
|
113
|
+
const scheme = useColorScheme();
|
|
114
|
+
const styles = buildStyles(scheme);
|
|
115
|
+
const send = React.useCallback((verdict, commentText) => {
|
|
116
|
+
const trimmed = commentText?.trim();
|
|
117
|
+
emit({
|
|
118
|
+
kind: 'ui-feedback',
|
|
119
|
+
verdict,
|
|
120
|
+
...(trimmed !== undefined && trimmed.length > 0 ? { comment: trimmed } : {}),
|
|
121
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
122
|
+
...(toolName !== undefined ? { toolName } : {}),
|
|
123
|
+
});
|
|
124
|
+
setPhase('sent');
|
|
125
|
+
}, [emit, sessionId, toolName]);
|
|
126
|
+
const onCommentSubmit = React.useCallback((e) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
send('other', comment);
|
|
129
|
+
}, [send, comment]);
|
|
130
|
+
if (phase === 'dismissed')
|
|
131
|
+
return null;
|
|
132
|
+
return (_jsxs("div", { "data-ggui-ui-feedback": true, role: "group", "aria-label": "Share feedback on this UI", style: styles.root, children: [phase === 'sent' ? (_jsx("span", { "data-ggui-ui-feedback-thanks": true, children: "Thanks for the feedback" })) : phase === 'comment' ? (_jsxs("form", { onSubmit: onCommentSubmit, style: styles.commentForm, "data-ggui-ui-feedback-comment-form": true, children: [_jsx("input", { type: "text", value: comment, onChange: (e) => setComment(e.target.value), placeholder: "What happened?", "aria-label": "Feedback comment", "data-ggui-ui-feedback-comment": true, style: styles.commentInput, autoFocus: true }), _jsx("button", { type: "submit", style: styles.verdictButton, "data-ggui-ui-feedback-send": true, children: "Send" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "love", onClick: () => send('love'), children: "Love" }), _jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "dislike", onClick: () => send('dislike'), children: "Dislike" }), _jsx("button", { type: "button", style: styles.verdictButton, "data-ggui-ui-feedback-verdict": "other", onClick: () => setPhase('comment'), children: "Other\u2026" })] })), _jsx("button", { type: "button", style: styles.dismissButton, "aria-label": "Dismiss feedback", "data-ggui-ui-feedback-dismiss": true, onClick: () => setPhase('dismissed'), children: '×' })] }));
|
|
133
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ObservabilityEmitter } from './observability.js';
|
|
2
|
+
export interface UiFeedbackChromeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Observability sink the card emits `ui-feedback` events into.
|
|
5
|
+
* Production passes the boot path's emitter (default
|
|
6
|
+
* `postObservabilityToParent`); tests inject a recorder.
|
|
7
|
+
*/
|
|
8
|
+
readonly emit: ObservabilityEmitter;
|
|
9
|
+
/** GguiSession id stamped onto every emitted event, when known. */
|
|
10
|
+
readonly sessionId?: string;
|
|
11
|
+
/** Producing tool name stamped onto every emitted event, when known. */
|
|
12
|
+
readonly toolName?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Window whose `parent` is consulted for the sink-reachability
|
|
15
|
+
* gate. Defaults to the global `window`; tests inject fakes so the
|
|
16
|
+
* gate is exercisable without mutating jsdom's window graph.
|
|
17
|
+
*/
|
|
18
|
+
readonly win?: Window;
|
|
19
|
+
}
|
|
20
|
+
export interface UiFeedbackChromeHandle {
|
|
21
|
+
/** The appended `[data-ggui-ui-feedback-chrome]` container. */
|
|
22
|
+
readonly container: HTMLElement;
|
|
23
|
+
/** Tear down the React root + remove the container. */
|
|
24
|
+
unmount(): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Mount the in-iframe UI-feedback affordance, or do nothing.
|
|
28
|
+
*
|
|
29
|
+
* Returns `null` without touching the DOM when the gate fails (no
|
|
30
|
+
* parent window — top-level tab / non-browser environment). Otherwise
|
|
31
|
+
* appends a `<div data-ggui-ui-feedback-chrome>` to `doc.body` (after
|
|
32
|
+
* the session root, which the boot path appended first) and renders
|
|
33
|
+
* the card into it. Idempotent per document: a re-entrant mount
|
|
34
|
+
* replaces any previous chrome container — one affordance per
|
|
35
|
+
* document, never two.
|
|
36
|
+
*
|
|
37
|
+
* Fire-and-forget from the boot path's perspective: chrome must never
|
|
38
|
+
* block or fail the boot, so callers `void` the promise.
|
|
39
|
+
*/
|
|
40
|
+
export declare function mountUiFeedbackChrome(doc: Document, opts: UiFeedbackChromeOptions): Promise<UiFeedbackChromeHandle | null>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-iframe UI-feedback chrome mount (ggui#244).
|
|
3
|
+
*
|
|
4
|
+
* Mounts {@link import('./system-cards/UiFeedbackCard.js').UiFeedbackCard}
|
|
5
|
+
* in its own container ADJACENT to the session root (appended to
|
|
6
|
+
* `document.body`, so it sits below the rendered UI) — the same
|
|
7
|
+
* append-my-own-root posture as `status-dom.ts#ensureStatusDom`.
|
|
8
|
+
* Interaction egress is the existing `ggui:observe` renderer → host
|
|
9
|
+
* postMessage envelope: the card builds a `ui-feedback`
|
|
10
|
+
* {@link import('./observability.js').ObservabilityEvent} arm and hands
|
|
11
|
+
* it to the injected emitter (production binds
|
|
12
|
+
* `postObservabilityToParent`).
|
|
13
|
+
*
|
|
14
|
+
* Sink-reachability gate — never render a dead affordance:
|
|
15
|
+
*
|
|
16
|
+
* - `window.parent !== window` → a parent window exists, the
|
|
17
|
+
* `ggui:observe` envelope has somewhere to go → mount. Parents
|
|
18
|
+
* that don't understand the envelope ignore it (best-effort emit;
|
|
19
|
+
* one postMessage of cost).
|
|
20
|
+
* - `window.parent === window` → top-level tab (the `/r/<shortCode>`
|
|
21
|
+
* share-link viewer) → NO mount. There is no parent to receive
|
|
22
|
+
* the envelope, so a rendered affordance would drop every click
|
|
23
|
+
* on the floor.
|
|
24
|
+
*
|
|
25
|
+
* Hosts that own DOM chrome around the iframe wire exactly ONE
|
|
26
|
+
* feedback surface: either their own chrome (`onUiFeedback` on the
|
|
27
|
+
* host-side SDK components) or this in-iframe affordance's
|
|
28
|
+
* `ui-feedback` arm via `onObserve` — never both.
|
|
29
|
+
*
|
|
30
|
+
* The card + react-dom are dynamic-imported so this module stays
|
|
31
|
+
* feather-weight in the runtime's base graph (mirrors the system-card
|
|
32
|
+
* branch in `render-item.ts`); the import cost is only paid when the
|
|
33
|
+
* gate passes.
|
|
34
|
+
*/
|
|
35
|
+
import { createElement } from 'react';
|
|
36
|
+
/**
|
|
37
|
+
* Mount the in-iframe UI-feedback affordance, or do nothing.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` without touching the DOM when the gate fails (no
|
|
40
|
+
* parent window — top-level tab / non-browser environment). Otherwise
|
|
41
|
+
* appends a `<div data-ggui-ui-feedback-chrome>` to `doc.body` (after
|
|
42
|
+
* the session root, which the boot path appended first) and renders
|
|
43
|
+
* the card into it. Idempotent per document: a re-entrant mount
|
|
44
|
+
* replaces any previous chrome container — one affordance per
|
|
45
|
+
* document, never two.
|
|
46
|
+
*
|
|
47
|
+
* Fire-and-forget from the boot path's perspective: chrome must never
|
|
48
|
+
* block or fail the boot, so callers `void` the promise.
|
|
49
|
+
*/
|
|
50
|
+
export async function mountUiFeedbackChrome(doc, opts) {
|
|
51
|
+
const win = opts.win ?? (typeof window !== 'undefined' ? window : undefined);
|
|
52
|
+
// Gate: mount only when a parent window exists to receive the
|
|
53
|
+
// `ggui:observe` envelope. `win.parent === win` is the top-level-tab
|
|
54
|
+
// signature; a nullish parent means a detached/nonstandard context.
|
|
55
|
+
if (win === undefined || win.parent === null || win.parent === win) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const [reactDomClient, cardMod] = await Promise.all([
|
|
59
|
+
import('react-dom/client'),
|
|
60
|
+
import('./system-cards/UiFeedbackCard.js'),
|
|
61
|
+
]);
|
|
62
|
+
// Idempotency: a re-entrant mount replaces the previous chrome
|
|
63
|
+
// container wholesale (remove + fresh element) rather than calling
|
|
64
|
+
// `createRoot` twice on one node — React warns on double-rooting a
|
|
65
|
+
// container, and chrome carries no state worth preserving.
|
|
66
|
+
doc.querySelector('[data-ggui-ui-feedback-chrome]')?.remove();
|
|
67
|
+
const container = doc.createElement('div');
|
|
68
|
+
container.setAttribute('data-ggui-ui-feedback-chrome', '');
|
|
69
|
+
doc.body.appendChild(container);
|
|
70
|
+
const root = reactDomClient.createRoot(container);
|
|
71
|
+
root.render(createElement(cardMod.UiFeedbackCard, {
|
|
72
|
+
emit: opts.emit,
|
|
73
|
+
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
74
|
+
...(opts.toolName !== undefined ? { toolName: opts.toolName } : {}),
|
|
75
|
+
}));
|
|
76
|
+
return {
|
|
77
|
+
container,
|
|
78
|
+
unmount() {
|
|
79
|
+
root.unmount();
|
|
80
|
+
container.remove();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/iframe-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The iframe-local ggui runtime: a self-contained ESM bundle that boots inside an MCP Apps host iframe, runs the protocol-version handshake, opens the live-channel WebSocket, and mounts the rendered UI. Bundles React, ReactDOM, and the ggui design system so generated component code has a single seam to read from.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"qrcode-generator": "^2.0.4",
|
|
36
36
|
"react": "^19.0.0",
|
|
37
37
|
"react-dom": "^19.0.0",
|
|
38
|
-
"@ggui-ai/live-channel": "0.
|
|
39
|
-
"@ggui-ai/
|
|
40
|
-
"@ggui-ai/gadgets": "0.
|
|
41
|
-
"@ggui-ai/
|
|
42
|
-
"@ggui-ai/
|
|
43
|
-
"@ggui-ai/wire": "0.
|
|
38
|
+
"@ggui-ai/live-channel": "0.6.0",
|
|
39
|
+
"@ggui-ai/preview-a2ui": "0.6.0",
|
|
40
|
+
"@ggui-ai/gadgets": "0.6.0",
|
|
41
|
+
"@ggui-ai/protocol": "0.6.0",
|
|
42
|
+
"@ggui-ai/design": "0.6.0",
|
|
43
|
+
"@ggui-ai/wire": "0.6.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@modelcontextprotocol/sdk": "^1.27.0"
|