@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,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `globalThis.__ggui__` registry setup — the one seam generated
|
|
3
|
+
* component code reads from.
|
|
4
|
+
*
|
|
5
|
+
* The renderer bundles React + ReactDOM + `@ggui-ai/wire` +
|
|
6
|
+
* `@ggui-ai/design` INSIDE itself, then exposes those runtime
|
|
7
|
+
* values on a single
|
|
8
|
+
* `globalThis.__ggui__` object so generated ESM modules can
|
|
9
|
+
* destructure them via the data-URL shim rewrite (see
|
|
10
|
+
* `@ggui-ai/design/rendering/rewrite-imports.ts`).
|
|
11
|
+
*
|
|
12
|
+
* Shape
|
|
13
|
+
* -----
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* globalThis.__ggui__ = {
|
|
17
|
+
* react, reactDom,
|
|
18
|
+
* primitives, components, compositions, interact,
|
|
19
|
+
* wire,
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Keys match the `gguiKey` vocabulary the design package's
|
|
24
|
+
* `rewriteImports()` resolves against (`'react'`, `'reactDom'`,
|
|
25
|
+
* `'primitives'`, `'components'`, `'compositions'`, `'interact'`,
|
|
26
|
+
* `'wire'`). Legacy window globals (`__REACT`, `__GGUI_PRIMITIVES`,
|
|
27
|
+
* …) are ALSO populated so data-URL shims that still fall back to
|
|
28
|
+
* the legacy path keep working.
|
|
29
|
+
*
|
|
30
|
+
* TOCTOU footgun
|
|
31
|
+
* --------------
|
|
32
|
+
*
|
|
33
|
+
* Generated code's data-URL ESM module imports resolve synchronously
|
|
34
|
+
* (the shim runs `globalThis.__ggui__[key]` at import time). That
|
|
35
|
+
* means the registry MUST be populated BEFORE any `loadModule()` call
|
|
36
|
+
* evaluates generated code. The boot orchestration in
|
|
37
|
+
* `runtime.ts::bootSequence` enforces this by calling
|
|
38
|
+
* `installGlobalRegistry()` BEFORE the subscribe ack's first stack
|
|
39
|
+
* item renders. Race-free: Node/browser event loop is single-threaded
|
|
40
|
+
* and no other code runs between `installGlobalRegistry()` and the
|
|
41
|
+
* first stack-item render invocation.
|
|
42
|
+
*/
|
|
43
|
+
import type { Context } from 'react';
|
|
44
|
+
/**
|
|
45
|
+
* Module-namespace-compatible shape. Accepts both:
|
|
46
|
+
* - ES module namespace objects returned by `import(...)` (the
|
|
47
|
+
* production case; `typeof import('react')` has typed exports).
|
|
48
|
+
* - Plain records of string keys → unknown (the test case; fakes
|
|
49
|
+
* stand in for the real module).
|
|
50
|
+
*
|
|
51
|
+
* The mapped-type widening is what frees the real module-namespace
|
|
52
|
+
* types from needing a runtime cast.
|
|
53
|
+
*/
|
|
54
|
+
export type ModuleNamespace = {
|
|
55
|
+
readonly [key: string]: unknown;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Gadget-package registry shape — package name → that package's whole
|
|
59
|
+
* loaded module namespace. The runtime `gadgets` slot (GG.8.2). The
|
|
60
|
+
* per-package data-URL shims the rewriter substitutes for gadget
|
|
61
|
+
* imports read `__ggui__.gadgets[package][export]` at call/render time.
|
|
62
|
+
*/
|
|
63
|
+
export type GadgetPackageRegistry = {
|
|
64
|
+
readonly [packageName: string]: ModuleNamespace;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Internal type for the object installed at `globalThis.__ggui__`.
|
|
68
|
+
*
|
|
69
|
+
* Intentionally NOT exported from the package's public `index.ts` —
|
|
70
|
+
* generated code doesn't type-check against it (it reads via
|
|
71
|
+
* data-URL shims at runtime). Keeping the type local to the renderer
|
|
72
|
+
* avoids leaking a public contract we'd have to version.
|
|
73
|
+
*
|
|
74
|
+
* Module shapes (`ReactModule` / `ReactDomModule` / design-layer
|
|
75
|
+
* modules) are `readonly Record<string, unknown>` because the
|
|
76
|
+
* generated code's data-URL shims pluck named exports by string key;
|
|
77
|
+
* exhaustively typing every React / design-system export would
|
|
78
|
+
* duplicate the upstream package contracts with zero TS benefit for
|
|
79
|
+
* generated code (which is eval'd, not type-checked). This is NOT the
|
|
80
|
+
* `Record<string, unknown>` anti-pattern — the shape IS unbounded by
|
|
81
|
+
* construction (the whole point is: every import the shim might ask
|
|
82
|
+
* for is present).
|
|
83
|
+
*/
|
|
84
|
+
export interface GguiGlobalRegistry {
|
|
85
|
+
readonly react: ModuleNamespace;
|
|
86
|
+
readonly reactDom: ModuleNamespace;
|
|
87
|
+
readonly primitives: ModuleNamespace;
|
|
88
|
+
readonly components: ModuleNamespace;
|
|
89
|
+
readonly compositions: ModuleNamespace;
|
|
90
|
+
readonly interact: ModuleNamespace;
|
|
91
|
+
readonly wire: ModuleNamespace;
|
|
92
|
+
/**
|
|
93
|
+
* Design tokens module (`@ggui-ai/design/tokens`). Static design-system
|
|
94
|
+
* constants — color scales, spacing, animation presets, typography
|
|
95
|
+
* styles. Generated component code imports from this path via the
|
|
96
|
+
* data-URL shim, which reads from `globalThis.__ggui__.tokens` at
|
|
97
|
+
* load time. 2026-05-15 audit fix.
|
|
98
|
+
*/
|
|
99
|
+
readonly tokens: ModuleNamespace;
|
|
100
|
+
/**
|
|
101
|
+
* Gadget-package registry. Read at call/render time by the
|
|
102
|
+
* per-package data-URL shims that
|
|
103
|
+
* `@ggui-ai/design/rendering/rewrite-imports.ts` substitutes for
|
|
104
|
+
* `@ggui-ai/gadgets` AND each operator-registered gadget package
|
|
105
|
+
* specifier.
|
|
106
|
+
*
|
|
107
|
+
* **Keyed by PACKAGE NAME** (GG.8.2 — e.g. `'@ggui-ai/gadgets'`,
|
|
108
|
+
* `'@ggui-samples/gadget-leaflet'`), each slot holding that
|
|
109
|
+
* package's whole loaded module namespace. The shim for
|
|
110
|
+
* `import { useLeafletMap } from '@ggui-samples/gadget-leaflet'`
|
|
111
|
+
* resolves to `gadgets['@ggui-samples/gadget-leaflet'].useLeafletMap`
|
|
112
|
+
* — package-keyed so two packages exporting the same name never
|
|
113
|
+
* collide.
|
|
114
|
+
*
|
|
115
|
+
* Populated at boot with:
|
|
116
|
+
* - The whole `@ggui-ai/gadgets` STDLIB namespace under
|
|
117
|
+
* `gadgets['@ggui-ai/gadgets']` (the seed).
|
|
118
|
+
* - One namespace per registered 3rd-party package, keyed by
|
|
119
|
+
* package name, from `loadGadgetRegistry()`'s dynamic imports.
|
|
120
|
+
*
|
|
121
|
+
* Why an open record (no exhaustive type): the operator-registered
|
|
122
|
+
* catalog is dynamic; static typing would force a code-gen step on
|
|
123
|
+
* every operator config change.
|
|
124
|
+
*/
|
|
125
|
+
readonly gadgets: GadgetPackageRegistry;
|
|
126
|
+
/**
|
|
127
|
+
* Public env values. Populated by the iframe-runtime's boot
|
|
128
|
+
* sequence from `bootstrap.publicEnv` — the union-filtered subset
|
|
129
|
+
* of `App.publicEnv` declared wrappers' `requires` cover.
|
|
130
|
+
* Read at hook-mount time by `getPublicEnv(key)` from
|
|
131
|
+
* `@ggui-ai/gadgets`.
|
|
132
|
+
*
|
|
133
|
+
* Empty record when the bootstrap has no `publicEnv` field (no
|
|
134
|
+
* wrappers required keys). The hook will throw on access to any
|
|
135
|
+
* key, which is the correct fail-fast behavior — the operator
|
|
136
|
+
* either forgot to configure publicEnv OR the wrapper didn't
|
|
137
|
+
* declare `requires`.
|
|
138
|
+
*/
|
|
139
|
+
readonly publicEnv: Readonly<Record<string, string>>;
|
|
140
|
+
/**
|
|
141
|
+
* React Context registry. Populated by
|
|
142
|
+
* `runtime.ts::installContextRegistry()` during boot from the
|
|
143
|
+
* bootstrap's `contextSlots` field. Keyed by PascalCase
|
|
144
|
+
* `contextName` (e.g. `'CurrentStepContext'`). The boilerplate
|
|
145
|
+
* destructures these so the LLM-authored component can wrap a tree
|
|
146
|
+
* in `<CurrentStepContext.Provider value={…}>` and the runtime
|
|
147
|
+
* observer reads values back via `useContext`.
|
|
148
|
+
*
|
|
149
|
+
* Mutable across re-mounts: on a second `bootSelfContained` call,
|
|
150
|
+
* the registry is REUSED (entries are keyed by contextName and
|
|
151
|
+
* skipped on re-create) so the LLM's destructured Context
|
|
152
|
+
* references stay live across re-mounts. Only NEW context names
|
|
153
|
+
* append; existing ones are never replaced.
|
|
154
|
+
*/
|
|
155
|
+
readonly contexts: GguiContextRegistry;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* React Context registry installed at `globalThis.__ggui__.contexts`.
|
|
159
|
+
* Open-ended record — the runtime adds one entry per declared
|
|
160
|
+
* `contextSpec` slot at boot, and the boilerplate destructures by
|
|
161
|
+
* `contextName`. Values are typed loosely (`Context<unknown>`) at the
|
|
162
|
+
* registry boundary because each slot's value type is schema-derived;
|
|
163
|
+
* the boilerplate's destructure cast narrows for the LLM's component.
|
|
164
|
+
*
|
|
165
|
+
* Mutable to allow per-slot append on re-mount when a new
|
|
166
|
+
* contextSpec adds a slot the previous mount didn't declare. Existing
|
|
167
|
+
* entries are NEVER replaced — the same Context reference must stay
|
|
168
|
+
* stable across re-mounts so the LLM's destructured component code
|
|
169
|
+
* keeps working.
|
|
170
|
+
*/
|
|
171
|
+
export interface GguiContextRegistry {
|
|
172
|
+
[contextName: string]: Context<unknown>;
|
|
173
|
+
}
|
|
174
|
+
declare global {
|
|
175
|
+
var __ggui__: GguiGlobalRegistry | undefined;
|
|
176
|
+
}
|
|
177
|
+
export interface InstallGlobalRegistryOptions {
|
|
178
|
+
readonly react: ModuleNamespace;
|
|
179
|
+
readonly reactDom: ModuleNamespace;
|
|
180
|
+
readonly primitives: ModuleNamespace;
|
|
181
|
+
readonly components: ModuleNamespace;
|
|
182
|
+
readonly compositions: ModuleNamespace;
|
|
183
|
+
readonly interact: ModuleNamespace;
|
|
184
|
+
readonly wire: ModuleNamespace;
|
|
185
|
+
readonly tokens: ModuleNamespace;
|
|
186
|
+
/**
|
|
187
|
+
* Gadget-package registry — package name → that package's loaded
|
|
188
|
+
* module namespace. Production callers compose this from the
|
|
189
|
+
* `@ggui-ai/gadgets` STDLIB seed plus each operator-registered
|
|
190
|
+
* package's dynamic import (see `loadGadgetRegistry`). Tests pass
|
|
191
|
+
* fakes.
|
|
192
|
+
*
|
|
193
|
+
* Optional: callers may omit this and the registry installs an
|
|
194
|
+
* empty record. Generated code that imports a gadget export then
|
|
195
|
+
* fails at the iframe's ESM module-eval (the rewriter emits a
|
|
196
|
+
* per-package shim only for registered packages). Operators who
|
|
197
|
+
* don't register any 3rd-party packages should leave
|
|
198
|
+
* `clientCapabilities.gadgets` off their contracts.
|
|
199
|
+
*/
|
|
200
|
+
readonly gadgets?: GadgetPackageRegistry;
|
|
201
|
+
/**
|
|
202
|
+
* Public env values. Forwarded from `bootstrap.publicEnv`
|
|
203
|
+
* verbatim — the server-side projection already filtered down to
|
|
204
|
+
* the union of declared wrappers' `requires`. Wrappers read via
|
|
205
|
+
* `getPublicEnv(key)` from `@ggui-ai/gadgets`.
|
|
206
|
+
*
|
|
207
|
+
* Optional: callers may omit this and the registry installs an
|
|
208
|
+
* empty record (wrappers that need
|
|
209
|
+
* env values will throw at hook-mount with a clear "not provided"
|
|
210
|
+
* message, which is the right fail-fast behavior).
|
|
211
|
+
*/
|
|
212
|
+
readonly publicEnv?: Readonly<Record<string, string>>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Install the `globalThis.__ggui__` registry. Idempotent — calling
|
|
216
|
+
* twice replaces the module-slot references wholesale; the `contexts`
|
|
217
|
+
* sub-object is the ONE thing reused across re-installs so the LLM's
|
|
218
|
+
* destructured Context references stay stable. The boot sequence calls
|
|
219
|
+
* this exactly once.
|
|
220
|
+
*
|
|
221
|
+
* Caller supplies every module reference. The renderer's `runtime.ts`
|
|
222
|
+
* does the imports at module-load time and passes the resolved
|
|
223
|
+
* namespaces in; tests pass fakes. Dependency injection rather than
|
|
224
|
+
* hard-coded imports keeps globals.ts free of the heavy React +
|
|
225
|
+
* design + wire module graph — makes unit testing a 200-line file
|
|
226
|
+
* rather than a 150KB bundle slice.
|
|
227
|
+
*/
|
|
228
|
+
export declare function installGlobalRegistry(opts: InstallGlobalRegistryOptions, target?: typeof globalThis): GguiGlobalRegistry;
|
|
229
|
+
/**
|
|
230
|
+
* Read the current registry. Returns `undefined` when
|
|
231
|
+
* `installGlobalRegistry()` has not yet been called — callers MUST
|
|
232
|
+
* handle the absence honestly rather than assuming.
|
|
233
|
+
*/
|
|
234
|
+
export declare function getGlobalRegistry(target?: typeof globalThis): GguiGlobalRegistry | undefined;
|
package/dist/globals.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Registry install
|
|
3
|
+
// =============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* The design package's `rewrite-imports.ts` falls back to legacy
|
|
6
|
+
* window globals (`window.__REACT`, `window.__GGUI_PRIMITIVES`, …)
|
|
7
|
+
* when `globalThis.__ggui__[key]` is absent. We populate BOTH paths
|
|
8
|
+
* so any in-flight cached generated code (which pre-dated the
|
|
9
|
+
* __ggui__ consolidation) keeps resolving.
|
|
10
|
+
*
|
|
11
|
+
* Pre-rebuilt design-package fallback — matches the list today's
|
|
12
|
+
* `ReactComponentRenderer.tsx` sets in the host SDK path.
|
|
13
|
+
*/
|
|
14
|
+
const LEGACY_GLOBAL_KEYS = {
|
|
15
|
+
react: '__REACT',
|
|
16
|
+
primitives: '__GGUI_PRIMITIVES',
|
|
17
|
+
components: '__GGUI_COMPONENTS',
|
|
18
|
+
compositions: '__GGUI_COMPOSITIONS',
|
|
19
|
+
interact: '__GGUI_INTERACT',
|
|
20
|
+
tokens: '__GGUI_TOKENS',
|
|
21
|
+
// Symmetric with the other layer globals. The rewriter's data-URL
|
|
22
|
+
// shim reads from `globalThis.__ggui__.gadgets` directly (no
|
|
23
|
+
// legacy fallback in the shim), but populating the legacy global
|
|
24
|
+
// keeps console debugging
|
|
25
|
+
// (`window.__GGUI_CLIENT_LIBRARIES.useGeolocation`) working
|
|
26
|
+
// uniformly with the other layers.
|
|
27
|
+
gadgets: '__GGUI_CLIENT_LIBRARIES',
|
|
28
|
+
// Symmetric with the other registry layers. The rewriter's
|
|
29
|
+
// `getPublicEnv` shim reads `globalThis.__ggui__.publicEnv`
|
|
30
|
+
// directly; the legacy global is for console-debugging parity
|
|
31
|
+
// (`window.__GGUI_PUBLIC_ENV.GGUI_PUBLIC_APP_FOO`).
|
|
32
|
+
publicEnv: '__GGUI_PUBLIC_ENV',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Install the `globalThis.__ggui__` registry. Idempotent — calling
|
|
36
|
+
* twice replaces the module-slot references wholesale; the `contexts`
|
|
37
|
+
* sub-object is the ONE thing reused across re-installs so the LLM's
|
|
38
|
+
* destructured Context references stay stable. The boot sequence calls
|
|
39
|
+
* this exactly once.
|
|
40
|
+
*
|
|
41
|
+
* Caller supplies every module reference. The renderer's `runtime.ts`
|
|
42
|
+
* does the imports at module-load time and passes the resolved
|
|
43
|
+
* namespaces in; tests pass fakes. Dependency injection rather than
|
|
44
|
+
* hard-coded imports keeps globals.ts free of the heavy React +
|
|
45
|
+
* design + wire module graph — makes unit testing a 200-line file
|
|
46
|
+
* rather than a 150KB bundle slice.
|
|
47
|
+
*/
|
|
48
|
+
export function installGlobalRegistry(opts, target = globalThis) {
|
|
49
|
+
// Idempotency on re-mount: the LLM's component holds Context
|
|
50
|
+
// references it destructured from `globalThis.__ggui__.contexts` at
|
|
51
|
+
// first boot. Re-creating the contexts record on every re-mount
|
|
52
|
+
// would invalidate those references. Reuse the existing `contexts`
|
|
53
|
+
// sub-object when present; only the module/wire references swap.
|
|
54
|
+
const existing = target.__ggui__;
|
|
55
|
+
const contexts = existing?.contexts ?? {};
|
|
56
|
+
const registry = {
|
|
57
|
+
react: opts.react,
|
|
58
|
+
reactDom: opts.reactDom,
|
|
59
|
+
primitives: opts.primitives,
|
|
60
|
+
components: opts.components,
|
|
61
|
+
compositions: opts.compositions,
|
|
62
|
+
interact: opts.interact,
|
|
63
|
+
wire: opts.wire,
|
|
64
|
+
tokens: opts.tokens,
|
|
65
|
+
// Gadget hooks (STDLIB + registered wrappers). Defaults to an
|
|
66
|
+
// empty record so callers who don't bind libraries don't have to
|
|
67
|
+
// thread the slot — generated code that references a hook in
|
|
68
|
+
// that case fails at the LLM's `useFoo()` call (which is
|
|
69
|
+
// correct: there's no impl to call).
|
|
70
|
+
gadgets: opts.gadgets ?? {},
|
|
71
|
+
// Public env values for wrapper hooks to read via
|
|
72
|
+
// `getPublicEnv(key)`. Defaults to an empty record; wrappers
|
|
73
|
+
// that need values throw clearly at hook-mount.
|
|
74
|
+
publicEnv: opts.publicEnv ?? {},
|
|
75
|
+
contexts,
|
|
76
|
+
};
|
|
77
|
+
// Install the consolidated registry.
|
|
78
|
+
target.__ggui__ = registry;
|
|
79
|
+
// Also populate the legacy window globals — today's
|
|
80
|
+
// `rewrite-imports.ts` shim path falls back to these when
|
|
81
|
+
// `globalThis.__ggui__[key]` misses. Setting both keeps any
|
|
82
|
+
// pre-registry cached generated code working.
|
|
83
|
+
const win = target;
|
|
84
|
+
win[LEGACY_GLOBAL_KEYS.react] = opts.react;
|
|
85
|
+
win[LEGACY_GLOBAL_KEYS.primitives] = opts.primitives;
|
|
86
|
+
win[LEGACY_GLOBAL_KEYS.components] = opts.components;
|
|
87
|
+
win[LEGACY_GLOBAL_KEYS.compositions] = opts.compositions;
|
|
88
|
+
win[LEGACY_GLOBAL_KEYS.interact] = opts.interact;
|
|
89
|
+
win[LEGACY_GLOBAL_KEYS.tokens] = opts.tokens;
|
|
90
|
+
win[LEGACY_GLOBAL_KEYS.gadgets] = registry.gadgets;
|
|
91
|
+
win[LEGACY_GLOBAL_KEYS.publicEnv] = registry.publicEnv;
|
|
92
|
+
return registry;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Read the current registry. Returns `undefined` when
|
|
96
|
+
* `installGlobalRegistry()` has not yet been called — callers MUST
|
|
97
|
+
* handle the absence honestly rather than assuming.
|
|
98
|
+
*/
|
|
99
|
+
export function getGlobalRegistry(target = globalThis) {
|
|
100
|
+
return target.__ggui__;
|
|
101
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-context emitter.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Hold the latest `HostContextProjection` the runtime has captured.
|
|
7
|
+
* Seeded from the iframe's `ui/initialize` response (Reading-A
|
|
8
|
+
* path, via `parseBootstrapFromUiInitialize`); updated on every
|
|
9
|
+
* `ui/notifications/host-context-changed` postMessage from the host.
|
|
10
|
+
* 2. Echo each fresh value as a `type: 'host_context_observed'`
|
|
11
|
+
* WebSocket envelope so the server can persist it on
|
|
12
|
+
* `SessionRecord.hostContext` for agent visibility.
|
|
13
|
+
* 3. Suppress no-op re-emissions (via deep-equality on the projection
|
|
14
|
+
* shape) — avoids server-side write traffic when a notification
|
|
15
|
+
* arrives but no projection-visible field actually changed (e.g.,
|
|
16
|
+
* the host's `theme` updated but ggui doesn't project theme).
|
|
17
|
+
*
|
|
18
|
+
* Lifecycle:
|
|
19
|
+
*
|
|
20
|
+
* - Initial seed: `seed(projection, sessionId)` called from
|
|
21
|
+
* `bootProduction` right after the WS transport is attached and we
|
|
22
|
+
* know the initial projection from `parseBootstrap.hostContext`.
|
|
23
|
+
* - Live updates: `attachListener()` installs a `window`-scoped
|
|
24
|
+
* `message` listener for the spec-defined notification method
|
|
25
|
+
* `'ui/notifications/host-context-changed'`. The notification's
|
|
26
|
+
* `params` carry a partial `McpUiHostContext`; we merge into the
|
|
27
|
+
* held projection and emit if changed.
|
|
28
|
+
* - Teardown: `detach()` removes the listener and clears state.
|
|
29
|
+
* Used by tests + future host-switch flows.
|
|
30
|
+
*
|
|
31
|
+
* Why a dedicated module:
|
|
32
|
+
*
|
|
33
|
+
* - `runtime.ts` is already ~3100 lines; adding another postMessage
|
|
34
|
+
* listener + state slot bloats it.
|
|
35
|
+
* - The emission policy (seed + change-merge + dedupe) is testable in
|
|
36
|
+
* isolation without the WS transport / bootstrap parse / DOM mount.
|
|
37
|
+
* - Future widening (echo more fields, debounce burst changes, etc.)
|
|
38
|
+
* happens in one place.
|
|
39
|
+
*/
|
|
40
|
+
import { type HostContextObservedPayload, type HostContextProjection } from '@ggui-ai/protocol';
|
|
41
|
+
/**
|
|
42
|
+
* Function the emitter calls to ship a `host_context_observed` envelope.
|
|
43
|
+
* Wired by `bootProduction` to the live WS transport's `send` —
|
|
44
|
+
* typically `(msg) => handle.handle.send(msg)` after subscribe ack.
|
|
45
|
+
*
|
|
46
|
+
* The emitter does NOT buffer pre-WS-up emissions: `seed()` MUST only
|
|
47
|
+
* be called once the send seam is live. If seed is called before send
|
|
48
|
+
* is wired, the initial echo is silently dropped (the WS hasn't been
|
|
49
|
+
* established; the server isn't listening). This matches the
|
|
50
|
+
* fire-and-forget posture of the protocol — agent visibility is
|
|
51
|
+
* eventually-consistent, never blocking.
|
|
52
|
+
*/
|
|
53
|
+
export type HostContextSendFn = (msg: {
|
|
54
|
+
readonly type: 'host_context_observed';
|
|
55
|
+
readonly payload: HostContextObservedPayload;
|
|
56
|
+
}) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Seed the emitter with the initial projection captured at boot time +
|
|
59
|
+
* the live WS send seam. Idempotent: a re-seed (e.g. on reconnect with
|
|
60
|
+
* the same projection) suppresses the duplicate emission via the
|
|
61
|
+
* equality check.
|
|
62
|
+
*
|
|
63
|
+
* Re-seeding with a DIFFERENT `sessionId` is undefined behavior; the
|
|
64
|
+
* iframe-runtime is one-session-per-mount by construction. Tests
|
|
65
|
+
* exercising multi-session flows MUST call `detach()` between seeds.
|
|
66
|
+
*/
|
|
67
|
+
export declare function seed(args: {
|
|
68
|
+
readonly sessionId: string;
|
|
69
|
+
readonly send: HostContextSendFn;
|
|
70
|
+
readonly initial: HostContextProjection;
|
|
71
|
+
}): void;
|
|
72
|
+
/**
|
|
73
|
+
* Install the `host-context-changed` notification listener on the
|
|
74
|
+
* iframe `window`. Safe to call multiple times — duplicate listeners
|
|
75
|
+
* are deduped via `state.listener !== null` guard.
|
|
76
|
+
*
|
|
77
|
+
* Detaches when `detach()` is called or when the module is torn down.
|
|
78
|
+
*/
|
|
79
|
+
export declare function attachListener(targetWindow?: Window): void;
|
|
80
|
+
/**
|
|
81
|
+
* Tear down: remove the listener, clear module state. Called from
|
|
82
|
+
* unit tests between scenarios and from the future host-switch flow.
|
|
83
|
+
*/
|
|
84
|
+
export declare function detach(targetWindow?: Window): void;
|
|
85
|
+
/**
|
|
86
|
+
* Test-only read of the current held projection. Production code
|
|
87
|
+
* MUST NOT depend on this; the emitter is fire-and-forget by design.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export declare function _peekCurrent(): HostContextProjection | undefined;
|
|
91
|
+
export declare function subscribeLocal(listener: (projection: HostContextProjection) => void): () => void;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-context emitter.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Hold the latest `HostContextProjection` the runtime has captured.
|
|
7
|
+
* Seeded from the iframe's `ui/initialize` response (Reading-A
|
|
8
|
+
* path, via `parseBootstrapFromUiInitialize`); updated on every
|
|
9
|
+
* `ui/notifications/host-context-changed` postMessage from the host.
|
|
10
|
+
* 2. Echo each fresh value as a `type: 'host_context_observed'`
|
|
11
|
+
* WebSocket envelope so the server can persist it on
|
|
12
|
+
* `SessionRecord.hostContext` for agent visibility.
|
|
13
|
+
* 3. Suppress no-op re-emissions (via deep-equality on the projection
|
|
14
|
+
* shape) — avoids server-side write traffic when a notification
|
|
15
|
+
* arrives but no projection-visible field actually changed (e.g.,
|
|
16
|
+
* the host's `theme` updated but ggui doesn't project theme).
|
|
17
|
+
*
|
|
18
|
+
* Lifecycle:
|
|
19
|
+
*
|
|
20
|
+
* - Initial seed: `seed(projection, sessionId)` called from
|
|
21
|
+
* `bootProduction` right after the WS transport is attached and we
|
|
22
|
+
* know the initial projection from `parseBootstrap.hostContext`.
|
|
23
|
+
* - Live updates: `attachListener()` installs a `window`-scoped
|
|
24
|
+
* `message` listener for the spec-defined notification method
|
|
25
|
+
* `'ui/notifications/host-context-changed'`. The notification's
|
|
26
|
+
* `params` carry a partial `McpUiHostContext`; we merge into the
|
|
27
|
+
* held projection and emit if changed.
|
|
28
|
+
* - Teardown: `detach()` removes the listener and clears state.
|
|
29
|
+
* Used by tests + future host-switch flows.
|
|
30
|
+
*
|
|
31
|
+
* Why a dedicated module:
|
|
32
|
+
*
|
|
33
|
+
* - `runtime.ts` is already ~3100 lines; adding another postMessage
|
|
34
|
+
* listener + state slot bloats it.
|
|
35
|
+
* - The emission policy (seed + change-merge + dedupe) is testable in
|
|
36
|
+
* isolation without the WS transport / bootstrap parse / DOM mount.
|
|
37
|
+
* - Future widening (echo more fields, debounce burst changes, etc.)
|
|
38
|
+
* happens in one place.
|
|
39
|
+
*/
|
|
40
|
+
import { hostContextProjectionsEqual, projectHostContext, } from '@ggui-ai/protocol';
|
|
41
|
+
let state = null;
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Public surface
|
|
44
|
+
// =============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Seed the emitter with the initial projection captured at boot time +
|
|
47
|
+
* the live WS send seam. Idempotent: a re-seed (e.g. on reconnect with
|
|
48
|
+
* the same projection) suppresses the duplicate emission via the
|
|
49
|
+
* equality check.
|
|
50
|
+
*
|
|
51
|
+
* Re-seeding with a DIFFERENT `sessionId` is undefined behavior; the
|
|
52
|
+
* iframe-runtime is one-session-per-mount by construction. Tests
|
|
53
|
+
* exercising multi-session flows MUST call `detach()` between seeds.
|
|
54
|
+
*/
|
|
55
|
+
export function seed(args) {
|
|
56
|
+
// First seed — record state + emit initial.
|
|
57
|
+
if (state === null) {
|
|
58
|
+
state = {
|
|
59
|
+
sessionId: args.sessionId,
|
|
60
|
+
send: args.send,
|
|
61
|
+
current: args.initial,
|
|
62
|
+
listener: null,
|
|
63
|
+
};
|
|
64
|
+
emit();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Re-seed — only emit if the projection actually changed. Keep the
|
|
68
|
+
// existing listener registration if any.
|
|
69
|
+
if (!hostContextProjectionsEqual(state.current, args.initial)) {
|
|
70
|
+
state.current = args.initial;
|
|
71
|
+
emit();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Install the `host-context-changed` notification listener on the
|
|
76
|
+
* iframe `window`. Safe to call multiple times — duplicate listeners
|
|
77
|
+
* are deduped via `state.listener !== null` guard.
|
|
78
|
+
*
|
|
79
|
+
* Detaches when `detach()` is called or when the module is torn down.
|
|
80
|
+
*/
|
|
81
|
+
export function attachListener(targetWindow = window) {
|
|
82
|
+
if (state === null) {
|
|
83
|
+
// Not yet seeded — listener can't usefully fire (no send seam).
|
|
84
|
+
// Caller should `seed()` first; this defensive bail keeps the
|
|
85
|
+
// surface idempotent rather than throwing.
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (state.listener !== null)
|
|
89
|
+
return;
|
|
90
|
+
const listener = (ev) => {
|
|
91
|
+
handleHostContextChangedMessage(ev.data);
|
|
92
|
+
};
|
|
93
|
+
state.listener = listener;
|
|
94
|
+
targetWindow.addEventListener('message', listener);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Tear down: remove the listener, clear module state. Called from
|
|
98
|
+
* unit tests between scenarios and from the future host-switch flow.
|
|
99
|
+
*/
|
|
100
|
+
export function detach(targetWindow = window) {
|
|
101
|
+
if (state !== null && state.listener !== null) {
|
|
102
|
+
targetWindow.removeEventListener('message', state.listener);
|
|
103
|
+
}
|
|
104
|
+
state = null;
|
|
105
|
+
// Reset the local subscriber set too — module-level state is the
|
|
106
|
+
// singleton's identity, and tests need clean isolation between
|
|
107
|
+
// scenarios. Without this, a subscriber registered in test 1 leaks
|
|
108
|
+
// into test 2's seed and fires against a stale closure.
|
|
109
|
+
localSubscribers.clear();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Test-only read of the current held projection. Production code
|
|
113
|
+
* MUST NOT depend on this; the emitter is fire-and-forget by design.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export function _peekCurrent() {
|
|
117
|
+
return state?.current;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Subscribe to local projection updates so
|
|
121
|
+
* iframe-side consumers (canvas mount handle's display-mode state)
|
|
122
|
+
* can react to `host-context-changed` notifications without re-
|
|
123
|
+
* parsing the postMessage envelope themselves.
|
|
124
|
+
*
|
|
125
|
+
* Fires:
|
|
126
|
+
* - Immediately on subscribe IF `state` is already seeded (so the
|
|
127
|
+
* subscriber doesn't have to also read `parsed.hostContext`).
|
|
128
|
+
* - On every `host-context-changed` notification that updates
|
|
129
|
+
* `state.current` (post-projection, post-equality-check).
|
|
130
|
+
*
|
|
131
|
+
* Returns an unsubscribe function. Multi-subscriber by design —
|
|
132
|
+
* tests + production canvas mount can coexist.
|
|
133
|
+
*/
|
|
134
|
+
const localSubscribers = new Set();
|
|
135
|
+
export function subscribeLocal(listener) {
|
|
136
|
+
localSubscribers.add(listener);
|
|
137
|
+
// Fire immediately if seed has already happened.
|
|
138
|
+
if (state !== null)
|
|
139
|
+
listener(state.current);
|
|
140
|
+
return () => {
|
|
141
|
+
localSubscribers.delete(listener);
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Internals
|
|
146
|
+
// =============================================================================
|
|
147
|
+
function isPlainObject(value) {
|
|
148
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Process an incoming postMessage payload. Filters to JSON-RPC
|
|
152
|
+
* notifications with method `ui/notifications/host-context-changed`;
|
|
153
|
+
* everything else is ignored. The notification's `params` is a partial
|
|
154
|
+
* `McpUiHostContext` — we project and merge into the held value.
|
|
155
|
+
*
|
|
156
|
+
* Exported via `attachListener` indirection; isolated here so it's
|
|
157
|
+
* unit-testable without dispatching real `MessageEvent`s.
|
|
158
|
+
*/
|
|
159
|
+
function handleHostContextChangedMessage(raw) {
|
|
160
|
+
if (state === null)
|
|
161
|
+
return;
|
|
162
|
+
if (!isPlainObject(raw))
|
|
163
|
+
return;
|
|
164
|
+
if (raw['method'] !== 'ui/notifications/host-context-changed')
|
|
165
|
+
return;
|
|
166
|
+
const params = raw['params'];
|
|
167
|
+
if (!isPlainObject(params))
|
|
168
|
+
return;
|
|
169
|
+
// Spec: the notification's params carry a partial McpUiHostContext.
|
|
170
|
+
// Project it (defensive — malformed inputs drop to undefined fields)
|
|
171
|
+
// and merge with the held projection so a partial update overlays
|
|
172
|
+
// unchanged fields cleanly.
|
|
173
|
+
const partial = projectHostContext(params);
|
|
174
|
+
if (partial === undefined)
|
|
175
|
+
return;
|
|
176
|
+
const merged = { ...state.current, ...partial };
|
|
177
|
+
if (hostContextProjectionsEqual(state.current, merged))
|
|
178
|
+
return;
|
|
179
|
+
state.current = merged;
|
|
180
|
+
emit();
|
|
181
|
+
}
|
|
182
|
+
function emit() {
|
|
183
|
+
if (state === null)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
state.send({
|
|
187
|
+
type: 'host_context_observed',
|
|
188
|
+
payload: {
|
|
189
|
+
sessionId: state.sessionId,
|
|
190
|
+
hostContext: state.current,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Send seam errored (WS detached, host disconnected, etc.). The
|
|
196
|
+
// emitter is fire-and-forget; swallow + carry on. The next change
|
|
197
|
+
// notification will retry. Server-side agent visibility is
|
|
198
|
+
// eventually-consistent, not transactional.
|
|
199
|
+
}
|
|
200
|
+
// Fan out to local iframe-side subscribers
|
|
201
|
+
// (canvas mount's display-mode state). Independent from the
|
|
202
|
+
// WS-send try/catch above — a send failure must not skip the
|
|
203
|
+
// local fan-out, and a subscriber throwing must not skip other
|
|
204
|
+
// subscribers.
|
|
205
|
+
for (const listener of localSubscribers) {
|
|
206
|
+
try {
|
|
207
|
+
listener(state.current);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Subscriber threw — swallow to keep the broadcast loop alive.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|