@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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default polling cadence for the iframe-polling transport (ms).
|
|
3
|
+
* Mirrors the server-side default on
|
|
4
|
+
* {@link DEFAULT_CHANNEL_POLL_DEFAULT_MS} in
|
|
5
|
+
* `packages/mcp-server/src/session-channel.ts`. Conservative: 10s
|
|
6
|
+
* matches a "data is fresh enough" bar without burning the parent
|
|
7
|
+
* host's `tools/call` quota.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* Factory.
|
|
12
|
+
*
|
|
13
|
+
* Per-channel transport routing — owns the `(stackItemId, channelName)`
|
|
14
|
+
* registry, the WS-vs-poll decision per channel, and the cross-state
|
|
15
|
+
* transitions (disconnect → poll fallback, reconnect → re-subscribe).
|
|
16
|
+
*/
|
|
17
|
+
export function createChannelTransportRouter(opts) {
|
|
18
|
+
/**
|
|
19
|
+
* Registry keyed by `${stackItemId}:${channelName}`. The composite
|
|
20
|
+
* key matches the server-side `channelSubs` map shape exactly, so
|
|
21
|
+
* test snapshots line up.
|
|
22
|
+
*/
|
|
23
|
+
const channels = new Map();
|
|
24
|
+
const allowlist = new Set(opts.streamWebSocketLocalTools ?? []);
|
|
25
|
+
const defaultPollMs = opts.defaultPollIntervalMs ?? DEFAULT_IFRAME_POLL_INTERVAL_MS;
|
|
26
|
+
/**
|
|
27
|
+
* WS lifecycle flag. We start in `'connected'` because the router
|
|
28
|
+
* is created AFTER the subscribe ack — the runtime's bootSequence
|
|
29
|
+
* resolves `subscribeFn(...)` before threading the manager into
|
|
30
|
+
* triadWiring's `attachManager` (where the router is constructed).
|
|
31
|
+
* Status transitions update this; the router uses it to decide
|
|
32
|
+
* whether to start WS subscribes or skip straight to polling.
|
|
33
|
+
*/
|
|
34
|
+
let wsStatus = 'connected';
|
|
35
|
+
let disposed = false;
|
|
36
|
+
const observe = opts.onObserve ?? (() => { });
|
|
37
|
+
function keyOf(stackItemId, channelName) {
|
|
38
|
+
return `${stackItemId}:${channelName}`;
|
|
39
|
+
}
|
|
40
|
+
function stopPolling(state) {
|
|
41
|
+
if (state.pollTimer !== null) {
|
|
42
|
+
clearInterval(state.pollTimer);
|
|
43
|
+
state.pollTimer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function emitToBus(state, payload, complete) {
|
|
47
|
+
const envelope = {
|
|
48
|
+
sessionId: opts.sessionId,
|
|
49
|
+
channel: state.channelName,
|
|
50
|
+
mode: state.mode,
|
|
51
|
+
payload,
|
|
52
|
+
...(complete === true ? { complete: true } : {}),
|
|
53
|
+
};
|
|
54
|
+
opts.streamBus.emit(envelope);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start an iframe-polling loop for one channel. Idempotent — if a
|
|
58
|
+
* timer is already running we leave it alone. Fires the first poll
|
|
59
|
+
* IMMEDIATELY (no `setInterval` lead-in delay) so the first
|
|
60
|
+
* `useStream` render isn't blocked on a 10-second wait.
|
|
61
|
+
*/
|
|
62
|
+
function startPolling(state) {
|
|
63
|
+
if (state.pollTimer !== null)
|
|
64
|
+
return;
|
|
65
|
+
const tick = async () => {
|
|
66
|
+
if (disposed)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const payload = await opts.toolsCall({
|
|
70
|
+
toolName: state.toolName,
|
|
71
|
+
args: { ...(state.args ?? {}) },
|
|
72
|
+
});
|
|
73
|
+
if (disposed)
|
|
74
|
+
return;
|
|
75
|
+
emitToBus(state, payload);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Swallow — next tick may succeed. The renderer's
|
|
79
|
+
// observability path already surfaces transport failures
|
|
80
|
+
// via the WS error route; per-tool poll failures stay quiet
|
|
81
|
+
// to avoid a noisy bus during transient outages.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// Fire-and-forget the leading tick + schedule the recurring
|
|
85
|
+
// ticks. Both are wrapped in the `disposed` guard above.
|
|
86
|
+
void tick();
|
|
87
|
+
state.pollTimer = setInterval(() => {
|
|
88
|
+
void tick();
|
|
89
|
+
}, defaultPollMs);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Send a `channel_subscribe` WS frame. Server-side bookkeeping is
|
|
93
|
+
* idempotent on `(sessionId, stackItemId, channelName)`, so re-sends
|
|
94
|
+
* on reconnect are safe.
|
|
95
|
+
*/
|
|
96
|
+
function sendSubscribe(state) {
|
|
97
|
+
opts.send({
|
|
98
|
+
type: 'channel_subscribe',
|
|
99
|
+
payload: {
|
|
100
|
+
sessionId: opts.sessionId,
|
|
101
|
+
appId: opts.appId,
|
|
102
|
+
stackItemId: state.stackItemId,
|
|
103
|
+
channelName: state.channelName,
|
|
104
|
+
...(state.args !== undefined ? { args: { ...state.args } } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send a `channel_unsubscribe` WS frame for a removed channel.
|
|
110
|
+
* Server-side is also idempotent on unknown pairs.
|
|
111
|
+
*/
|
|
112
|
+
function sendUnsubscribe(state) {
|
|
113
|
+
if (wsStatus !== 'connected')
|
|
114
|
+
return;
|
|
115
|
+
opts.send({
|
|
116
|
+
type: 'channel_unsubscribe',
|
|
117
|
+
payload: {
|
|
118
|
+
sessionId: opts.sessionId,
|
|
119
|
+
appId: opts.appId,
|
|
120
|
+
stackItemId: state.stackItemId,
|
|
121
|
+
channelName: state.channelName,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Bootstrap a channel onto its preferred transport.
|
|
127
|
+
*
|
|
128
|
+
* - `preferWs && wsStatus === 'connected'` → send subscribe,
|
|
129
|
+
* observe `'ws'` pick.
|
|
130
|
+
* - `preferWs && wsStatus !== 'connected'` → start polling
|
|
131
|
+
* fallback NOW so the user sees data while the WS reconnects,
|
|
132
|
+
* observe `'poll'` pick. When the WS comes up, we'll attempt
|
|
133
|
+
* `sendSubscribe` from `onWsStatusChange`.
|
|
134
|
+
* - `!preferWs` → polling, observe `'poll'`.
|
|
135
|
+
*/
|
|
136
|
+
function activate(state) {
|
|
137
|
+
if (state.preferWs && !state.permanentPollFallback) {
|
|
138
|
+
if (wsStatus === 'connected') {
|
|
139
|
+
sendSubscribe(state);
|
|
140
|
+
observe({
|
|
141
|
+
kind: 'channel-transport-picked',
|
|
142
|
+
stackItemId: state.stackItemId,
|
|
143
|
+
channelName: state.channelName,
|
|
144
|
+
transport: 'ws',
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// WS not ready yet — fall through to polling. We'll re-attempt
|
|
149
|
+
// the subscribe on the next 'connected' transition.
|
|
150
|
+
startPolling(state);
|
|
151
|
+
observe({
|
|
152
|
+
kind: 'channel-transport-picked',
|
|
153
|
+
stackItemId: state.stackItemId,
|
|
154
|
+
channelName: state.channelName,
|
|
155
|
+
transport: 'poll',
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
startPolling(state);
|
|
160
|
+
observe({
|
|
161
|
+
kind: 'channel-transport-picked',
|
|
162
|
+
stackItemId: state.stackItemId,
|
|
163
|
+
channelName: state.channelName,
|
|
164
|
+
transport: 'poll',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
applyStackItem: (item) => {
|
|
169
|
+
if (disposed)
|
|
170
|
+
return;
|
|
171
|
+
const seenKeys = new Set();
|
|
172
|
+
const spec = item.streamSpec ?? {};
|
|
173
|
+
for (const [channelName, entry] of Object.entries(spec)) {
|
|
174
|
+
const source = entry.source;
|
|
175
|
+
// Channels without a `source.tool` declaration stay on the
|
|
176
|
+
// legacy `data` frame path — agent → server → fan-out. The
|
|
177
|
+
// router only manages the source-fed subset (1c scope).
|
|
178
|
+
if (source === undefined ||
|
|
179
|
+
source === null ||
|
|
180
|
+
typeof source !== 'object' ||
|
|
181
|
+
typeof source.tool !== 'string' ||
|
|
182
|
+
source.tool.length === 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const toolName = source.tool;
|
|
186
|
+
const channelArgs = source.args !== undefined && source.args !== null
|
|
187
|
+
? source.args
|
|
188
|
+
: undefined;
|
|
189
|
+
const k = keyOf(item.stackItemId, channelName);
|
|
190
|
+
seenKeys.add(k);
|
|
191
|
+
const existing = channels.get(k);
|
|
192
|
+
if (existing !== undefined) {
|
|
193
|
+
// Same (stackItemId, channelName) — leave the transport
|
|
194
|
+
// bookkeeping alone. Spec changes that flip preferWs
|
|
195
|
+
// mid-session are out of 1c scope (would require
|
|
196
|
+
// re-handshake).
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const state = {
|
|
200
|
+
stackItemId: item.stackItemId,
|
|
201
|
+
channelName,
|
|
202
|
+
toolName,
|
|
203
|
+
...(channelArgs !== undefined ? { args: channelArgs } : {}),
|
|
204
|
+
mode: entry.mode ?? 'append',
|
|
205
|
+
preferWs: allowlist.has(toolName),
|
|
206
|
+
pollTimer: null,
|
|
207
|
+
hasReceivedWsPayload: false,
|
|
208
|
+
permanentPollFallback: false,
|
|
209
|
+
};
|
|
210
|
+
channels.set(k, state);
|
|
211
|
+
activate(state);
|
|
212
|
+
}
|
|
213
|
+
// Tear down any channel for THIS stack item that's no longer in
|
|
214
|
+
// the spec. Channels for OTHER stack items stay — they belong
|
|
215
|
+
// to other items that haven't been re-applied yet. Stack-pop /
|
|
216
|
+
// close-session paths call `dispose()` for the wholesale
|
|
217
|
+
// teardown.
|
|
218
|
+
for (const [k, state] of channels) {
|
|
219
|
+
if (state.stackItemId === item.stackItemId &&
|
|
220
|
+
!seenKeys.has(k)) {
|
|
221
|
+
stopPolling(state);
|
|
222
|
+
sendUnsubscribe(state);
|
|
223
|
+
channels.delete(k);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
handleWsFrame: (msg) => {
|
|
228
|
+
if (disposed)
|
|
229
|
+
return false;
|
|
230
|
+
if (msg.type === 'channel_payload') {
|
|
231
|
+
const p = msg.payload;
|
|
232
|
+
const k = keyOf(p.stackItemId, p.channelName);
|
|
233
|
+
const state = channels.get(k);
|
|
234
|
+
if (state === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
// First WS payload after a disconnect-fallback → cancel the
|
|
237
|
+
// polling loop. Server has demonstrably resumed fan-out, so
|
|
238
|
+
// we don't need the redundant iframe-poll source for this
|
|
239
|
+
// channel any more (on this lifecycle).
|
|
240
|
+
if (!state.hasReceivedWsPayload && state.pollTimer !== null) {
|
|
241
|
+
stopPolling(state);
|
|
242
|
+
}
|
|
243
|
+
state.hasReceivedWsPayload = true;
|
|
244
|
+
emitToBus(state, p.payload, p.complete);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (msg.type === 'channel_error') {
|
|
248
|
+
const p = msg.payload;
|
|
249
|
+
// Locate by `channelName` only — the error payload doesn't
|
|
250
|
+
// carry `stackItemId`. We match by channel-name across active
|
|
251
|
+
// items and apply the permanent-fallback flag to the matching
|
|
252
|
+
// entries. Same channel name across stack items is rare; the
|
|
253
|
+
// policy is conservative (fall back ALL matches rather than
|
|
254
|
+
// miss one).
|
|
255
|
+
for (const state of channels.values()) {
|
|
256
|
+
if (state.channelName !== p.channelName)
|
|
257
|
+
continue;
|
|
258
|
+
if (p.code === 'CHANNEL_NOT_LOCAL' ||
|
|
259
|
+
p.code === 'CHANNEL_UNKNOWN' ||
|
|
260
|
+
p.code === 'STACK_ITEM_NOT_FOUND') {
|
|
261
|
+
state.permanentPollFallback = true;
|
|
262
|
+
startPolling(state);
|
|
263
|
+
observe({
|
|
264
|
+
kind: 'channel-transport-fallback',
|
|
265
|
+
stackItemId: state.stackItemId,
|
|
266
|
+
channelName: state.channelName,
|
|
267
|
+
reason: 'channel-not-local',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// POLL_FAILED / SUBSCRIBE_UNAUTHORIZED are transient — the
|
|
271
|
+
// server may recover. Don't flip the permanent fallback;
|
|
272
|
+
// leave the WS path active. (UNAUTHORIZED would normally
|
|
273
|
+
// imply the bootstrap token expired, in which case the
|
|
274
|
+
// outer subscribe-fail path tears down the whole socket;
|
|
275
|
+
// we don't need to special-case it here.)
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
},
|
|
281
|
+
onWsStatusChange: (status) => {
|
|
282
|
+
if (disposed)
|
|
283
|
+
return;
|
|
284
|
+
const prevStatus = wsStatus;
|
|
285
|
+
wsStatus = status;
|
|
286
|
+
if (status === 'disconnected' || status === 'reconnecting') {
|
|
287
|
+
// Every WS-preferring channel falls back to polling
|
|
288
|
+
// IMMEDIATELY. The user sees fresh data while the reconnect
|
|
289
|
+
// ladder runs. Reset the `hasReceivedWsPayload` flag so the
|
|
290
|
+
// first post-reconnect payload re-triggers the
|
|
291
|
+
// "stop the polling fallback" transition.
|
|
292
|
+
for (const state of channels.values()) {
|
|
293
|
+
if (!state.preferWs)
|
|
294
|
+
continue;
|
|
295
|
+
if (state.permanentPollFallback)
|
|
296
|
+
continue;
|
|
297
|
+
if (state.hasReceivedWsPayload) {
|
|
298
|
+
state.hasReceivedWsPayload = false;
|
|
299
|
+
}
|
|
300
|
+
if (state.pollTimer === null) {
|
|
301
|
+
startPolling(state);
|
|
302
|
+
observe({
|
|
303
|
+
kind: 'channel-transport-fallback',
|
|
304
|
+
stackItemId: state.stackItemId,
|
|
305
|
+
channelName: state.channelName,
|
|
306
|
+
reason: 'ws-disconnect',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (status === 'connected' &&
|
|
313
|
+
(prevStatus === 'disconnected' || prevStatus === 'reconnecting')) {
|
|
314
|
+
// Reconnected. Re-send `channel_subscribe` for every
|
|
315
|
+
// WS-preferring channel. Server is idempotent on the (session,
|
|
316
|
+
// stackItem, channel) triple, so duplicates are safe. We
|
|
317
|
+
// intentionally LEAVE the polling fallback running until the
|
|
318
|
+
// first `channel_payload` lands — bridges the gap where the
|
|
319
|
+
// server's first post-reconnect poll cycle hasn't fired yet.
|
|
320
|
+
for (const state of channels.values()) {
|
|
321
|
+
if (!state.preferWs)
|
|
322
|
+
continue;
|
|
323
|
+
if (state.permanentPollFallback)
|
|
324
|
+
continue;
|
|
325
|
+
sendSubscribe(state);
|
|
326
|
+
observe({
|
|
327
|
+
kind: 'channel-transport-resubscribed',
|
|
328
|
+
stackItemId: state.stackItemId,
|
|
329
|
+
channelName: state.channelName,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
dispose: () => {
|
|
335
|
+
if (disposed)
|
|
336
|
+
return;
|
|
337
|
+
disposed = true;
|
|
338
|
+
for (const state of channels.values()) {
|
|
339
|
+
stopPolling(state);
|
|
340
|
+
}
|
|
341
|
+
channels.clear();
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `channel_payload` + `channel_error` handlers — forward to the
|
|
3
|
+
* per-channel transport router (`channel-transport.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The router owns the streamSpec subscription registry (which channels
|
|
6
|
+
* are WS-subscribed vs iframe-polling). Forwarding here is a thin
|
|
7
|
+
* passthrough — the router parses the frame contents against its
|
|
8
|
+
* subscription map and dispatches to the matching consumer.
|
|
9
|
+
*
|
|
10
|
+
* Factored out of `handleTriadMessage` as part of the B3a handler
|
|
11
|
+
* extraction.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChannelHandler } from '@ggui-ai/channel-client';
|
|
14
|
+
import type { ChannelErrorPayload, ChannelPayloadFrame } from '@ggui-ai/protocol';
|
|
15
|
+
import type { ChannelTransportRouter } from '../channel-transport.js';
|
|
16
|
+
export interface ChannelRouterHandlerDeps {
|
|
17
|
+
readonly getChannelTransport: () => ChannelTransportRouter;
|
|
18
|
+
}
|
|
19
|
+
export declare function createChannelPayloadHandler(deps: ChannelRouterHandlerDeps): ChannelHandler<ChannelPayloadFrame>;
|
|
20
|
+
export declare function createChannelErrorHandler(deps: ChannelRouterHandlerDeps): ChannelHandler<ChannelErrorPayload>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `channel_payload` + `channel_error` handlers — forward to the
|
|
3
|
+
* per-channel transport router (`channel-transport.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The router owns the streamSpec subscription registry (which channels
|
|
6
|
+
* are WS-subscribed vs iframe-polling). Forwarding here is a thin
|
|
7
|
+
* passthrough — the router parses the frame contents against its
|
|
8
|
+
* subscription map and dispatches to the matching consumer.
|
|
9
|
+
*
|
|
10
|
+
* Factored out of `handleTriadMessage` as part of the B3a handler
|
|
11
|
+
* extraction.
|
|
12
|
+
*/
|
|
13
|
+
export function createChannelPayloadHandler(deps) {
|
|
14
|
+
return {
|
|
15
|
+
type: 'channel_payload',
|
|
16
|
+
onMessage: (payload) => {
|
|
17
|
+
deps.getChannelTransport().handleWsFrame({
|
|
18
|
+
type: 'channel_payload',
|
|
19
|
+
payload,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createChannelErrorHandler(deps) {
|
|
25
|
+
return {
|
|
26
|
+
type: 'channel_error',
|
|
27
|
+
onMessage: (payload) => {
|
|
28
|
+
deps.getChannelTransport().handleWsFrame({
|
|
29
|
+
type: 'channel_error',
|
|
30
|
+
payload,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
|
+
* against the active stack item's `streamSpec`, then fans the validated
|
|
4
|
+
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
|
+
* (matching `GguiSession.handleServerMessage`'s surfacing policy —
|
|
6
|
+
* the violation is already logged by the validator).
|
|
7
|
+
*
|
|
8
|
+
* Also fires `contract-error-emitted` on the optional observability
|
|
9
|
+
* sink when the envelope arrives on the reserved
|
|
10
|
+
* `CONTRACT_ERROR_CHANNEL`. Absorbed from `handleObservableMessage`
|
|
11
|
+
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
|
+
* sole dispatch surface for `data` frames.
|
|
13
|
+
*
|
|
14
|
+
* Factored out of `handleTriadMessage` as part of the B3a handler
|
|
15
|
+
* extraction; expanded in B3b to absorb the observability-axis
|
|
16
|
+
* emission previously fired in parallel from `bootSequence`.
|
|
17
|
+
*/
|
|
18
|
+
import type { ChannelHandler } from '@ggui-ai/channel-client';
|
|
19
|
+
import { type CanvasLifecyclePayload, type StreamEnvelope } from '@ggui-ai/protocol';
|
|
20
|
+
import type { ObservabilityEmitter } from '../observability.js';
|
|
21
|
+
import type { StackModel } from '../stack.js';
|
|
22
|
+
import { type RendererValidatorContext } from '../validation.js';
|
|
23
|
+
import type { StreamBus } from '../wire-config.js';
|
|
24
|
+
export interface DataHandlerDeps {
|
|
25
|
+
readonly stackModel: StackModel;
|
|
26
|
+
readonly streamBus: StreamBus;
|
|
27
|
+
readonly validatorCtx: RendererValidatorContext;
|
|
28
|
+
/**
|
|
29
|
+
* Optional observability sink. When present, every envelope on the
|
|
30
|
+
* reserved `_ggui:contract-error` channel fires a
|
|
31
|
+
* {@link ContractErrorEmittedEvent}. Absent = the observation skips
|
|
32
|
+
* silently (matches the pre-B3b posture when no `onObserve` is
|
|
33
|
+
* bound).
|
|
34
|
+
*/
|
|
35
|
+
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
|
+
}
|
|
48
|
+
export declare function createDataHandler(deps: DataHandlerDeps): ChannelHandler<StreamEnvelope>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `data` channel handler — validates inbound stream-envelope payloads
|
|
3
|
+
* against the active stack item's `streamSpec`, then fans the validated
|
|
4
|
+
* envelope out via `streamBus`. Invalid payloads drop silently
|
|
5
|
+
* (matching `GguiSession.handleServerMessage`'s surfacing policy —
|
|
6
|
+
* the violation is already logged by the validator).
|
|
7
|
+
*
|
|
8
|
+
* Also fires `contract-error-emitted` on the optional observability
|
|
9
|
+
* sink when the envelope arrives on the reserved
|
|
10
|
+
* `CONTRACT_ERROR_CHANNEL`. Absorbed from `handleObservableMessage`
|
|
11
|
+
* in `runtime.ts` as part of the B3b cleanup — the handler is now the
|
|
12
|
+
* sole dispatch surface for `data` frames.
|
|
13
|
+
*
|
|
14
|
+
* Factored out of `handleTriadMessage` as part of the B3a handler
|
|
15
|
+
* extraction; expanded in B3b to absorb the observability-axis
|
|
16
|
+
* emission previously fired in parallel from `bootSequence`.
|
|
17
|
+
*/
|
|
18
|
+
import { CONTRACT_ERROR_CHANNEL, LIFECYCLE_CHANNEL, } from '@ggui-ai/protocol';
|
|
19
|
+
import { validateInboundStreamPayload, } from '../validation.js';
|
|
20
|
+
export function createDataHandler(deps) {
|
|
21
|
+
return {
|
|
22
|
+
type: 'data',
|
|
23
|
+
onMessage: (envelope) => {
|
|
24
|
+
if (envelope === undefined ||
|
|
25
|
+
envelope === null ||
|
|
26
|
+
typeof envelope.channel !== 'string') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Observability-axis emission for the reserved contract-error
|
|
30
|
+
// channel. Fires BEFORE validation because the envelope shape
|
|
31
|
+
// is owned by the server's reserved validator
|
|
32
|
+
// (`BUILTIN_RESERVED_VALIDATORS`) — invalid payloads at this
|
|
33
|
+
// boundary are server-side bugs the host inspector should still
|
|
34
|
+
// surface, not silently drop.
|
|
35
|
+
if (envelope.channel === CONTRACT_ERROR_CHANNEL &&
|
|
36
|
+
deps.onObserve !== undefined) {
|
|
37
|
+
emitContractErrorFromDataFrame(envelope, deps.onObserve);
|
|
38
|
+
}
|
|
39
|
+
// Canvas-mode lifecycle
|
|
40
|
+
// envelopes — fires BEFORE validation for the same reason as
|
|
41
|
+
// the contract-error path: the wire shape is server-owned, and
|
|
42
|
+
// any reachable envelope is a server-emitted signal. The
|
|
43
|
+
// payload's `kind` discriminator narrows downstream; the
|
|
44
|
+
// animator host treats unknown kinds as no-ops. Invalid shapes
|
|
45
|
+
// silently drop here (typeof check) — defensive against future
|
|
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
|
|
65
|
+
: undefined;
|
|
66
|
+
const result = validateInboundStreamPayload(streamSpec, envelope.channel, envelope.payload, deps.validatorCtx);
|
|
67
|
+
if (!result.valid)
|
|
68
|
+
return;
|
|
69
|
+
deps.streamBus.emit(envelope);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Narrow a contract-error envelope and emit
|
|
75
|
+
* {@link ContractErrorEmittedEvent}. Malformed envelopes silently
|
|
76
|
+
* skip (trust-boundary posture — host inspector gets a row only when
|
|
77
|
+
* the wire payload carries the required discriminator fields).
|
|
78
|
+
*/
|
|
79
|
+
function emitContractErrorFromDataFrame(envelope, emit) {
|
|
80
|
+
const payload = envelope.payload;
|
|
81
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const shaped = payload;
|
|
85
|
+
const code = shaped.error !== undefined &&
|
|
86
|
+
shaped.error !== null &&
|
|
87
|
+
typeof shaped.error === 'object' &&
|
|
88
|
+
typeof shaped.error.code === 'string'
|
|
89
|
+
? shaped.error.code
|
|
90
|
+
: undefined;
|
|
91
|
+
const toolName = typeof shaped.toolName === 'string' ? shaped.toolName : undefined;
|
|
92
|
+
if (code === undefined || toolName === undefined)
|
|
93
|
+
return;
|
|
94
|
+
const event = {
|
|
95
|
+
kind: 'contract-error-emitted',
|
|
96
|
+
code,
|
|
97
|
+
toolName,
|
|
98
|
+
...(typeof shaped.actionName === 'string'
|
|
99
|
+
? { actionName: shaped.actionName }
|
|
100
|
+
: {}),
|
|
101
|
+
};
|
|
102
|
+
emit(event);
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drain_ack` channel handler — factored out of `runtime.ts` into
|
|
3
|
+
* the `@ggui-ai/channel-client` layer. The `onMessage` body fans the
|
|
4
|
+
* payload out to the module-scoped `subscribeDrainAck` listener
|
|
5
|
+
* registry so per-action state machines stay decoupled from how the
|
|
6
|
+
* frame arrives on the wire.
|
|
7
|
+
*/
|
|
8
|
+
import type { ChannelHandler } from '@ggui-ai/channel-client';
|
|
9
|
+
import type { DrainAckPayload } from '@ggui-ai/protocol';
|
|
10
|
+
export interface DrainAckHandlerDeps {
|
|
11
|
+
readonly dispatch: (payload: DrainAckPayload) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createDrainAckHandler(deps: DrainAckHandlerDeps): ChannelHandler<DrainAckPayload>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drain_ack` channel handler — factored out of `runtime.ts` into
|
|
3
|
+
* the `@ggui-ai/channel-client` layer. The `onMessage` body fans the
|
|
4
|
+
* payload out to the module-scoped `subscribeDrainAck` listener
|
|
5
|
+
* registry so per-action state machines stay decoupled from how the
|
|
6
|
+
* frame arrives on the wire.
|
|
7
|
+
*/
|
|
8
|
+
export function createDrainAckHandler(deps) {
|
|
9
|
+
return {
|
|
10
|
+
type: 'drain_ack',
|
|
11
|
+
onMessage: (payload) => {
|
|
12
|
+
deps.dispatch(payload);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `feedback` channel handler — no-op stub.
|
|
3
|
+
*
|
|
4
|
+
* Legacy `feedback` frame: older servers emitted this as the round-trip
|
|
5
|
+
* half of the retired `tool:<name>` client-tool RPC path. Silently
|
|
6
|
+
* ignored under the `clientCapabilities` reframe; kept as a recognized
|
|
7
|
+
* type until the WS message union drops `'feedback'`. Registering a
|
|
8
|
+
* no-op handler is what keeps the registry's dispatch from logging
|
|
9
|
+
* "no handler for type 'feedback'" warnings — the silence is
|
|
10
|
+
* intentional, the handler documents that.
|
|
11
|
+
*/
|
|
12
|
+
import type { ChannelHandler } from '@ggui-ai/channel-client';
|
|
13
|
+
import type { JsonObject } from '@ggui-ai/protocol';
|
|
14
|
+
export declare function createFeedbackHandler(): ChannelHandler<JsonObject>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `feedback` channel handler — no-op stub.
|
|
3
|
+
*
|
|
4
|
+
* Legacy `feedback` frame: older servers emitted this as the round-trip
|
|
5
|
+
* half of the retired `tool:<name>` client-tool RPC path. Silently
|
|
6
|
+
* ignored under the `clientCapabilities` reframe; kept as a recognized
|
|
7
|
+
* type until the WS message union drops `'feedback'`. Registering a
|
|
8
|
+
* no-op handler is what keeps the registry's dispatch from logging
|
|
9
|
+
* "no handler for type 'feedback'" warnings — the silence is
|
|
10
|
+
* intentional, the handler documents that.
|
|
11
|
+
*/
|
|
12
|
+
export function createFeedbackHandler() {
|
|
13
|
+
return {
|
|
14
|
+
type: 'feedback',
|
|
15
|
+
onMessage: () => {
|
|
16
|
+
/* no-op — legacy stub */
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createPropsUpdateHandler, type PropsUpdateHandlerDeps, } from './props-update.js';
|
|
2
|
+
export { createDrainAckHandler, type DrainAckHandlerDeps, } from './drain-ack.js';
|
|
3
|
+
export { createPushHandler, type PushHandlerDeps } from './push.js';
|
|
4
|
+
export { createDataHandler, type DataHandlerDeps } from './data.js';
|
|
5
|
+
export { createFeedbackHandler } from './feedback.js';
|
|
6
|
+
export { createChannelPayloadHandler, createChannelErrorHandler, type ChannelRouterHandlerDeps, } from './channel-payload.js';
|
|
7
|
+
export { createSystemHandler, type SystemHandlerDeps } from './system.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createPropsUpdateHandler, } from './props-update.js';
|
|
2
|
+
export { createDrainAckHandler, } from './drain-ack.js';
|
|
3
|
+
export { createPushHandler } from './push.js';
|
|
4
|
+
export { createDataHandler } from './data.js';
|
|
5
|
+
export { createFeedbackHandler } from './feedback.js';
|
|
6
|
+
export { createChannelPayloadHandler, createChannelErrorHandler, } from './channel-payload.js';
|
|
7
|
+
export { createSystemHandler } from './system.js';
|