@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,343 @@
|
|
|
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, { Component, createElement, Fragment, } from 'react';
|
|
34
|
+
import { createRoot } from 'react-dom/client';
|
|
35
|
+
import { stripMarkers, rewriteImports, getScopedThemeCss, getScopedCssTokens, getThemeCss, getCssTokens, } from '@ggui-ai/design/rendering';
|
|
36
|
+
import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Scope class helpers — port of RCR's `makeScopeClass` + counter.
|
|
39
|
+
// =============================================================================
|
|
40
|
+
let scopeCounter = 0;
|
|
41
|
+
/**
|
|
42
|
+
* Generate a CSS-safe scope class. The iframe-internal mount doesn't
|
|
43
|
+
* have access to React's `useId` (we're not inside a React tree at
|
|
44
|
+
* the call site), so the counter is the always-available fallback.
|
|
45
|
+
* `data-` attribute wrapping plus the counter-based class is fine —
|
|
46
|
+
* only one ReactRootMount exists per iframe by construction.
|
|
47
|
+
*/
|
|
48
|
+
function makeScopeClass() {
|
|
49
|
+
scopeCounter += 1;
|
|
50
|
+
return `ggui-rcr-${scopeCounter}`;
|
|
51
|
+
}
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Error boundary — port of RCR's internal ErrorBoundary (L68–203).
|
|
54
|
+
// =============================================================================
|
|
55
|
+
const AUTO_RETRY_LIMIT = 1;
|
|
56
|
+
const AUTO_RETRY_DELAY = 500;
|
|
57
|
+
class RcrErrorBoundary extends Component {
|
|
58
|
+
state = {
|
|
59
|
+
error: null,
|
|
60
|
+
catchCount: 0,
|
|
61
|
+
autoRetrying: false,
|
|
62
|
+
repairRequested: false,
|
|
63
|
+
};
|
|
64
|
+
retryTimer = null;
|
|
65
|
+
static getDerivedStateFromError(error) {
|
|
66
|
+
return { error };
|
|
67
|
+
}
|
|
68
|
+
componentDidCatch(error, _info) {
|
|
69
|
+
const nextCount = this.state.catchCount + 1;
|
|
70
|
+
this.setState({ catchCount: nextCount });
|
|
71
|
+
if (nextCount <= AUTO_RETRY_LIMIT) {
|
|
72
|
+
this.setState({ autoRetrying: true });
|
|
73
|
+
this.retryTimer = setTimeout(() => {
|
|
74
|
+
this.setState({ error: null, autoRetrying: false });
|
|
75
|
+
}, AUTO_RETRY_DELAY);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.props.onError?.(error);
|
|
79
|
+
if (this.props.onRequestRepair && !this.state.repairRequested) {
|
|
80
|
+
this.setState({ repairRequested: true });
|
|
81
|
+
this.props.onRequestRepair(error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
componentWillUnmount() {
|
|
85
|
+
if (this.retryTimer !== null)
|
|
86
|
+
clearTimeout(this.retryTimer);
|
|
87
|
+
}
|
|
88
|
+
render() {
|
|
89
|
+
const { error, autoRetrying, repairRequested, catchCount } = this.state;
|
|
90
|
+
if (error === null)
|
|
91
|
+
return this.props.children;
|
|
92
|
+
// The three fallback UIs match the host-SDK error boundary
|
|
93
|
+
// verbatim (same inline styles so operator-visible DOM is
|
|
94
|
+
// identical between the host-SDK and iframe-renderer paths).
|
|
95
|
+
const FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
96
|
+
if (autoRetrying) {
|
|
97
|
+
return createElement('div', {
|
|
98
|
+
style: {
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
padding: 24,
|
|
103
|
+
minHeight: 80,
|
|
104
|
+
gap: 8,
|
|
105
|
+
fontFamily: FONT,
|
|
106
|
+
fontSize: 13,
|
|
107
|
+
color: 'rgba(255,255,255,0.4)',
|
|
108
|
+
},
|
|
109
|
+
}, createElement('div', {
|
|
110
|
+
style: {
|
|
111
|
+
width: 16,
|
|
112
|
+
height: 16,
|
|
113
|
+
border: '2px solid rgba(255,255,255,0.15)',
|
|
114
|
+
borderTopColor: 'rgba(255,255,255,0.5)',
|
|
115
|
+
borderRadius: '50%',
|
|
116
|
+
animation: 'ggui-err-spin 0.6s linear infinite',
|
|
117
|
+
},
|
|
118
|
+
}), 'Retrying...', createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
|
|
119
|
+
}
|
|
120
|
+
if (repairRequested) {
|
|
121
|
+
return createElement('div', {
|
|
122
|
+
style: {
|
|
123
|
+
display: 'flex',
|
|
124
|
+
flexDirection: 'column',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
padding: '32px 24px',
|
|
128
|
+
minHeight: 120,
|
|
129
|
+
gap: 10,
|
|
130
|
+
textAlign: 'center',
|
|
131
|
+
fontFamily: FONT,
|
|
132
|
+
},
|
|
133
|
+
}, createElement('div', {
|
|
134
|
+
style: {
|
|
135
|
+
width: 16,
|
|
136
|
+
height: 16,
|
|
137
|
+
border: '2px solid rgba(139,92,246,0.2)',
|
|
138
|
+
borderTopColor: 'rgba(139,92,246,0.7)',
|
|
139
|
+
borderRadius: '50%',
|
|
140
|
+
animation: 'ggui-err-spin 0.8s linear infinite',
|
|
141
|
+
},
|
|
142
|
+
}), createElement('div', { style: { fontSize: 13, color: 'rgba(255,255,255,0.5)' } }, 'Repairing component...'), createElement('style', null, '@keyframes ggui-err-spin { to { transform: rotate(360deg); } }'));
|
|
143
|
+
}
|
|
144
|
+
return createElement('div', {
|
|
145
|
+
style: {
|
|
146
|
+
display: 'flex',
|
|
147
|
+
flexDirection: 'column',
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
justifyContent: 'center',
|
|
150
|
+
padding: '32px 24px',
|
|
151
|
+
minHeight: 120,
|
|
152
|
+
gap: 12,
|
|
153
|
+
textAlign: 'center',
|
|
154
|
+
fontFamily: FONT,
|
|
155
|
+
},
|
|
156
|
+
}, createElement('div', {
|
|
157
|
+
style: {
|
|
158
|
+
width: 48,
|
|
159
|
+
height: 48,
|
|
160
|
+
borderRadius: 14,
|
|
161
|
+
background: 'rgba(239, 68, 68, 0.1)',
|
|
162
|
+
display: 'flex',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'center',
|
|
165
|
+
fontSize: 22,
|
|
166
|
+
},
|
|
167
|
+
}, '!'), createElement('div', { style: { fontSize: 14, fontWeight: 600, color: 'rgba(255,255,255,0.8)' } }, 'Something went wrong'), createElement('div', { style: { fontSize: 12, color: 'rgba(255,255,255,0.4)', maxWidth: 280, lineHeight: 1.4 } }, 'This component encountered an error. Try asking for a different view.'), createElement('button', {
|
|
168
|
+
onClick: () => this.setState({ error: null, catchCount: 0, repairRequested: false }),
|
|
169
|
+
style: {
|
|
170
|
+
marginTop: 4,
|
|
171
|
+
padding: '8px 20px',
|
|
172
|
+
borderRadius: 8,
|
|
173
|
+
border: '1px solid rgba(255,255,255,0.15)',
|
|
174
|
+
background: 'rgba(255,255,255,0.08)',
|
|
175
|
+
color: 'rgba(255,255,255,0.7)',
|
|
176
|
+
fontSize: 13,
|
|
177
|
+
fontWeight: 500,
|
|
178
|
+
cursor: 'pointer',
|
|
179
|
+
},
|
|
180
|
+
}, 'Retry'), catchCount > 1
|
|
181
|
+
? createElement('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.2)' } }, error.message)
|
|
182
|
+
: null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Mount a React root inside `container` and render `stackItem`'s
|
|
187
|
+
* compiled componentCode. The container is fully owned after this
|
|
188
|
+
* call (existing children are replaced).
|
|
189
|
+
*/
|
|
190
|
+
export async function mountReactRoot(container, opts) {
|
|
191
|
+
container.replaceChildren();
|
|
192
|
+
const root = createRoot(container);
|
|
193
|
+
const scopeClass = makeScopeClass();
|
|
194
|
+
let currentOpts = opts;
|
|
195
|
+
let currentCode = null;
|
|
196
|
+
let currentComponent = null;
|
|
197
|
+
async function evaluate(code) {
|
|
198
|
+
// 0. If code is a URL (S3 presigned), fetch the bytes first.
|
|
199
|
+
// Matches RCR's first-step URL-fetch guard verbatim.
|
|
200
|
+
let resolved = code;
|
|
201
|
+
if (code.startsWith('https://')) {
|
|
202
|
+
const response = await fetch(code);
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new Error(`Failed to fetch component code: ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
resolved = await response.text();
|
|
207
|
+
}
|
|
208
|
+
// 1. Strip metadata markers (__GGUI_META__ / __GGUI_STREAM_SPEC__).
|
|
209
|
+
const cleaned = stripMarkers(resolved);
|
|
210
|
+
// 2. Hoist imports above var declarations.
|
|
211
|
+
const hoisted = hoistImports(cleaned);
|
|
212
|
+
// 3. Rewrite bare specifiers to data-url shims reading from
|
|
213
|
+
// `globalThis.__ggui__`. The __ggui__ registry is installed
|
|
214
|
+
// by `runtime.ts::bootSequence` BEFORE any mountReactRoot call
|
|
215
|
+
// — see globals.ts docstring on TOCTOU ordering.
|
|
216
|
+
const rewritten = rewriteImports(hoisted, {
|
|
217
|
+
mode: 'data-url',
|
|
218
|
+
gadgetPackages: currentOpts.gadgetPackages,
|
|
219
|
+
});
|
|
220
|
+
// 4. Dynamically import the module.
|
|
221
|
+
const mod = await loadModule(rewritten);
|
|
222
|
+
// 5. Extract default export (or first function export).
|
|
223
|
+
const Comp = (mod.default ??
|
|
224
|
+
Object.values(mod).find((v) => typeof v === 'function'));
|
|
225
|
+
if (Comp === undefined) {
|
|
226
|
+
throw new Error('Module does not export a default component');
|
|
227
|
+
}
|
|
228
|
+
return Comp;
|
|
229
|
+
}
|
|
230
|
+
function renderTree(opts) {
|
|
231
|
+
const themeCss = opts.themeId
|
|
232
|
+
? getScopedThemeCss(opts.themeId, scopeClass, opts.themeMode)
|
|
233
|
+
: getScopedCssTokens(scopeClass, opts.themeMode);
|
|
234
|
+
// Also inject theme CSS at `:root` on `document.head`. The scoped
|
|
235
|
+
// injection above isolates token resolution to the React tree, but
|
|
236
|
+
// the embedding shell's body styles (font-family, color, background)
|
|
237
|
+
// resolve against `:root` — without this, body chrome falls back to
|
|
238
|
+
// the inline-style defaults (-apple-system, etc.) even when the
|
|
239
|
+
// tree itself paints correctly. Idempotent — last mount wins on
|
|
240
|
+
// repeat calls. Same shared id as `@ggui-ai/design/themes`'
|
|
241
|
+
// `ThemeProvider` so a Studio/Portal context with both runtime + RP
|
|
242
|
+
// active doesn't double-stack.
|
|
243
|
+
if (typeof document !== 'undefined') {
|
|
244
|
+
const rootCss = opts.themeId
|
|
245
|
+
? getThemeCss(opts.themeId, opts.themeMode)
|
|
246
|
+
: getCssTokens(opts.themeMode);
|
|
247
|
+
const styleId = 'ggui-theme-vars';
|
|
248
|
+
let styleEl = document.getElementById(styleId);
|
|
249
|
+
if (!styleEl) {
|
|
250
|
+
styleEl = document.createElement('style');
|
|
251
|
+
styleEl.id = styleId;
|
|
252
|
+
document.head.appendChild(styleEl);
|
|
253
|
+
}
|
|
254
|
+
if (styleEl.textContent !== rootCss) {
|
|
255
|
+
styleEl.textContent = rootCss;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const componentElement = currentComponent === null
|
|
259
|
+
? null
|
|
260
|
+
: createElement(currentComponent, opts.stackItem.props ?? {});
|
|
261
|
+
const wrapped = opts.renderWrapper && componentElement !== null
|
|
262
|
+
? opts.renderWrapper(componentElement)
|
|
263
|
+
: componentElement;
|
|
264
|
+
// The scope `<div>` + inline `<style>` + error-boundary-wrapped
|
|
265
|
+
// children match RCR L376–383. `key={currentCode?.length}` mirrors
|
|
266
|
+
// RCR's "reset boundary when code changes" heuristic.
|
|
267
|
+
root.render(createElement('div', { className: scopeClass }, createElement('style', null, `${themeCss}${opts.cssOverrides ?? ''}`), createElement(RcrErrorBoundary, {
|
|
268
|
+
key: currentCode?.length ?? 0,
|
|
269
|
+
...(opts.onError ? { onError: opts.onError } : {}),
|
|
270
|
+
...(opts.onRequestRepair ? { onRequestRepair: opts.onRequestRepair } : {}),
|
|
271
|
+
}, createElement(Fragment, null, wrapped))));
|
|
272
|
+
}
|
|
273
|
+
async function initialEvaluate() {
|
|
274
|
+
const code = currentOpts.stackItem.componentCode ?? '';
|
|
275
|
+
if (code.trim().length === 0) {
|
|
276
|
+
currentComponent = null;
|
|
277
|
+
currentCode = null;
|
|
278
|
+
renderTree(currentOpts);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
currentComponent = await evaluate(code);
|
|
283
|
+
currentCode = code;
|
|
284
|
+
renderTree(currentOpts);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
288
|
+
// Fail-loud on eval errors. The OPTIONAL `onError` callback is
|
|
289
|
+
// not enough on its own — bootSelfContained doesn't wire one,
|
|
290
|
+
// so without this log a self-contained mount with a broken
|
|
291
|
+
// component renders a blank iframe and emits nothing.
|
|
292
|
+
// eslint-disable-next-line no-console -- operator-visible failure signal
|
|
293
|
+
console.error('[ggui] mountReactRoot: component evaluation failed —', e);
|
|
294
|
+
currentComponent = null;
|
|
295
|
+
currentCode = null;
|
|
296
|
+
renderTree(currentOpts);
|
|
297
|
+
currentOpts.onError?.(e);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
await initialEvaluate();
|
|
301
|
+
return {
|
|
302
|
+
get currentCode() {
|
|
303
|
+
return currentCode;
|
|
304
|
+
},
|
|
305
|
+
async update(next) {
|
|
306
|
+
const prevCode = currentCode;
|
|
307
|
+
currentOpts = next;
|
|
308
|
+
const nextCode = next.stackItem.componentCode ?? '';
|
|
309
|
+
// Props-only change — skip module eval. Re-render with new props.
|
|
310
|
+
if (nextCode === prevCode && currentComponent !== null) {
|
|
311
|
+
renderTree(currentOpts);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (nextCode.trim().length === 0) {
|
|
315
|
+
currentComponent = null;
|
|
316
|
+
currentCode = null;
|
|
317
|
+
renderTree(currentOpts);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
currentComponent = await evaluate(nextCode);
|
|
322
|
+
currentCode = nextCode;
|
|
323
|
+
renderTree(currentOpts);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
327
|
+
currentComponent = null;
|
|
328
|
+
currentCode = null;
|
|
329
|
+
renderTree(currentOpts);
|
|
330
|
+
next.onError?.(e);
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
unmount() {
|
|
334
|
+
root.unmount();
|
|
335
|
+
container.replaceChildren();
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// Re-export React so downstream renderer modules can import it via
|
|
340
|
+
// the same module specifier as the mount consumer without a separate
|
|
341
|
+
// `react` import (keeps the dependency graph flat inside the renderer
|
|
342
|
+
// package).
|
|
343
|
+
export { React };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AckPayload, ContractErrorPayload } from '@ggui-ai/protocol';
|
|
2
|
+
import type { ConnectionStatus } from '@ggui-ai/protocol/transport/websocket';
|
|
3
|
+
import type { GguiBootstrapMeta } from '@ggui-ai/protocol/integrations/mcp-apps';
|
|
4
|
+
import type { ChannelRegistry, WsTransportHandle } from '@ggui-ai/channel-client';
|
|
5
|
+
import { type ProtocolErrorEmitter } from './protocol-error.js';
|
|
6
|
+
import type { ObservabilityEmitter } from './observability.js';
|
|
7
|
+
/**
|
|
8
|
+
* Result of a successful handshake — the live typed transport handle
|
|
9
|
+
* plus the resolved `AckPayload` from the first ack frame. The caller
|
|
10
|
+
* attaches `handle` to the triad's outbound send surface (via the
|
|
11
|
+
* BufferedManagerShim flush) and applies `ack.stack` to the stack
|
|
12
|
+
* model.
|
|
13
|
+
*/
|
|
14
|
+
export interface RegistrySubscribeHandle {
|
|
15
|
+
readonly handle: WsTransportHandle;
|
|
16
|
+
readonly ack: AckPayload;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Caller-provided callbacks. The runtime's `bootSequence` owns the
|
|
20
|
+
* concrete implementations; tests pass mocks via the `connectFn` seam.
|
|
21
|
+
*/
|
|
22
|
+
export interface ConnectViaRegistryOptions {
|
|
23
|
+
readonly bootstrap: GguiBootstrapMeta;
|
|
24
|
+
readonly registry: ChannelRegistry;
|
|
25
|
+
/**
|
|
26
|
+
* Fires on every transport-status transition mapped onto the
|
|
27
|
+
* protocol-level `ConnectionStatus` axis. The mapping is:
|
|
28
|
+
*
|
|
29
|
+
* - First `'connecting'` after construction → `'connecting'`.
|
|
30
|
+
* - First `'open'` → `'connected'`.
|
|
31
|
+
* - `'closed'` after a prior `'open'` (drop) → `'disconnected'`.
|
|
32
|
+
* - `'connecting'` after a prior `'open'` (reconnect attempt) →
|
|
33
|
+
* `'reconnecting'`.
|
|
34
|
+
* - `'failed'` → `'disconnected'` (terminal — the reconnect ladder
|
|
35
|
+
* gave up). Also rejects the handshake promise pre-ack.
|
|
36
|
+
*
|
|
37
|
+
* The mapping is load-bearing for the renderer's status DOM logic
|
|
38
|
+
* (which already keyed off `ConnectionStatus`) and the
|
|
39
|
+
* `subscribe-failed` observability emission rule (fires on
|
|
40
|
+
* `'reconnecting'` only).
|
|
41
|
+
*/
|
|
42
|
+
readonly onStatusChange: (status: ConnectionStatus) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Optional typed {@link ProtocolError} sink. Pre-ack `error`
|
|
45
|
+
* frames are classified through this emitter in parallel with the
|
|
46
|
+
* throw/reject path.
|
|
47
|
+
*/
|
|
48
|
+
readonly onProtocolError?: ProtocolErrorEmitter;
|
|
49
|
+
/**
|
|
50
|
+
* Optional {@link ObservabilityEmitter} sink. Two events flow:
|
|
51
|
+
*
|
|
52
|
+
* - `schema-version-mismatch` — on every UPGRADE_REQUIRED
|
|
53
|
+
* classification (client-side ack-mismatch + server-emitted
|
|
54
|
+
* error frame).
|
|
55
|
+
* - `subscribe-failed` — on non-terminal transport status
|
|
56
|
+
* transitions to `'reconnecting'`. Terminal failures route
|
|
57
|
+
* through the ProtocolError path only.
|
|
58
|
+
*/
|
|
59
|
+
readonly onObserve?: ObservabilityEmitter;
|
|
60
|
+
/**
|
|
61
|
+
* Optional post-handshake ack callback. Fires for every ack frame
|
|
62
|
+
* AFTER the initial handshake settled — i.e. on every reconnect's
|
|
63
|
+
* subscribe ack. The runtime uses this to reapply the server's
|
|
64
|
+
* authoritative `stack` snapshot so a push or update that landed
|
|
65
|
+
* during a WS dropout window flows back into the iframe on
|
|
66
|
+
* reconnect.
|
|
67
|
+
*
|
|
68
|
+
* Idempotent at the runtime layer: `stackModel.setAll(ack.stack)`
|
|
69
|
+
* is a snapshot replace. Channels re-subscribed by
|
|
70
|
+
* `ChannelTransportRouter.onWsStatusChange('connected')` upstream
|
|
71
|
+
* of this callback, so the per-channel transport state is already
|
|
72
|
+
* coherent by the time the resubscribe-ack arrives.
|
|
73
|
+
*/
|
|
74
|
+
readonly onResubscribeAck?: (ack: AckPayload) => void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Open the WS via the registry, send subscribe (composed by the
|
|
78
|
+
* registry's `subscribeFrameBuilder`), await the first `ack`. Resolves
|
|
79
|
+
* with `{ handle, ack }` on success; rejects with
|
|
80
|
+
* `UpgradeRequiredError` on a version-handshake failure (either side);
|
|
81
|
+
* rejects with a plain `Error` on any other server-emitted pre-ack
|
|
82
|
+
* error.
|
|
83
|
+
*
|
|
84
|
+
* Once the promise settles, subsequent server frames flow through the
|
|
85
|
+
* registered channel handlers (props_update, drain_ack, push, data,
|
|
86
|
+
* etc.) — there is no longer a separate `onMessage` callback fanning
|
|
87
|
+
* frames across multiple handler chains.
|
|
88
|
+
*/
|
|
89
|
+
export declare function connectViaRegistry(opts: ConnectViaRegistryOptions): Promise<RegistrySubscribeHandle>;
|
|
90
|
+
/**
|
|
91
|
+
* Default `connectFn` seam — tests can swap this for a fake to drive
|
|
92
|
+
* the handshake without a real WebSocket. The renderer's `bootSequence`
|
|
93
|
+
* accepts an optional override via the `BootSequenceOptions.connectFn`
|
|
94
|
+
* field; the default below points back at this module's
|
|
95
|
+
* `connectViaRegistry` export.
|
|
96
|
+
*/
|
|
97
|
+
export type ConnectFn = typeof connectViaRegistry;
|
|
98
|
+
/**
|
|
99
|
+
* Type re-export for callers that want to thread the handle type
|
|
100
|
+
* through their own seams (e.g. `bootSequence`'s shim attach).
|
|
101
|
+
*/
|
|
102
|
+
export type { WsTransportHandle, ContractErrorPayload };
|