@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.
Files changed (92) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/bootstrap.d.ts +72 -0
  4. package/dist/bootstrap.js +500 -0
  5. package/dist/canvas/animator/animator-host.d.ts +63 -0
  6. package/dist/canvas/animator/animator-host.js +105 -0
  7. package/dist/canvas/animator/animator-pill.d.ts +42 -0
  8. package/dist/canvas/animator/animator-pill.js +119 -0
  9. package/dist/canvas/animator/state-machine.d.ts +106 -0
  10. package/dist/canvas/animator/state-machine.js +150 -0
  11. package/dist/canvas/canvas-shell.d.ts +77 -0
  12. package/dist/canvas/canvas-shell.js +117 -0
  13. package/dist/canvas/display-mode-policy.d.ts +104 -0
  14. package/dist/canvas/display-mode-policy.js +98 -0
  15. package/dist/canvas/mount.d.ts +150 -0
  16. package/dist/canvas/mount.js +202 -0
  17. package/dist/canvas/nav-stack.d.ts +133 -0
  18. package/dist/canvas/nav-stack.js +172 -0
  19. package/dist/channel-transport.d.ts +182 -0
  20. package/dist/channel-transport.js +344 -0
  21. package/dist/channels/channel-payload.d.ts +20 -0
  22. package/dist/channels/channel-payload.js +34 -0
  23. package/dist/channels/data.d.ts +48 -0
  24. package/dist/channels/data.js +103 -0
  25. package/dist/channels/drain-ack.d.ts +13 -0
  26. package/dist/channels/drain-ack.js +15 -0
  27. package/dist/channels/feedback.d.ts +14 -0
  28. package/dist/channels/feedback.js +19 -0
  29. package/dist/channels/index.d.ts +7 -0
  30. package/dist/channels/index.js +7 -0
  31. package/dist/channels/props-update.d.ts +53 -0
  32. package/dist/channels/props-update.js +130 -0
  33. package/dist/channels/push.d.ts +47 -0
  34. package/dist/channels/push.js +54 -0
  35. package/dist/channels/system.d.ts +26 -0
  36. package/dist/channels/system.js +44 -0
  37. package/dist/compiled-validators.d.ts +40 -0
  38. package/dist/compiled-validators.js +74 -0
  39. package/dist/context-observer.d.ts +205 -0
  40. package/dist/context-observer.js +312 -0
  41. package/dist/gadget-loader.d.ts +118 -0
  42. package/dist/gadget-loader.js +136 -0
  43. package/dist/globals.d.ts +234 -0
  44. package/dist/globals.js +101 -0
  45. package/dist/host-context-emitter.d.ts +91 -0
  46. package/dist/host-context-emitter.js +213 -0
  47. package/dist/iframe-runtime.js +261 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +4 -0
  50. package/dist/lifecycle.d.ts +66 -0
  51. package/dist/lifecycle.js +49 -0
  52. package/dist/mcp-app-iframe-host.d.ts +69 -0
  53. package/dist/mcp-app-iframe-host.js +214 -0
  54. package/dist/observability.d.ts +225 -0
  55. package/dist/observability.js +28 -0
  56. package/dist/protocol-error.d.ts +199 -0
  57. package/dist/protocol-error.js +116 -0
  58. package/dist/provisional-renderer.d.ts +62 -0
  59. package/dist/provisional-renderer.js +323 -0
  60. package/dist/react-renderer.d.ts +95 -0
  61. package/dist/react-renderer.js +343 -0
  62. package/dist/registry-subscribe.d.ts +102 -0
  63. package/dist/registry-subscribe.js +319 -0
  64. package/dist/runtime.d.ts +531 -0
  65. package/dist/runtime.js +2623 -0
  66. package/dist/server.d.ts +36 -0
  67. package/dist/server.js +72 -0
  68. package/dist/stack-item-renderer.d.ts +103 -0
  69. package/dist/stack-item-renderer.js +270 -0
  70. package/dist/stack.d.ts +90 -0
  71. package/dist/stack.js +118 -0
  72. package/dist/status-dom.d.ts +72 -0
  73. package/dist/status-dom.js +51 -0
  74. package/dist/system-cards/NoCredentialsCard.d.ts +33 -0
  75. package/dist/system-cards/NoCredentialsCard.js +189 -0
  76. package/dist/system-cards/ProtocolProbeCard.d.ts +64 -0
  77. package/dist/system-cards/ProtocolProbeCard.js +463 -0
  78. package/dist/system-cards/QrCode.d.ts +46 -0
  79. package/dist/system-cards/QrCode.js +55 -0
  80. package/dist/system-cards/host-detect.d.ts +31 -0
  81. package/dist/system-cards/host-detect.js +108 -0
  82. package/dist/system-cards/host-intents.d.ts +85 -0
  83. package/dist/system-cards/host-intents.js +170 -0
  84. package/dist/system-cards/index.d.ts +70 -0
  85. package/dist/system-cards/index.js +134 -0
  86. package/dist/types.d.ts +69 -0
  87. package/dist/types.js +3 -0
  88. package/dist/validation.d.ts +83 -0
  89. package/dist/validation.js +106 -0
  90. package/dist/wire-config.d.ts +214 -0
  91. package/dist/wire-config.js +290 -0
  92. package/package.json +81 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * NavStackModel.
3
+ *
4
+ * Canvas-local navigation stack. **Distinct from the server's
5
+ * `StackModel`**:
6
+ *
7
+ * - Server's `StackModel` (`@ggui-ai/iframe-runtime/stack.ts`)
8
+ * tracks every stack item the agent has pushed in this session.
9
+ * The durable shape; mutated only by server-side push/pop/replace
10
+ * deliveries.
11
+ * - NavStackModel tracks the USER'S NAVIGATION HISTORY through
12
+ * those items. When the user back-navigates, the popped item
13
+ * leaves navStack but stays in server's StackModel.
14
+ *
15
+ * These can diverge:
16
+ *
17
+ * ```
18
+ * server StackModel: [a, b, c] ← all items the agent has pushed
19
+ * navStack: [a, b] ← user has back-navigated away from c
20
+ * activeItem: b ← top of navStack; the mounted screen
21
+ * ```
22
+ *
23
+ * The server-side `activeStackItemId` field (on AckPayload +
24
+ * SessionRecord) follows navStack-top via `canvas_navigated` outbound
25
+ * envelopes, so `ggui_consume` returns actions for the active item
26
+ * regardless of what the agent has pushed since.
27
+ *
28
+ * Pure model — no DOM, no React. Methods return the new state (or
29
+ * mutate in-place; v1 mutates for simplicity since the canvas shell
30
+ * holds the only reference). The renderer consumes the model via a
31
+ * version counter (bumped on every mutation) so React picks up
32
+ * changes without deep equality.
33
+ *
34
+ * Two listener seams (distinct semantics):
35
+ *
36
+ * - `onMutation` fires on EVERY state mutation including in-place
37
+ * replace (ggui_update patch). The canvas shell uses this to
38
+ * re-read the version counter and re-render — without it, a
39
+ * props-only update on the active item bumps the version but
40
+ * React never schedules a render and the screen shows stale props.
41
+ * - `onNavChange` fires only when the active item actually changes
42
+ * (new push, pop, reset that lands on a different top). The
43
+ * live-channel `canvas_navigated` envelope is emitted from here, so
44
+ * an in-place replace doesn't spuriously tell the server the
45
+ * user back-navigated.
46
+ */
47
+ export class NavStackModel {
48
+ items = [];
49
+ mutationListener = null;
50
+ navChangeListener = null;
51
+ versionCounter = 0;
52
+ /**
53
+ * Replace the entire nav stack. Used on subscribe ack to seed
54
+ * navStack from the server's `activeStackItemId` + the snapshot's
55
+ * stack array.
56
+ */
57
+ reset(items) {
58
+ const previous = this.peek();
59
+ this.items = [...items];
60
+ this.versionCounter += 1;
61
+ this.mutationListener?.();
62
+ this.navChangeListener?.({
63
+ direction: 'forward',
64
+ activeItemId: this.peek()?.id ?? null,
65
+ previousActiveItemId: previous?.id ?? null,
66
+ });
67
+ }
68
+ /**
69
+ * Append an item to the top. Called when a `stack_item_appended`
70
+ * envelope arrives (server pushed a new item).
71
+ *
72
+ * If an item with the same id already exists, the existing entry is
73
+ * REPLACED IN PLACE (preserves nav position) — matches the server-
74
+ * side StackModel's upsert semantics so `ggui_update` patches don't
75
+ * shift the user's nav state. The mutation listener still fires (so
76
+ * React re-renders with the new props) but the nav-change listener
77
+ * is suppressed (active item hasn't changed).
78
+ */
79
+ push(item) {
80
+ const existingIdx = this.items.findIndex((i) => i.id === item.id);
81
+ if (existingIdx >= 0) {
82
+ this.items[existingIdx] = item;
83
+ this.versionCounter += 1;
84
+ this.mutationListener?.();
85
+ return;
86
+ }
87
+ const previous = this.peek();
88
+ this.items.push(item);
89
+ this.versionCounter += 1;
90
+ this.mutationListener?.();
91
+ this.navChangeListener?.({
92
+ direction: 'forward',
93
+ activeItemId: item.id,
94
+ previousActiveItemId: previous?.id ?? null,
95
+ });
96
+ }
97
+ /**
98
+ * Pop the top item. Returns the new active item (or null if the
99
+ * stack is now empty). No-op if already empty.
100
+ */
101
+ pop() {
102
+ if (this.items.length === 0)
103
+ return null;
104
+ const popped = this.items.pop() ?? null;
105
+ this.versionCounter += 1;
106
+ const active = this.peek();
107
+ this.mutationListener?.();
108
+ this.navChangeListener?.({
109
+ direction: 'back',
110
+ activeItemId: active?.id ?? null,
111
+ previousActiveItemId: popped?.id ?? null,
112
+ });
113
+ return active;
114
+ }
115
+ /** Top of the nav stack (the currently-mounted item), or null when empty. */
116
+ peek() {
117
+ return this.items[this.items.length - 1] ?? null;
118
+ }
119
+ /** Read-only snapshot of the full nav stack in order (root → top). */
120
+ snapshot() {
121
+ return this.items;
122
+ }
123
+ /** Item count. */
124
+ size() {
125
+ return this.items.length;
126
+ }
127
+ /**
128
+ * Monotonic mutation counter — bumped on every push/pop/reset.
129
+ * React consumers can use this as the `useSyncExternalStore`
130
+ * snapshot value: comparing version numbers is O(1).
131
+ */
132
+ version() {
133
+ return this.versionCounter;
134
+ }
135
+ /**
136
+ * Install a listener that fires on every state mutation (push, pop,
137
+ * reset, in-place replace). React consumers bridge this to a
138
+ * version-counter `useState` so the component re-renders for every
139
+ * change — including in-place replaces from `ggui_update` patches
140
+ * where the active item is unchanged but its props have moved.
141
+ *
142
+ * Single-subscriber model — the canvas shell is the only caller.
143
+ * Returns an unsubscribe function.
144
+ */
145
+ onMutation(listener) {
146
+ this.mutationListener = listener;
147
+ return () => {
148
+ if (this.mutationListener === listener)
149
+ this.mutationListener = null;
150
+ };
151
+ }
152
+ /**
153
+ * Install a listener that fires only on nav-changing mutations
154
+ * (push of new id, pop, reset). Used by the canvas shell to emit
155
+ * the live-channel `canvas_navigated` envelope so the server can
156
+ * update `activeStackItemId` and abort in-flight cold-gen for the
157
+ * previous active item.
158
+ *
159
+ * In-place replaces from `ggui_update` patches do NOT fire this
160
+ * listener — the active item hasn't moved. Use {@link onMutation}
161
+ * for React re-render signaling.
162
+ *
163
+ * Single-subscriber model. Returns an unsubscribe function.
164
+ */
165
+ onNavChange(listener) {
166
+ this.navChangeListener = listener;
167
+ return () => {
168
+ if (this.navChangeListener === listener)
169
+ this.navChangeListener = null;
170
+ };
171
+ }
172
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Per-channel transport router.
3
+ *
4
+ * Owns the runtime-side half of the dual-transport stream-channel
5
+ * negotiation. For each `streamSpec[channelName]` entry that
6
+ * declares a `source.tool`, the router picks ONE of two transports
7
+ * per channel:
8
+ *
9
+ * - **WS subscribe** — when `source.tool` is in the bootstrap's
10
+ * `streamWebSocketLocalTools` allowlist. The router sends a
11
+ * `channel_subscribe` WS frame; the server polls the tool and
12
+ * fans out `channel_payload` frames. Cheapest path: zero LLM
13
+ * traffic, zero parent-iframe postMessage round-trips per
14
+ * payload.
15
+ *
16
+ * - **Iframe polling** — fallback for every other channel. The
17
+ * router runs a setInterval that fires `tools/call` against
18
+ * the parent MCP host (Pattern α — direct, no bridge). Works
19
+ * for any tool the agent has access to, including third-party
20
+ * MCP servers the local pod can't subscribe-for.
21
+ *
22
+ * The dispatch decision is per-channel + per-render (each new push
23
+ * brings a potentially-new contract; we re-run the routing). The
24
+ * StreamBus + `useStream(channel)` API stay unchanged — the router
25
+ * adapts payloads from both transports into the same envelope shape
26
+ * the bus already accepts.
27
+ *
28
+ * **Reconnect handling** — when the WS drops, every WS-bound channel:
29
+ * 1. Immediately falls back to iframe polling (no delay). The user
30
+ * keeps seeing fresh data while the WS reconnect ladder runs.
31
+ * 2. On successful reconnect → re-send `channel_subscribe`. On the
32
+ * first `channel_payload` from that channel, cancel the polling
33
+ * fallback (don't double-deliver).
34
+ *
35
+ * The WS-level exponential backoff (1s → 2s → 4s → ... cap 60s) lives
36
+ * on the {@link RendererWebSocketManager}. The router only watches
37
+ * status transitions; it doesn't own the reconnect schedule.
38
+ *
39
+ * **Idempotence** — re-subscribing the same `(stackItemId, channelName)`
40
+ * pair is server-side idempotent (the server's `ChannelSubscriptionState`
41
+ * replaces in place). Clients can re-send on reconnect without bookkeeping
42
+ * a "did we already subscribe?" gate.
43
+ *
44
+ * **Boundary** — this module produces a `StreamEnvelope` for every payload
45
+ * (both transports) and emits on the supplied bus. It does NOT validate
46
+ * payloads against `streamSpec[ch].schema` — the existing
47
+ * `validateInboundStreamPayload` path in `runtime.ts` does that on the
48
+ * `data` frame side. For WS `channel_payload` frames we route directly
49
+ * to the bus because the SERVER has already validated server-side; for
50
+ * iframe-poll payloads we trust the tool's return value (matches today's
51
+ * un-routed channel posture pre-1c, where any payload that landed on the
52
+ * data frame was treated as authoritative).
53
+ */
54
+ import type { JsonObject, JsonValue, StreamSpec } from '@ggui-ai/protocol';
55
+ import type { WebSocketMessage } from '@ggui-ai/protocol/transport/websocket';
56
+ import type { StreamBus } from './wire-config.js';
57
+ /**
58
+ * Default polling cadence for the iframe-polling transport (ms).
59
+ * Mirrors the server-side default on
60
+ * {@link DEFAULT_CHANNEL_POLL_DEFAULT_MS} in
61
+ * `packages/mcp-server/src/session-channel.ts`. Conservative: 10s
62
+ * matches a "data is fresh enough" bar without burning the parent
63
+ * host's `tools/call` quota.
64
+ */
65
+ export declare const DEFAULT_IFRAME_POLL_INTERVAL_MS = 10000;
66
+ /**
67
+ * Sender for outbound WS frames. The renderer's
68
+ * {@link RendererWebSocketManager.send} fits structurally — pass
69
+ * `manager.send.bind(manager)` or the buffered manager shim.
70
+ */
71
+ export type WsSender = (msg: WebSocketMessage) => void;
72
+ /**
73
+ * Iframe → parent `tools/call` invoker. Resolves with the tool's
74
+ * structured-content output (parsed `JsonValue`) or rejects on
75
+ * transport-level failure. The router will swallow the failure +
76
+ * keep the poll loop running (next tick may succeed).
77
+ */
78
+ export type ToolsCallInvoker = (args: {
79
+ readonly toolName: string;
80
+ readonly args: JsonObject;
81
+ }) => Promise<JsonValue>;
82
+ /**
83
+ * Options for {@link createChannelTransportRouter}.
84
+ */
85
+ export interface ChannelTransportRouterOptions {
86
+ /** Session id the router scopes channel_subscribe frames against. */
87
+ readonly sessionId: string;
88
+ /** App (tenant) id paired with sessionId on the subscribe frame. */
89
+ readonly appId: string;
90
+ /**
91
+ * Allowlist of `source.tool` names this server can subscribe-for
92
+ * over WS. From `bootstrap.streamWebSocketLocalTools`. Absent or
93
+ * empty ⇒ every channel routes through the iframe-poll fallback.
94
+ */
95
+ readonly streamWebSocketLocalTools?: readonly string[];
96
+ /** Outbound WS sender. */
97
+ readonly send: WsSender;
98
+ /** Iframe → parent `tools/call` proxy. */
99
+ readonly toolsCall: ToolsCallInvoker;
100
+ /** StreamBus the router emits envelopes onto. */
101
+ readonly streamBus: StreamBus;
102
+ /**
103
+ * Default poll cadence (ms) for the iframe-polling transport.
104
+ * Overridden per-channel via `streamSpec[name].source.pollIntervalMs`
105
+ * when authored. Falls back to {@link DEFAULT_IFRAME_POLL_INTERVAL_MS}
106
+ * when neither the spec nor the option supplies one.
107
+ */
108
+ readonly defaultPollIntervalMs?: number;
109
+ /**
110
+ * Observability sink — fires once per transport pick + once per
111
+ * disconnect/reconnect-fallback transition. Optional; absent ⇒
112
+ * silent.
113
+ */
114
+ readonly onObserve?: (event: ChannelTransportEvent) => void;
115
+ }
116
+ /**
117
+ * Observability events the router emits. Hosts that wire `onObserve`
118
+ * see these alongside the renderer's other observability events
119
+ * (`subscribe-failed`, `wired-tool-invoked`, …). The shape stays
120
+ * narrow + non-breaking so future events can extend the union.
121
+ */
122
+ export type ChannelTransportEvent = {
123
+ readonly kind: 'channel-transport-picked';
124
+ readonly stackItemId: string;
125
+ readonly channelName: string;
126
+ readonly transport: 'ws' | 'poll';
127
+ } | {
128
+ readonly kind: 'channel-transport-fallback';
129
+ readonly stackItemId: string;
130
+ readonly channelName: string;
131
+ readonly reason: 'ws-disconnect' | 'channel-not-local';
132
+ } | {
133
+ readonly kind: 'channel-transport-resubscribed';
134
+ readonly stackItemId: string;
135
+ readonly channelName: string;
136
+ };
137
+ /**
138
+ * Router handle returned by {@link createChannelTransportRouter}.
139
+ */
140
+ export interface ChannelTransportRouter {
141
+ /**
142
+ * Apply a new stack item's `streamSpec`. Idempotent against
143
+ * re-applying the same shape (no churn). Channels added/removed
144
+ * across renders fire transport pick / teardown accordingly.
145
+ *
146
+ * The legacy `data` frame path on `streamSpec[ch]` entries
147
+ * WITHOUT `source.tool` is unaffected — the router only manages
148
+ * the source-fed subset.
149
+ */
150
+ readonly applyStackItem: (item: {
151
+ readonly stackItemId: string;
152
+ readonly streamSpec?: StreamSpec;
153
+ }) => void;
154
+ /**
155
+ * Forward a single inbound WS frame. The router consumes
156
+ * `channel_payload` + `channel_error` types and ignores everything
157
+ * else. Returns `true` iff the frame was consumed (so the caller
158
+ * can short-circuit), `false` otherwise.
159
+ */
160
+ readonly handleWsFrame: (msg: WebSocketMessage) => boolean;
161
+ /**
162
+ * Notify the router of a WS connection status transition. Used to:
163
+ * - On `'disconnected'` / `'reconnecting'`: start polling fallback
164
+ * for every WS-bound channel (no delay).
165
+ * - On `'connected'` AFTER a prior disconnect: re-send
166
+ * `channel_subscribe` for every WS-bound channel.
167
+ */
168
+ readonly onWsStatusChange: (status: 'connecting' | 'connected' | 'disconnected' | 'reconnecting') => void;
169
+ /**
170
+ * Tear down every active subscription + timer. Called on
171
+ * `triadWiring.teardown`.
172
+ */
173
+ readonly dispose: () => void;
174
+ }
175
+ /**
176
+ * Factory.
177
+ *
178
+ * Per-channel transport routing — owns the `(stackItemId, channelName)`
179
+ * registry, the WS-vs-poll decision per channel, and the cross-state
180
+ * transitions (disconnect → poll fallback, reconnect → re-subscribe).
181
+ */
182
+ export declare function createChannelTransportRouter(opts: ChannelTransportRouterOptions): ChannelTransportRouter;