@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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisional (A2UI preview) renderer for the iframe runtime.
|
|
3
|
+
*
|
|
4
|
+
* Port of `@ggui-ai/react::components/ProvisionalRenderer.tsx` with
|
|
5
|
+
* the React-hook consumer stripped. The A2UI reduce + adjacency-list
|
|
6
|
+
* render is preserved verbatim; the inputs + mount lifecycle differ:
|
|
7
|
+
*
|
|
8
|
+
* - Host-SDK version reads via `useChannelStream(PREVIEW_CHANNEL)`
|
|
9
|
+
* (a React hook subscribed to a live-channel bridge event).
|
|
10
|
+
* - Iframe-renderer version is driven by an explicit
|
|
11
|
+
* `pushEnvelope(env)` entry point invoked by `subscribe.ts` on
|
|
12
|
+
* every validated inbound `data` envelope whose channel is
|
|
13
|
+
* `_ggui:preview`. The caller (runtime.ts) forwards envelopes
|
|
14
|
+
* after the reserved-channel A2UI validator has accepted them.
|
|
15
|
+
*
|
|
16
|
+
* Visual language: `PreviewSurface` / `PreviewFragmentEnter` /
|
|
17
|
+
* `StreamingText` from `@ggui-ai/design/preview`; primitive mapping
|
|
18
|
+
* from `@ggui-ai/design/primitives`. Matches the host-SDK output
|
|
19
|
+
* 1:1 — operator-visible preview DOM is identical on both paths.
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle:
|
|
22
|
+
*
|
|
23
|
+
* - `mountProvisional(container)` → `{pushEnvelope, suspend,
|
|
24
|
+
* resume, unmount}` handle. The container renders the default
|
|
25
|
+
* loading fallback until A2UI's `root` fragment arrives; then
|
|
26
|
+
* the adjacency tree paints.
|
|
27
|
+
* - `suspend()` hides the surface (used when the authoritative
|
|
28
|
+
* render takes over — the stack-item dispatcher in Commit 4
|
|
29
|
+
* toggles this on ReactComponentRenderer mount).
|
|
30
|
+
* - `resume()` re-shows. Envelopes keep accumulating in state
|
|
31
|
+
* while suspended, matching host-SDK behavior.
|
|
32
|
+
*/
|
|
33
|
+
import React, { createElement, Fragment, useEffect, useState } from 'react';
|
|
34
|
+
import { createRoot } from 'react-dom/client';
|
|
35
|
+
/**
|
|
36
|
+
* Spec-canonical A2UI preview channel name (from
|
|
37
|
+
* `@ggui-ai/protocol::PREVIEW_CHANNEL`). Inlined here so this
|
|
38
|
+
* module's runtime imports stay off the protocol root barrel —
|
|
39
|
+
* importing the constant from `@ggui-ai/protocol` pulls in
|
|
40
|
+
* `openrouter-models` + the entire zod validation graph via the
|
|
41
|
+
* root re-export chain, adding ~250 KB gz to the renderer bundle.
|
|
42
|
+
* The value is a stable string literal (`isKnownReservedChannel`
|
|
43
|
+
* and the spec both reference `_ggui:preview` directly); re-declaring
|
|
44
|
+
* it here is shape-preserving and covered by the structural-lock
|
|
45
|
+
* test in the source module.
|
|
46
|
+
*/
|
|
47
|
+
const PREVIEW_CHANNEL = '_ggui:preview';
|
|
48
|
+
import { parseServerMessage, } from '@ggui-ai/preview-a2ui';
|
|
49
|
+
import { Button, Card, Checkbox, Divider, Heading, Icon, Image, Input, Row, Select, Spinner, Stack, Text, } from '@ggui-ai/design/primitives';
|
|
50
|
+
import { PreviewFragmentEnter, PreviewSurface, StreamingText, } from '@ggui-ai/design/preview';
|
|
51
|
+
const A2UI_ROOT_ID = 'root';
|
|
52
|
+
const EMPTY_STATE = {
|
|
53
|
+
surfaceId: null,
|
|
54
|
+
fragments: new Map(),
|
|
55
|
+
};
|
|
56
|
+
function reduce(state, msg) {
|
|
57
|
+
if ('createSurface' in msg) {
|
|
58
|
+
return {
|
|
59
|
+
surfaceId: msg.createSurface.surfaceId,
|
|
60
|
+
fragments: new Map(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if ('updateComponents' in msg) {
|
|
64
|
+
if (state.surfaceId !== msg.updateComponents.surfaceId)
|
|
65
|
+
return state;
|
|
66
|
+
const next = new Map(state.fragments);
|
|
67
|
+
for (const component of msg.updateComponents.components) {
|
|
68
|
+
next.set(component.id, component);
|
|
69
|
+
}
|
|
70
|
+
return { ...state, fragments: next };
|
|
71
|
+
}
|
|
72
|
+
if ('deleteSurface' in msg) {
|
|
73
|
+
if (state.surfaceId !== msg.deleteSurface.surfaceId)
|
|
74
|
+
return state;
|
|
75
|
+
return EMPTY_STATE;
|
|
76
|
+
}
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
function reduceAll(envelopes) {
|
|
80
|
+
let state = EMPTY_STATE;
|
|
81
|
+
for (const envelope of envelopes) {
|
|
82
|
+
const parsed = parseServerMessage(envelope.payload);
|
|
83
|
+
if (!parsed.ok)
|
|
84
|
+
continue;
|
|
85
|
+
state = reduce(state, parsed.value);
|
|
86
|
+
}
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// A2UI component → primitives renderer — ports renderComponent verbatim.
|
|
91
|
+
// =============================================================================
|
|
92
|
+
function UnresolvedPlaceholder() {
|
|
93
|
+
return createElement('div', {
|
|
94
|
+
style: {
|
|
95
|
+
minHeight: 24,
|
|
96
|
+
minWidth: 32,
|
|
97
|
+
borderRadius: 'var(--ggui-shape-radius-sm, 4px)',
|
|
98
|
+
backgroundColor: 'var(--ggui-color-outlineVariant, rgba(148, 163, 184, 0.18))',
|
|
99
|
+
},
|
|
100
|
+
'aria-hidden': 'true',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function UnsupportedComponentShell({ name }) {
|
|
104
|
+
return createElement(Card, { padding: 12, shadow: 'none', border: true }, createElement(Text, { variant: 'caption' }, `[${name}]`));
|
|
105
|
+
}
|
|
106
|
+
// Stable noop so React doesn't warn about switching onChange identity.
|
|
107
|
+
function noopOnChange() {
|
|
108
|
+
// intentionally empty — provisional controls are non-interactive
|
|
109
|
+
}
|
|
110
|
+
function renderComponent(component, all) {
|
|
111
|
+
const resolveChildren = (childIds) => {
|
|
112
|
+
if (!childIds)
|
|
113
|
+
return [];
|
|
114
|
+
return childIds.map((id) => {
|
|
115
|
+
const child = all.get(id);
|
|
116
|
+
if (!child)
|
|
117
|
+
return createElement(UnresolvedPlaceholder, { key: id });
|
|
118
|
+
return renderComponent(child, all);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const { id } = component;
|
|
122
|
+
const wrap = (children) => createElement(PreviewFragmentEnter, { key: id, children });
|
|
123
|
+
switch (component.component) {
|
|
124
|
+
case 'Row':
|
|
125
|
+
return wrap(createElement(Row, {
|
|
126
|
+
gap: component.gap,
|
|
127
|
+
align: component.align ?? 'stretch',
|
|
128
|
+
justify: component.justify ?? 'start',
|
|
129
|
+
}, ...resolveChildren(component.children)));
|
|
130
|
+
case 'Column':
|
|
131
|
+
return wrap(createElement(Stack, {
|
|
132
|
+
direction: 'vertical',
|
|
133
|
+
gap: component.gap,
|
|
134
|
+
align: component.align ?? 'stretch',
|
|
135
|
+
justify: component.justify ?? 'start',
|
|
136
|
+
}, ...resolveChildren(component.children)));
|
|
137
|
+
case 'List':
|
|
138
|
+
return wrap(createElement(Stack, { direction: 'vertical', gap: 8 }, ...resolveChildren(component.children)));
|
|
139
|
+
case 'Card': {
|
|
140
|
+
const child = component.child ? all.get(component.child) : undefined;
|
|
141
|
+
const cardContent = component.child && !child
|
|
142
|
+
? createElement(UnresolvedPlaceholder)
|
|
143
|
+
: child
|
|
144
|
+
? renderComponent(child, all)
|
|
145
|
+
: null;
|
|
146
|
+
return wrap(createElement(Card, { padding: 16, shadow: 'sm' }, cardContent));
|
|
147
|
+
}
|
|
148
|
+
case 'Divider':
|
|
149
|
+
return wrap(createElement(Divider, { orientation: component.orientation ?? 'horizontal' }));
|
|
150
|
+
case 'Text': {
|
|
151
|
+
const variant = component.variant ?? 'body';
|
|
152
|
+
const headingMatch = /^h([1-6])$/.exec(variant);
|
|
153
|
+
if (headingMatch) {
|
|
154
|
+
const level = Number(headingMatch[1]);
|
|
155
|
+
return wrap(createElement(Heading, { level }, createElement(StreamingText, null, component.text)));
|
|
156
|
+
}
|
|
157
|
+
const textVariant = variant === 'label' || variant === 'caption' ? variant : 'body';
|
|
158
|
+
return wrap(createElement(Text, { variant: textVariant }, createElement(StreamingText, null, component.text)));
|
|
159
|
+
}
|
|
160
|
+
case 'Image':
|
|
161
|
+
return wrap(createElement(Image, { src: component.src, alt: component.alt ?? '' }));
|
|
162
|
+
case 'Icon':
|
|
163
|
+
return wrap(createElement(Icon, { name: component.name }));
|
|
164
|
+
case 'Button':
|
|
165
|
+
return wrap(createElement(Button, { disabled: true }, component.label));
|
|
166
|
+
case 'TextField':
|
|
167
|
+
return wrap(createElement(Input, {
|
|
168
|
+
disabled: true,
|
|
169
|
+
label: component.label,
|
|
170
|
+
placeholder: component.placeholder,
|
|
171
|
+
value: component.value ?? '',
|
|
172
|
+
onChange: noopOnChange,
|
|
173
|
+
}));
|
|
174
|
+
case 'CheckBox':
|
|
175
|
+
return wrap(createElement(Checkbox, {
|
|
176
|
+
disabled: true,
|
|
177
|
+
label: component.label ?? '',
|
|
178
|
+
checked: component.checked ?? false,
|
|
179
|
+
onChange: noopOnChange,
|
|
180
|
+
}));
|
|
181
|
+
case 'ChoicePicker': {
|
|
182
|
+
const options = component.options ?? [];
|
|
183
|
+
return wrap(createElement(Select, {
|
|
184
|
+
disabled: true,
|
|
185
|
+
label: component.label,
|
|
186
|
+
value: component.value ?? '',
|
|
187
|
+
options,
|
|
188
|
+
onChange: noopOnChange,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
default: {
|
|
192
|
+
const unknown = component;
|
|
193
|
+
return wrap(createElement(UnsupportedComponentShell, { name: unknown.component }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Inner React component — drives the adjacency-list render against
|
|
199
|
+
* the accumulated envelope list. Re-render on suspend/resume or on
|
|
200
|
+
* new envelope is governed by the component's own useState setter in
|
|
201
|
+
* `ProvisionalController`.
|
|
202
|
+
*/
|
|
203
|
+
function ProvisionalRoot({ envelopes, suspended, fallback }) {
|
|
204
|
+
const state = reduceAll(envelopes);
|
|
205
|
+
if (suspended)
|
|
206
|
+
return null;
|
|
207
|
+
const root = state.fragments.get(A2UI_ROOT_ID);
|
|
208
|
+
if (!root) {
|
|
209
|
+
return createElement(Fragment, null, fallback ?? createElement(DefaultFallback));
|
|
210
|
+
}
|
|
211
|
+
return createElement(PreviewSurface, null, renderComponent(root, state.fragments));
|
|
212
|
+
}
|
|
213
|
+
function DefaultFallback() {
|
|
214
|
+
return createElement('div', {
|
|
215
|
+
style: {
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
justifyContent: 'center',
|
|
219
|
+
minHeight: 80,
|
|
220
|
+
},
|
|
221
|
+
}, createElement(Spinner));
|
|
222
|
+
}
|
|
223
|
+
function ProvisionalController({ controlRef, onAttach, fallback, }) {
|
|
224
|
+
const [envelopes, setEnvelopes] = useState([]);
|
|
225
|
+
const [suspended, setSuspended] = useState(false);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
controlRef.current = {
|
|
228
|
+
pushEnvelope: (env) => {
|
|
229
|
+
if (env.channel !== PREVIEW_CHANNEL)
|
|
230
|
+
return;
|
|
231
|
+
setEnvelopes((prev) => [...prev, env]);
|
|
232
|
+
},
|
|
233
|
+
suspend: () => setSuspended(true),
|
|
234
|
+
resume: () => setSuspended(false),
|
|
235
|
+
};
|
|
236
|
+
onAttach?.();
|
|
237
|
+
return () => {
|
|
238
|
+
controlRef.current = null;
|
|
239
|
+
};
|
|
240
|
+
}, [controlRef, onAttach]);
|
|
241
|
+
return createElement(ProvisionalRoot, {
|
|
242
|
+
envelopes,
|
|
243
|
+
suspended,
|
|
244
|
+
...(fallback !== undefined ? { fallback } : {}),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Mount the provisional surface into `container`. Returns a handle
|
|
249
|
+
* callers use to push envelopes + suspend/resume + unmount.
|
|
250
|
+
*
|
|
251
|
+
* Synchronous-ish — the React root renders on next microtask, so
|
|
252
|
+
* the `controlRef` is populated by the time the caller's next
|
|
253
|
+
* microtask runs. Tests use `await Promise.resolve()` to sync.
|
|
254
|
+
*/
|
|
255
|
+
export function mountProvisional(container, opts = {}) {
|
|
256
|
+
container.replaceChildren();
|
|
257
|
+
const root = createRoot(container);
|
|
258
|
+
const controlRef = { current: null };
|
|
259
|
+
// Buffer envelopes that arrive before React's first effect runs.
|
|
260
|
+
// Once `controlRef.current` exists we drain the buffer in order;
|
|
261
|
+
// same for suspend/resume calls. Matches ProvisionalRenderer's
|
|
262
|
+
// pre-mount semantics (envelopes are reduced in arrival order
|
|
263
|
+
// regardless of mount timing).
|
|
264
|
+
//
|
|
265
|
+
// Drain trigger: React fires `onAttach` from the controller's
|
|
266
|
+
// `useEffect` once `controlRef.current` is populated. Pre-fix this
|
|
267
|
+
// path scheduled `queueMicrotask(drainPending)` from `pushEnvelope`,
|
|
268
|
+
// which raced React's commit — when the microtask won, the queue
|
|
269
|
+
// was abandoned because `controlRef.current` was still null and the
|
|
270
|
+
// microtask never re-fired. The reserved-channel replay path on the
|
|
271
|
+
// iframe-runtime delivers WS frames synchronously from the WS
|
|
272
|
+
// message handler, so this race fired every time on a freshly-
|
|
273
|
+
// navigated viewer.
|
|
274
|
+
const pending = [];
|
|
275
|
+
let pendingSuspendState = null;
|
|
276
|
+
function drainPending() {
|
|
277
|
+
const ref = controlRef.current;
|
|
278
|
+
if (ref === null)
|
|
279
|
+
return;
|
|
280
|
+
for (const env of pending)
|
|
281
|
+
ref.pushEnvelope(env);
|
|
282
|
+
pending.length = 0;
|
|
283
|
+
if (pendingSuspendState === true)
|
|
284
|
+
ref.suspend();
|
|
285
|
+
if (pendingSuspendState === false)
|
|
286
|
+
ref.resume();
|
|
287
|
+
pendingSuspendState = null;
|
|
288
|
+
}
|
|
289
|
+
root.render(createElement(ProvisionalController, {
|
|
290
|
+
controlRef,
|
|
291
|
+
onAttach: drainPending,
|
|
292
|
+
...(opts.fallback !== undefined ? { fallback: opts.fallback } : {}),
|
|
293
|
+
}));
|
|
294
|
+
return {
|
|
295
|
+
pushEnvelope(envelope) {
|
|
296
|
+
if (controlRef.current === null) {
|
|
297
|
+
pending.push(envelope);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
controlRef.current.pushEnvelope(envelope);
|
|
301
|
+
},
|
|
302
|
+
suspend() {
|
|
303
|
+
if (controlRef.current === null) {
|
|
304
|
+
pendingSuspendState = true;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
controlRef.current.suspend();
|
|
308
|
+
},
|
|
309
|
+
resume() {
|
|
310
|
+
if (controlRef.current === null) {
|
|
311
|
+
pendingSuspendState = false;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
controlRef.current.resume();
|
|
315
|
+
},
|
|
316
|
+
unmount() {
|
|
317
|
+
root.unmount();
|
|
318
|
+
container.replaceChildren();
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Re-export React for internal symmetry with react-renderer.ts.
|
|
323
|
+
export { React };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React component renderer for the iframe runtime.
|
|
3
|
+
*
|
|
4
|
+
* Port of `@ggui-ai/react::components/ReactComponentRenderer.tsx` (L10-384)
|
|
5
|
+
* with the enclosing React component stripped — the iframe runtime
|
|
6
|
+
* does not have an outer React tree; it owns the root `createRoot`
|
|
7
|
+
* mount itself. The inner eval + data-URL-shim + error-boundary
|
|
8
|
+
* pattern is preserved verbatim so today's generated code (which
|
|
9
|
+
* reads `globalThis.__ggui__` via the shim rewrite) keeps working.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle seams:
|
|
12
|
+
*
|
|
13
|
+
* - `mount(container, stackItem)` — replace the container's
|
|
14
|
+
* children with a React root rendering the compiled component.
|
|
15
|
+
* Returns a `ReactRootMount` handle; callers use `update()` to
|
|
16
|
+
* swap props without re-evaluating the module, `unmount()` to
|
|
17
|
+
* tear down.
|
|
18
|
+
* - `update(stackItem)` — swap props (and/or componentCode). When
|
|
19
|
+
* componentCode changes, re-evaluates; when only props change,
|
|
20
|
+
* re-renders the existing component tree with new props.
|
|
21
|
+
* - `unmount()` — `root.unmount()` + detach observability.
|
|
22
|
+
*
|
|
23
|
+
* Error handling — ErrorBoundary with auto-retry (AUTO_RETRY_LIMIT +
|
|
24
|
+
* AUTO_RETRY_DELAY) matches the `ReactComponentRenderer` contract.
|
|
25
|
+
* On terminal error, the callback `onRequestRepair` fires; the
|
|
26
|
+
* stack-item dispatcher wires this to the `ggui:request-repair`
|
|
27
|
+
* live-channel envelope `StackItemRenderer` dispatches.
|
|
28
|
+
*
|
|
29
|
+
* No JSX in this file — the mount-root code uses
|
|
30
|
+
* `React.createElement` + `React.Fragment` directly so the renderer's
|
|
31
|
+
* TypeScript config doesn't need a JSX runtime plumbing.
|
|
32
|
+
*/
|
|
33
|
+
import React, { type ReactNode } from 'react';
|
|
34
|
+
export interface ReactRootMountOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Stack item whose componentCode + props drive the mount. Only
|
|
37
|
+
* `componentCode` + `props` are read here; the caller is
|
|
38
|
+
* responsible for wrapping this with a `<GguiWireProvider>` at the
|
|
39
|
+
* higher-level dispatcher (see `stack-item-renderer.ts` in Commit
|
|
40
|
+
* 4).
|
|
41
|
+
*/
|
|
42
|
+
readonly stackItem: {
|
|
43
|
+
readonly id?: string;
|
|
44
|
+
readonly componentCode: string;
|
|
45
|
+
readonly props?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
readonly themeId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Theme color mode resolved for this mount. Forwards to
|
|
50
|
+
* `getScopedThemeCss` / `getThemeCss` so the dark variant of the
|
|
51
|
+
* registered theme paints when the operator selected it.
|
|
52
|
+
*/
|
|
53
|
+
readonly themeMode?: 'light' | 'dark';
|
|
54
|
+
readonly cssOverrides?: string;
|
|
55
|
+
readonly onError?: (error: Error) => void;
|
|
56
|
+
readonly onRequestRepair?: (error: Error) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Children injected BETWEEN the mount DOM (scope + CSS) and the
|
|
59
|
+
* evaluated component element. The caller's stack-item renderer
|
|
60
|
+
* uses this to wrap the eval'd component in `<GguiWireProvider>`
|
|
61
|
+
* so wire hooks resolve per-item contracts.
|
|
62
|
+
*
|
|
63
|
+
* When `undefined`, the evaluated component renders directly.
|
|
64
|
+
*/
|
|
65
|
+
readonly renderWrapper?: (mountedComponent: ReactNode) => ReactNode;
|
|
66
|
+
/**
|
|
67
|
+
* Bare npm package names of the operator-registered 3rd-party gadget
|
|
68
|
+
* packages reachable in this session (GG.8.2). Forwarded to
|
|
69
|
+
* `rewriteImports` so each direct gadget import resolves to its
|
|
70
|
+
* per-package data-URL shim. Sourced from the bootstrap's `gadgets`
|
|
71
|
+
* channel; `@ggui-ai/gadgets` (STDLIB) is always rewritten and need
|
|
72
|
+
* not appear here.
|
|
73
|
+
*/
|
|
74
|
+
readonly gadgetPackages?: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
export interface ReactRootMount {
|
|
77
|
+
/**
|
|
78
|
+
* Swap props (and/or componentCode). When componentCode changes,
|
|
79
|
+
* the module is re-evaluated. Pure-props changes skip the
|
|
80
|
+
* evaluation and re-render with new props — matches RCR's
|
|
81
|
+
* `useEffect([code])` dependency behavior.
|
|
82
|
+
*/
|
|
83
|
+
update(next: ReactRootMountOptions): Promise<void>;
|
|
84
|
+
/** Tear down the React root + release retained state. */
|
|
85
|
+
unmount(): void;
|
|
86
|
+
/** Current component code (useful for tests + audit). */
|
|
87
|
+
readonly currentCode: string | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mount a React root inside `container` and render `stackItem`'s
|
|
91
|
+
* compiled componentCode. The container is fully owned after this
|
|
92
|
+
* call (existing children are replaced).
|
|
93
|
+
*/
|
|
94
|
+
export declare function mountReactRoot(container: HTMLElement, opts: ReactRootMountOptions): Promise<ReactRootMount>;
|
|
95
|
+
export { React };
|