@ggui-ai/iframe-runtime 0.1.0-rc.1 → 0.2.0-alpha.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/README.md +7 -6
- package/dist/channel-transport.d.ts +15 -15
- package/dist/channel-transport.js +32 -35
- package/dist/channels/channel-payload.d.ts +2 -2
- package/dist/channels/channel-payload.js +1 -1
- package/dist/channels/data.d.ts +14 -19
- package/dist/channels/data.js +13 -31
- package/dist/channels/drain-ack.d.ts +2 -2
- package/dist/channels/drain-ack.js +1 -1
- package/dist/channels/feedback.d.ts +1 -1
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/index.js +1 -1
- package/dist/channels/props-update.d.ts +33 -37
- package/dist/channels/props-update.js +30 -106
- package/dist/channels/render-frame.d.ts +52 -0
- package/dist/channels/render-frame.js +53 -0
- package/dist/channels/system.d.ts +1 -1
- package/dist/compiled-validators.d.ts +15 -0
- package/dist/compiled-validators.js +31 -0
- package/dist/context-observer.d.ts +68 -8
- package/dist/context-observer.js +25 -45
- package/dist/events-polling.d.ts +95 -0
- package/dist/events-polling.js +143 -0
- package/dist/gadget-loader.d.ts +1 -1
- package/dist/globals.d.ts +4 -4
- package/dist/host-context-emitter.d.ts +84 -20
- package/dist/host-context-emitter.js +159 -29
- package/dist/iframe-runtime.js +98 -59
- package/dist/index.d.ts +18 -13
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +5 -5
- package/dist/mcp-app-iframe-host.d.ts +9 -9
- package/dist/mcp-app-iframe-host.js +16 -17
- package/dist/meta-parse.d.ts +44 -0
- package/dist/meta-parse.js +390 -0
- package/dist/observability.d.ts +6 -6
- package/dist/protocol-error.d.ts +7 -7
- package/dist/provisional-renderer.d.ts +1 -1
- package/dist/provisional-renderer.js +1 -1
- package/dist/react-renderer.d.ts +10 -11
- package/dist/react-renderer.js +8 -8
- package/dist/registry-subscribe.d.ts +27 -9
- package/dist/registry-subscribe.js +32 -27
- package/dist/render-item.d.ts +83 -0
- package/dist/{stack-item-renderer.js → render-item.js} +106 -106
- package/dist/runtime.d.ts +183 -131
- package/dist/runtime.js +1264 -1330
- package/dist/status-dom.d.ts +52 -47
- package/dist/status-dom.js +51 -34
- package/dist/system-cards/NoCredentialsCard.d.ts +1 -1
- package/dist/system-cards/NoCredentialsCard.js +1 -1
- package/dist/system-cards/ProtocolProbeCard.js +2 -2
- package/dist/system-cards/index.d.ts +4 -4
- package/dist/system-cards/index.js +4 -4
- package/dist/types.d.ts +70 -34
- package/dist/types.js +1 -1
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +2 -2
- package/dist/wire-config.d.ts +54 -82
- package/dist/wire-config.js +61 -93
- package/package.json +9 -8
- package/dist/bootstrap.d.ts +0 -72
- package/dist/bootstrap.js +0 -500
- package/dist/canvas/animator/animator-host.d.ts +0 -63
- package/dist/canvas/animator/animator-host.js +0 -105
- package/dist/canvas/animator/animator-pill.d.ts +0 -42
- package/dist/canvas/animator/animator-pill.js +0 -119
- package/dist/canvas/animator/state-machine.d.ts +0 -106
- package/dist/canvas/animator/state-machine.js +0 -150
- package/dist/canvas/canvas-shell.d.ts +0 -77
- package/dist/canvas/canvas-shell.js +0 -117
- package/dist/canvas/display-mode-policy.d.ts +0 -104
- package/dist/canvas/display-mode-policy.js +0 -98
- package/dist/canvas/mount.d.ts +0 -150
- package/dist/canvas/mount.js +0 -202
- package/dist/canvas/nav-stack.d.ts +0 -133
- package/dist/canvas/nav-stack.js +0 -172
- package/dist/channels/push.d.ts +0 -47
- package/dist/channels/push.js +0 -54
- package/dist/stack-item-renderer.d.ts +0 -103
- package/dist/stack.d.ts +0 -90
- package/dist/stack.js +0 -118
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The iframe-local runtime for [ggui](https://ggui.ai).
|
|
|
4
4
|
|
|
5
5
|
A self-contained ESM bundle that boots inside an MCP Apps host iframe,
|
|
6
6
|
runs the protocol-version handshake, opens the live-channel WebSocket,
|
|
7
|
-
and
|
|
7
|
+
and mounts the rendered UI. It bundles React, ReactDOM, and the
|
|
8
8
|
ggui design system inside the artifact so generated component code has a
|
|
9
9
|
single seam to read from.
|
|
10
10
|
|
|
@@ -16,17 +16,18 @@ exists for host apps, the `<McpAppIframe>` wrapper, and tests.
|
|
|
16
16
|
|
|
17
17
|
For host-side code that needs to validate or react to runtime state:
|
|
18
18
|
|
|
19
|
-
- `
|
|
20
|
-
parse and validate a
|
|
19
|
+
- `parseMetaFromGlobal` / `parseMetaFromToolResult` —
|
|
20
|
+
parse and validate a render bootstrap before spawning the iframe.
|
|
21
21
|
- `ProtocolError` and the `from*` constructors — the canonical typed
|
|
22
22
|
union for every failure the renderer classifies outward.
|
|
23
23
|
- `ObservabilityEvent` / `postObservabilityToParent` — the renderer →
|
|
24
24
|
host telemetry seam.
|
|
25
25
|
- `LifecycleEmitter` / `postLifecycleToParent` — lifecycle events.
|
|
26
26
|
|
|
27
|
-
Internal runtime types (`
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
Internal runtime types (`bootSequence`, `connectViaRegistry`, the
|
|
28
|
+
per-channel handler factories, and the single-item render mount) are
|
|
29
|
+
deliberately not exported — they are an internal implementation
|
|
30
|
+
contract, not a public API.
|
|
30
31
|
|
|
31
32
|
## License
|
|
32
33
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* for any tool the agent has access to, including third-party
|
|
20
20
|
* MCP servers the local pod can't subscribe-for.
|
|
21
21
|
*
|
|
22
|
-
* The dispatch decision is per-channel + per-render (each new
|
|
22
|
+
* The dispatch decision is per-channel + per-render (each new render
|
|
23
23
|
* brings a potentially-new contract; we re-run the routing). The
|
|
24
24
|
* StreamBus + `useStream(channel)` API stay unchanged — the router
|
|
25
25
|
* adapts payloads from both transports into the same envelope shape
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* on the {@link RendererWebSocketManager}. The router only watches
|
|
37
37
|
* status transitions; it doesn't own the reconnect schedule.
|
|
38
38
|
*
|
|
39
|
-
* **Idempotence** — re-subscribing the same `(
|
|
39
|
+
* **Idempotence** — re-subscribing the same `(renderId, channelName)`
|
|
40
40
|
* pair is server-side idempotent (the server's `ChannelSubscriptionState`
|
|
41
41
|
* replaces in place). Clients can re-send on reconnect without bookkeeping
|
|
42
42
|
* a "did we already subscribe?" gate.
|
|
@@ -83,9 +83,9 @@ export type ToolsCallInvoker = (args: {
|
|
|
83
83
|
* Options for {@link createChannelTransportRouter}.
|
|
84
84
|
*/
|
|
85
85
|
export interface ChannelTransportRouterOptions {
|
|
86
|
-
/**
|
|
87
|
-
readonly
|
|
88
|
-
/** App (tenant) id paired with
|
|
86
|
+
/** Render id the router scopes channel_subscribe frames against. */
|
|
87
|
+
readonly renderId: string;
|
|
88
|
+
/** App (tenant) id paired with renderId on the subscribe frame. */
|
|
89
89
|
readonly appId: string;
|
|
90
90
|
/**
|
|
91
91
|
* Allowlist of `source.tool` names this server can subscribe-for
|
|
@@ -121,17 +121,17 @@ export interface ChannelTransportRouterOptions {
|
|
|
121
121
|
*/
|
|
122
122
|
export type ChannelTransportEvent = {
|
|
123
123
|
readonly kind: 'channel-transport-picked';
|
|
124
|
-
readonly
|
|
124
|
+
readonly renderId: string;
|
|
125
125
|
readonly channelName: string;
|
|
126
126
|
readonly transport: 'ws' | 'poll';
|
|
127
127
|
} | {
|
|
128
128
|
readonly kind: 'channel-transport-fallback';
|
|
129
|
-
readonly
|
|
129
|
+
readonly renderId: string;
|
|
130
130
|
readonly channelName: string;
|
|
131
131
|
readonly reason: 'ws-disconnect' | 'channel-not-local';
|
|
132
132
|
} | {
|
|
133
133
|
readonly kind: 'channel-transport-resubscribed';
|
|
134
|
-
readonly
|
|
134
|
+
readonly renderId: string;
|
|
135
135
|
readonly channelName: string;
|
|
136
136
|
};
|
|
137
137
|
/**
|
|
@@ -139,16 +139,16 @@ export type ChannelTransportEvent = {
|
|
|
139
139
|
*/
|
|
140
140
|
export interface ChannelTransportRouter {
|
|
141
141
|
/**
|
|
142
|
-
* Apply a new
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* Apply a new render's `streamSpec`. Idempotent against re-applying
|
|
143
|
+
* the same shape (no churn). Channels added/removed across renders
|
|
144
|
+
* fire transport pick / teardown accordingly.
|
|
145
145
|
*
|
|
146
146
|
* The legacy `data` frame path on `streamSpec[ch]` entries
|
|
147
147
|
* WITHOUT `source.tool` is unaffected — the router only manages
|
|
148
148
|
* the source-fed subset.
|
|
149
149
|
*/
|
|
150
|
-
readonly
|
|
151
|
-
readonly
|
|
150
|
+
readonly applyRender: (render: {
|
|
151
|
+
readonly renderId: string;
|
|
152
152
|
readonly streamSpec?: StreamSpec;
|
|
153
153
|
}) => void;
|
|
154
154
|
/**
|
|
@@ -168,14 +168,14 @@ export interface ChannelTransportRouter {
|
|
|
168
168
|
readonly onWsStatusChange: (status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting') => void;
|
|
169
169
|
/**
|
|
170
170
|
* Tear down every active subscription + timer. Called on
|
|
171
|
-
* `
|
|
171
|
+
* `renderer.teardown`.
|
|
172
172
|
*/
|
|
173
173
|
readonly dispose: () => void;
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
176
|
* Factory.
|
|
177
177
|
*
|
|
178
|
-
* Per-channel transport routing — owns the `(
|
|
178
|
+
* Per-channel transport routing — owns the `(renderId, channelName)`
|
|
179
179
|
* registry, the WS-vs-poll decision per channel, and the cross-state
|
|
180
180
|
* transitions (disconnect → poll fallback, reconnect → re-subscribe).
|
|
181
181
|
*/
|
|
@@ -10,13 +10,13 @@ export const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10_000;
|
|
|
10
10
|
/**
|
|
11
11
|
* Factory.
|
|
12
12
|
*
|
|
13
|
-
* Per-channel transport routing — owns the `(
|
|
13
|
+
* Per-channel transport routing — owns the `(renderId, channelName)`
|
|
14
14
|
* registry, the WS-vs-poll decision per channel, and the cross-state
|
|
15
15
|
* transitions (disconnect → poll fallback, reconnect → re-subscribe).
|
|
16
16
|
*/
|
|
17
17
|
export function createChannelTransportRouter(opts) {
|
|
18
18
|
/**
|
|
19
|
-
* Registry keyed by `${
|
|
19
|
+
* Registry keyed by `${renderId}:${channelName}`. The composite
|
|
20
20
|
* key matches the server-side `channelSubs` map shape exactly, so
|
|
21
21
|
* test snapshots line up.
|
|
22
22
|
*/
|
|
@@ -27,15 +27,15 @@ export function createChannelTransportRouter(opts) {
|
|
|
27
27
|
* WS lifecycle flag. We start in `'connected'` because the router
|
|
28
28
|
* is created AFTER the subscribe ack — the runtime's bootSequence
|
|
29
29
|
* resolves `subscribeFn(...)` before threading the manager into
|
|
30
|
-
*
|
|
30
|
+
* renderer's `attachManager` (where the router is constructed).
|
|
31
31
|
* Status transitions update this; the router uses it to decide
|
|
32
32
|
* whether to start WS subscribes or skip straight to polling.
|
|
33
33
|
*/
|
|
34
34
|
let wsStatus = 'connected';
|
|
35
35
|
let disposed = false;
|
|
36
36
|
const observe = opts.onObserve ?? (() => { });
|
|
37
|
-
function keyOf(
|
|
38
|
-
return `${
|
|
37
|
+
function keyOf(renderId, channelName) {
|
|
38
|
+
return `${renderId}:${channelName}`;
|
|
39
39
|
}
|
|
40
40
|
function stopPolling(state) {
|
|
41
41
|
if (state.pollTimer !== null) {
|
|
@@ -45,7 +45,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
45
45
|
}
|
|
46
46
|
function emitToBus(state, payload, complete) {
|
|
47
47
|
const envelope = {
|
|
48
|
-
|
|
48
|
+
renderId: state.renderId,
|
|
49
49
|
channel: state.channelName,
|
|
50
50
|
mode: state.mode,
|
|
51
51
|
payload,
|
|
@@ -90,16 +90,15 @@ export function createChannelTransportRouter(opts) {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Send a `channel_subscribe` WS frame. Server-side bookkeeping is
|
|
93
|
-
* idempotent on `(
|
|
94
|
-
*
|
|
93
|
+
* idempotent on `(renderId, channelName)`, so re-sends on reconnect
|
|
94
|
+
* are safe.
|
|
95
95
|
*/
|
|
96
96
|
function sendSubscribe(state) {
|
|
97
97
|
opts.send({
|
|
98
98
|
type: 'channel_subscribe',
|
|
99
99
|
payload: {
|
|
100
|
-
|
|
100
|
+
renderId: state.renderId,
|
|
101
101
|
appId: opts.appId,
|
|
102
|
-
stackItemId: state.stackItemId,
|
|
103
102
|
channelName: state.channelName,
|
|
104
103
|
...(state.args !== undefined ? { args: { ...state.args } } : {}),
|
|
105
104
|
},
|
|
@@ -115,9 +114,8 @@ export function createChannelTransportRouter(opts) {
|
|
|
115
114
|
opts.send({
|
|
116
115
|
type: 'channel_unsubscribe',
|
|
117
116
|
payload: {
|
|
118
|
-
|
|
117
|
+
renderId: state.renderId,
|
|
119
118
|
appId: opts.appId,
|
|
120
|
-
stackItemId: state.stackItemId,
|
|
121
119
|
channelName: state.channelName,
|
|
122
120
|
},
|
|
123
121
|
});
|
|
@@ -139,7 +137,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
139
137
|
sendSubscribe(state);
|
|
140
138
|
observe({
|
|
141
139
|
kind: 'channel-transport-picked',
|
|
142
|
-
|
|
140
|
+
renderId: state.renderId,
|
|
143
141
|
channelName: state.channelName,
|
|
144
142
|
transport: 'ws',
|
|
145
143
|
});
|
|
@@ -150,7 +148,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
150
148
|
startPolling(state);
|
|
151
149
|
observe({
|
|
152
150
|
kind: 'channel-transport-picked',
|
|
153
|
-
|
|
151
|
+
renderId: state.renderId,
|
|
154
152
|
channelName: state.channelName,
|
|
155
153
|
transport: 'poll',
|
|
156
154
|
});
|
|
@@ -159,17 +157,17 @@ export function createChannelTransportRouter(opts) {
|
|
|
159
157
|
startPolling(state);
|
|
160
158
|
observe({
|
|
161
159
|
kind: 'channel-transport-picked',
|
|
162
|
-
|
|
160
|
+
renderId: state.renderId,
|
|
163
161
|
channelName: state.channelName,
|
|
164
162
|
transport: 'poll',
|
|
165
163
|
});
|
|
166
164
|
}
|
|
167
165
|
return {
|
|
168
|
-
|
|
166
|
+
applyRender: (render) => {
|
|
169
167
|
if (disposed)
|
|
170
168
|
return;
|
|
171
169
|
const seenKeys = new Set();
|
|
172
|
-
const spec =
|
|
170
|
+
const spec = render.streamSpec ?? {};
|
|
173
171
|
for (const [channelName, entry] of Object.entries(spec)) {
|
|
174
172
|
const source = entry.source;
|
|
175
173
|
// Channels without a `source.tool` declaration stay on the
|
|
@@ -186,18 +184,18 @@ export function createChannelTransportRouter(opts) {
|
|
|
186
184
|
const channelArgs = source.args !== undefined && source.args !== null
|
|
187
185
|
? source.args
|
|
188
186
|
: undefined;
|
|
189
|
-
const k = keyOf(
|
|
187
|
+
const k = keyOf(render.renderId, channelName);
|
|
190
188
|
seenKeys.add(k);
|
|
191
189
|
const existing = channels.get(k);
|
|
192
190
|
if (existing !== undefined) {
|
|
193
|
-
// Same (
|
|
191
|
+
// Same (renderId, channelName) — leave the transport
|
|
194
192
|
// bookkeeping alone. Spec changes that flip preferWs
|
|
195
|
-
// mid-
|
|
193
|
+
// mid-render are out of 1c scope (would require
|
|
196
194
|
// re-handshake).
|
|
197
195
|
continue;
|
|
198
196
|
}
|
|
199
197
|
const state = {
|
|
200
|
-
|
|
198
|
+
renderId: render.renderId,
|
|
201
199
|
channelName,
|
|
202
200
|
toolName,
|
|
203
201
|
...(channelArgs !== undefined ? { args: channelArgs } : {}),
|
|
@@ -210,13 +208,12 @@ export function createChannelTransportRouter(opts) {
|
|
|
210
208
|
channels.set(k, state);
|
|
211
209
|
activate(state);
|
|
212
210
|
}
|
|
213
|
-
// Tear down any channel for THIS
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// teardown.
|
|
211
|
+
// Tear down any channel for THIS render that's no longer in the
|
|
212
|
+
// spec. Channels for OTHER renders stay — they belong to other
|
|
213
|
+
// mounts that haven't been re-applied yet. Close-render paths
|
|
214
|
+
// call `dispose()` for the wholesale teardown.
|
|
218
215
|
for (const [k, state] of channels) {
|
|
219
|
-
if (state.
|
|
216
|
+
if (state.renderId === render.renderId &&
|
|
220
217
|
!seenKeys.has(k)) {
|
|
221
218
|
stopPolling(state);
|
|
222
219
|
sendUnsubscribe(state);
|
|
@@ -229,7 +226,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
229
226
|
return false;
|
|
230
227
|
if (msg.type === 'channel_payload') {
|
|
231
228
|
const p = msg.payload;
|
|
232
|
-
const k = keyOf(p.
|
|
229
|
+
const k = keyOf(p.renderId, p.channelName);
|
|
233
230
|
const state = channels.get(k);
|
|
234
231
|
if (state === undefined)
|
|
235
232
|
return false;
|
|
@@ -247,9 +244,9 @@ export function createChannelTransportRouter(opts) {
|
|
|
247
244
|
if (msg.type === 'channel_error') {
|
|
248
245
|
const p = msg.payload;
|
|
249
246
|
// Locate by `channelName` only — the error payload doesn't
|
|
250
|
-
// carry `
|
|
247
|
+
// carry `renderId`. We match by channel-name across active
|
|
251
248
|
// items and apply the permanent-fallback flag to the matching
|
|
252
|
-
// entries. Same channel name across
|
|
249
|
+
// entries. Same channel name across renders is rare; the
|
|
253
250
|
// policy is conservative (fall back ALL matches rather than
|
|
254
251
|
// miss one).
|
|
255
252
|
for (const state of channels.values()) {
|
|
@@ -257,12 +254,12 @@ export function createChannelTransportRouter(opts) {
|
|
|
257
254
|
continue;
|
|
258
255
|
if (p.code === 'CHANNEL_NOT_LOCAL' ||
|
|
259
256
|
p.code === 'CHANNEL_UNKNOWN' ||
|
|
260
|
-
p.code === '
|
|
257
|
+
p.code === 'RENDER_NOT_FOUND') {
|
|
261
258
|
state.permanentPollFallback = true;
|
|
262
259
|
startPolling(state);
|
|
263
260
|
observe({
|
|
264
261
|
kind: 'channel-transport-fallback',
|
|
265
|
-
|
|
262
|
+
renderId: state.renderId,
|
|
266
263
|
channelName: state.channelName,
|
|
267
264
|
reason: 'channel-not-local',
|
|
268
265
|
});
|
|
@@ -301,7 +298,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
301
298
|
startPolling(state);
|
|
302
299
|
observe({
|
|
303
300
|
kind: 'channel-transport-fallback',
|
|
304
|
-
|
|
301
|
+
renderId: state.renderId,
|
|
305
302
|
channelName: state.channelName,
|
|
306
303
|
reason: 'ws-disconnect',
|
|
307
304
|
});
|
|
@@ -313,7 +310,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
313
310
|
(prevStatus === 'disconnected' || prevStatus === 'reconnecting')) {
|
|
314
311
|
// Reconnected. Re-send `channel_subscribe` for every
|
|
315
312
|
// WS-preferring channel. Server is idempotent on the (session,
|
|
316
|
-
//
|
|
313
|
+
// render, channel) tuple, so duplicates are safe. We
|
|
317
314
|
// intentionally LEAVE the polling fallback running until the
|
|
318
315
|
// first `channel_payload` lands — bridges the gap where the
|
|
319
316
|
// server's first post-reconnect poll cycle hasn't fired yet.
|
|
@@ -325,7 +322,7 @@ export function createChannelTransportRouter(opts) {
|
|
|
325
322
|
sendSubscribe(state);
|
|
326
323
|
observe({
|
|
327
324
|
kind: 'channel-transport-resubscribed',
|
|
328
|
-
|
|
325
|
+
renderId: state.renderId,
|
|
329
326
|
channelName: state.channelName,
|
|
330
327
|
});
|
|
331
328
|
}
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* passthrough — the router parses the frame contents against its
|
|
8
8
|
* subscription map and dispatches to the matching consumer.
|
|
9
9
|
*
|
|
10
|
-
* Factored out of `
|
|
10
|
+
* Factored out of `handleRendererMessage` as part of the B3a handler
|
|
11
11
|
* extraction.
|
|
12
12
|
*/
|
|
13
|
-
import type { ChannelHandler } from '@ggui-ai/channel
|
|
13
|
+
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
14
14
|
import type { ChannelErrorPayload, ChannelPayloadFrame } from '@ggui-ai/protocol';
|
|
15
15
|
import type { ChannelTransportRouter } from '../channel-transport.js';
|
|
16
16
|
export interface ChannelRouterHandlerDeps {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* passthrough — the router parses the frame contents against its
|
|
8
8
|
* subscription map and dispatches to the matching consumer.
|
|
9
9
|
*
|
|
10
|
-
* Factored out of `
|
|
10
|
+
* Factored out of `handleRendererMessage` as part of the B3a handler
|
|
11
11
|
* extraction.
|
|
12
12
|
*/
|
|
13
13
|
export function createChannelPayloadHandler(deps) {
|
package/dist/channels/data.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
|
-
* against the active
|
|
3
|
+
* against the active render's `streamSpec`, then fans the validated
|
|
4
4
|
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
5
|
* (matching `GguiSession.handleServerMessage`'s surfacing policy —
|
|
6
6
|
* the violation is already logged by the validator).
|
|
@@ -11,18 +11,24 @@
|
|
|
11
11
|
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
12
|
* sole dispatch surface for `data` frames.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Post-stack-removal (2026-05-27): the active render is read through
|
|
15
|
+
* the caller-supplied `getCurrentRender` thunk instead of via a
|
|
16
|
+
* `StackModel.snapshot()` walk — the iframe holds exactly one mounted
|
|
17
|
+
* render, so the lookup is direct.
|
|
17
18
|
*/
|
|
18
|
-
import type { ChannelHandler } from '@ggui-ai/channel
|
|
19
|
-
import { type
|
|
19
|
+
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
20
|
+
import { type Render, type StreamEnvelope } from '@ggui-ai/protocol';
|
|
21
|
+
import type { RenderSeedInput } from '../types.js';
|
|
20
22
|
import type { ObservabilityEmitter } from '../observability.js';
|
|
21
|
-
import type { StackModel } from '../stack.js';
|
|
22
23
|
import { type RendererValidatorContext } from '../validation.js';
|
|
23
24
|
import type { StreamBus } from '../wire-config.js';
|
|
24
25
|
export interface DataHandlerDeps {
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Read the currently-mounted render. Returns `null` when no render
|
|
28
|
+
* has been mounted yet — data frames received pre-mount have no
|
|
29
|
+
* streamSpec to validate against and silently drop.
|
|
30
|
+
*/
|
|
31
|
+
readonly getCurrentRender: () => Render | RenderSeedInput | null;
|
|
26
32
|
readonly streamBus: StreamBus;
|
|
27
33
|
readonly validatorCtx: RendererValidatorContext;
|
|
28
34
|
/**
|
|
@@ -33,16 +39,5 @@ export interface DataHandlerDeps {
|
|
|
33
39
|
* bound).
|
|
34
40
|
*/
|
|
35
41
|
readonly onObserve?: ObservabilityEmitter;
|
|
36
|
-
/**
|
|
37
|
-
* Optional canvas-mode lifecycle sink. When present, every envelope
|
|
38
|
-
* on the reserved `_ggui:lifecycle` channel fires this callback
|
|
39
|
-
* with the parsed payload. Production wires it to mount.tsx's
|
|
40
|
-
* AnimatorEventBus so the animator pill state machine advances.
|
|
41
|
-
* Non-canvas iframes leave it absent and the envelopes drop
|
|
42
|
-
* silently — `validateInboundStreamPayload` rejects lifecycle
|
|
43
|
-
* payloads when no reserved validator is registered for them
|
|
44
|
-
* client-side, so they never reach `streamBus.emit` either.
|
|
45
|
-
*/
|
|
46
|
-
readonly onCanvasLifecycle?: (payload: CanvasLifecyclePayload) => void;
|
|
47
42
|
}
|
|
48
43
|
export declare function createDataHandler(deps: DataHandlerDeps): ChannelHandler<StreamEnvelope>;
|
package/dist/channels/data.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
|
-
* against the active
|
|
3
|
+
* against the active render's `streamSpec`, then fans the validated
|
|
4
4
|
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
5
|
* (matching `GguiSession.handleServerMessage`'s surfacing policy —
|
|
6
6
|
* the violation is already logged by the validator).
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
12
|
* sole dispatch surface for `data` frames.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Post-stack-removal (2026-05-27): the active render is read through
|
|
15
|
+
* the caller-supplied `getCurrentRender` thunk instead of via a
|
|
16
|
+
* `StackModel.snapshot()` walk — the iframe holds exactly one mounted
|
|
17
|
+
* render, so the lookup is direct.
|
|
17
18
|
*/
|
|
18
|
-
import { CONTRACT_ERROR_CHANNEL,
|
|
19
|
+
import { CONTRACT_ERROR_CHANNEL, } from '@ggui-ai/protocol';
|
|
19
20
|
import { validateInboundStreamPayload, } from '../validation.js';
|
|
20
21
|
export function createDataHandler(deps) {
|
|
21
22
|
return {
|
|
@@ -36,32 +37,13 @@ export function createDataHandler(deps) {
|
|
|
36
37
|
deps.onObserve !== undefined) {
|
|
37
38
|
emitContractErrorFromDataFrame(envelope, deps.onObserve);
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// wire shape drift.
|
|
47
|
-
if (envelope.channel === LIFECYCLE_CHANNEL &&
|
|
48
|
-
deps.onCanvasLifecycle !== undefined) {
|
|
49
|
-
const payload = envelope.payload;
|
|
50
|
-
if (payload !== null &&
|
|
51
|
-
typeof payload === 'object' &&
|
|
52
|
-
!Array.isArray(payload) &&
|
|
53
|
-
typeof payload.kind === 'string') {
|
|
54
|
-
deps.onCanvasLifecycle(payload);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Active stack item carries the streamSpec — mirrors
|
|
58
|
-
// `GguiSession.handleServerMessage`. Top of stack only.
|
|
59
|
-
const snapshot = deps.stackModel.snapshot();
|
|
60
|
-
const activeItem = snapshot[snapshot.length - 1];
|
|
61
|
-
const streamSpec = activeItem !== undefined &&
|
|
62
|
-
activeItem.type !== 'mcpApps' &&
|
|
63
|
-
activeItem.type !== 'system'
|
|
64
|
-
? activeItem.streamSpec
|
|
40
|
+
// Active render carries the streamSpec — mirrors
|
|
41
|
+
// `GguiSession.handleServerMessage`.
|
|
42
|
+
const activeRender = deps.getCurrentRender();
|
|
43
|
+
const streamSpec = activeRender !== null &&
|
|
44
|
+
activeRender.type !== 'mcpApps' &&
|
|
45
|
+
activeRender.type !== 'system'
|
|
46
|
+
? activeRender.streamSpec
|
|
65
47
|
: undefined;
|
|
66
48
|
const result = validateInboundStreamPayload(streamSpec, envelope.channel, envelope.payload, deps.validatorCtx);
|
|
67
49
|
if (!result.valid)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `drain_ack` channel handler — factored out of `runtime.ts` into
|
|
3
|
-
* the `@ggui-ai/channel
|
|
3
|
+
* the `@ggui-ai/live-channel` layer. The `onMessage` body fans the
|
|
4
4
|
* payload out to the module-scoped `subscribeDrainAck` listener
|
|
5
5
|
* registry so per-action state machines stay decoupled from how the
|
|
6
6
|
* frame arrives on the wire.
|
|
7
7
|
*/
|
|
8
|
-
import type { ChannelHandler } from '@ggui-ai/channel
|
|
8
|
+
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
9
9
|
import type { DrainAckPayload } from '@ggui-ai/protocol';
|
|
10
10
|
export interface DrainAckHandlerDeps {
|
|
11
11
|
readonly dispatch: (payload: DrainAckPayload) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `drain_ack` channel handler — factored out of `runtime.ts` into
|
|
3
|
-
* the `@ggui-ai/channel
|
|
3
|
+
* the `@ggui-ai/live-channel` layer. The `onMessage` body fans the
|
|
4
4
|
* payload out to the module-scoped `subscribeDrainAck` listener
|
|
5
5
|
* registry so per-action state machines stay decoupled from how the
|
|
6
6
|
* frame arrives on the wire.
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
* "no handler for type 'feedback'" warnings — the silence is
|
|
10
10
|
* intentional, the handler documents that.
|
|
11
11
|
*/
|
|
12
|
-
import type { ChannelHandler } from '@ggui-ai/channel
|
|
12
|
+
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
13
13
|
import type { JsonObject } from '@ggui-ai/protocol';
|
|
14
14
|
export declare function createFeedbackHandler(): ChannelHandler<JsonObject>;
|
package/dist/channels/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createPropsUpdateHandler, type PropsUpdateHandlerDeps, } from './props-update.js';
|
|
2
2
|
export { createDrainAckHandler, type DrainAckHandlerDeps, } from './drain-ack.js';
|
|
3
|
-
export {
|
|
3
|
+
export { createRenderHandler, type RenderHandlerDeps } from './render-frame.js';
|
|
4
4
|
export { createDataHandler, type DataHandlerDeps } from './data.js';
|
|
5
5
|
export { createFeedbackHandler } from './feedback.js';
|
|
6
6
|
export { createChannelPayloadHandler, createChannelErrorHandler, type ChannelRouterHandlerDeps, } from './channel-payload.js';
|
package/dist/channels/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createPropsUpdateHandler, } from './props-update.js';
|
|
2
2
|
export { createDrainAckHandler, } from './drain-ack.js';
|
|
3
|
-
export {
|
|
3
|
+
export { createRenderHandler } from './render-frame.js';
|
|
4
4
|
export { createDataHandler } from './data.js';
|
|
5
5
|
export { createFeedbackHandler } from './feedback.js';
|
|
6
6
|
export { createChannelPayloadHandler, createChannelErrorHandler, } from './channel-payload.js';
|
|
@@ -1,53 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `props_update` channel handler —
|
|
3
|
-
* the
|
|
2
|
+
* `props_update` channel handler — patches the currently-mounted
|
|
3
|
+
* render's props in place via the renderer's `applyRender`
|
|
4
|
+
* callback.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the
|
|
6
|
+
* Post-stack-removal (2026-05-27): each iframe holds exactly one
|
|
7
|
+
* mounted render. The handler reads the current render via
|
|
8
|
+
* `getCurrentRender()`, validates the inbound props against its
|
|
9
|
+
* cached `propsSpec`, and re-applies the patched render through
|
|
10
|
+
* `applyRender` — the same callback the render handler uses, so the
|
|
11
|
+
* React update surface is unified.
|
|
9
12
|
*
|
|
10
13
|
* Skips when:
|
|
11
|
-
* - `
|
|
14
|
+
* - `renderId` is empty or not a string.
|
|
12
15
|
* - `props` is null / not an object (defensive — server can't emit
|
|
13
16
|
* this shape, but the dispatcher routes the frame on type alone).
|
|
14
|
-
* - No
|
|
15
|
-
* - The
|
|
16
|
-
*
|
|
17
|
+
* - No render is currently mounted (`getCurrentRender` returns null).
|
|
18
|
+
* - The current render's id doesn't match `payload.renderId` —
|
|
19
|
+
* the server may have raced ahead of an in-flight render swap.
|
|
20
|
+
* - The current render is `mcpApps` / `system` (no `propsSpec`;
|
|
21
|
+
* server should never emit `props_update` for these).
|
|
17
22
|
* - The new props fail validation against the cached spec.
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* R6 (2026-05-26) retired the per-handler polling descriptor. Polling
|
|
25
|
+
* is now registry-level — the iframe-runtime composes the
|
|
26
|
+
* `/api/renders/:id/state?wsToken=<token>` URL and a snapshot-parsing
|
|
27
|
+
* function once at bind time (see `runtime.ts`); a single fetch per
|
|
28
|
+
* tick projects the slice envelope into per-handler frames the
|
|
29
|
+
* `PollingTransport` dispatches.
|
|
24
30
|
*/
|
|
25
|
-
import type { ChannelHandler } from '@ggui-ai/channel
|
|
26
|
-
import type { PropsUpdatePayload } from '@ggui-ai/protocol';
|
|
27
|
-
import type {
|
|
28
|
-
import type { StackModel } from '../stack.js';
|
|
31
|
+
import type { ChannelHandler } from '@ggui-ai/live-channel';
|
|
32
|
+
import type { PropsUpdatePayload, Render } from '@ggui-ai/protocol';
|
|
33
|
+
import type { RenderSeedInput } from '../types.js';
|
|
29
34
|
export interface PropsUpdateHandlerDeps {
|
|
30
|
-
readonly stackModel: StackModel;
|
|
31
|
-
readonly getStackRenderer: () => StackRenderer;
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* returns); the handler diffs `propsJson` between ticks and synthesizes
|
|
39
|
-
* a `props_update` frame whenever it changes. WS hosts ignore this
|
|
40
|
-
* descriptor (polling is inert under `WSTransport`).
|
|
41
|
-
*
|
|
42
|
-
* Producer: server stamps `bootstrap.pollingUrl` on the bootstrap
|
|
43
|
-
* envelope. Consumer: iframe-runtime threads it here at handler
|
|
44
|
-
* registration time. Absent → no polling fallback (WS-only mode).
|
|
36
|
+
* Read the currently-mounted render. Returns `null` when no
|
|
37
|
+
* render has been mounted yet (the first render frame hasn't landed)
|
|
38
|
+
* or after teardown. The handler short-circuits on `null` —
|
|
39
|
+
* `props_update` before the first render frame has no React tree to
|
|
40
|
+
* patch.
|
|
45
41
|
*/
|
|
46
|
-
readonly
|
|
42
|
+
readonly getCurrentRender: () => Render | RenderSeedInput | null;
|
|
47
43
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* Re-apply the patched render to the single mount slot. Shared with
|
|
45
|
+
* the render-frame handler so React updates flow through one path.
|
|
50
46
|
*/
|
|
51
|
-
readonly
|
|
47
|
+
readonly applyRender: (render: Render | RenderSeedInput) => Promise<void>;
|
|
52
48
|
}
|
|
53
49
|
export declare function createPropsUpdateHandler(deps: PropsUpdateHandlerDeps): ChannelHandler<PropsUpdatePayload>;
|