@ulearning/ai-generative-frame 0.0.0-stage → 1.0.0-next.0

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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -2
  3. package/dist/assistant-ui/entry.d.ts +34 -0
  4. package/dist/assistant-ui/entry.d.ts.map +1 -0
  5. package/dist/assistant-ui/entry.js +50 -0
  6. package/dist/assistant-ui/entry.js.map +1 -0
  7. package/dist/assistant-ui/extension.d.ts +18 -0
  8. package/dist/assistant-ui/extension.d.ts.map +1 -0
  9. package/dist/assistant-ui/extension.js +0 -0
  10. package/dist/assistant-ui/history.d.ts +15 -0
  11. package/dist/assistant-ui/history.d.ts.map +1 -0
  12. package/dist/assistant-ui/history.js +39 -0
  13. package/dist/assistant-ui/history.js.map +1 -0
  14. package/dist/assistant-ui/modelContext.d.ts +45 -0
  15. package/dist/assistant-ui/modelContext.d.ts.map +1 -0
  16. package/dist/assistant-ui/modelContext.js +88 -0
  17. package/dist/assistant-ui/modelContext.js.map +1 -0
  18. package/dist/assistant-ui/snapshotSaver.d.ts +11 -0
  19. package/dist/assistant-ui/snapshotSaver.d.ts.map +1 -0
  20. package/dist/assistant-ui/snapshotSaver.js +83 -0
  21. package/dist/assistant-ui/snapshotSaver.js.map +1 -0
  22. package/dist/assistant-ui/tool-calls.d.ts +21 -0
  23. package/dist/assistant-ui/tool-calls.d.ts.map +1 -0
  24. package/dist/assistant-ui/tool-calls.js +65 -0
  25. package/dist/assistant-ui/tool-calls.js.map +1 -0
  26. package/dist/assistant-ui/toolkit.d.ts +96 -0
  27. package/dist/assistant-ui/toolkit.d.ts.map +1 -0
  28. package/dist/assistant-ui/toolkit.js +456 -0
  29. package/dist/assistant-ui/toolkit.js.map +1 -0
  30. package/dist/assistant-ui/useSharedContext.d.ts +14 -0
  31. package/dist/assistant-ui/useSharedContext.d.ts.map +1 -0
  32. package/dist/assistant-ui/useSharedContext.js +27 -0
  33. package/dist/assistant-ui/useSharedContext.js.map +1 -0
  34. package/dist/assistant-ui.d.ts +3 -0
  35. package/dist/assistant-ui.d.ts.map +1 -0
  36. package/dist/assistant-ui.js +1 -0
  37. package/dist/bootstrap.d.ts +23 -0
  38. package/dist/bootstrap.d.ts.map +1 -0
  39. package/dist/bootstrap.js +78 -0
  40. package/dist/bootstrap.js.map +1 -0
  41. package/dist/csp.d.ts +25 -0
  42. package/dist/csp.d.ts.map +1 -0
  43. package/dist/csp.js +78 -0
  44. package/dist/csp.js.map +1 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +5 -0
  48. package/dist/json-schema.d.ts +70 -0
  49. package/dist/json-schema.d.ts.map +1 -0
  50. package/dist/json-schema.js +163 -0
  51. package/dist/json-schema.js.map +1 -0
  52. package/dist/preview.d.ts +30 -0
  53. package/dist/preview.d.ts.map +1 -0
  54. package/dist/preview.js +60 -0
  55. package/dist/preview.js.map +1 -0
  56. package/dist/prompts/guidance.d.ts +41 -0
  57. package/dist/prompts/guidance.d.ts.map +1 -0
  58. package/dist/prompts/guidance.js +268 -0
  59. package/dist/prompts/guidance.js.map +1 -0
  60. package/dist/prompts.d.ts +2 -0
  61. package/dist/prompts.d.ts.map +1 -0
  62. package/dist/prompts.js +1 -0
  63. package/dist/protocol.d.ts +114 -0
  64. package/dist/protocol.d.ts.map +1 -0
  65. package/dist/protocol.js +49 -0
  66. package/dist/protocol.js.map +1 -0
  67. package/dist/repair/repair.d.ts +72 -0
  68. package/dist/repair/repair.d.ts.map +1 -0
  69. package/dist/repair/repair.js +103 -0
  70. package/dist/repair/repair.js.map +1 -0
  71. package/dist/repair.d.ts +2 -0
  72. package/dist/repair.d.ts.map +1 -0
  73. package/dist/repair.js +1 -0
  74. package/dist/rpc.d.ts +60 -0
  75. package/dist/rpc.d.ts.map +1 -0
  76. package/dist/rpc.js +127 -0
  77. package/dist/rpc.js.map +1 -0
  78. package/dist/runtime/diagnostics.d.ts +17 -0
  79. package/dist/runtime/diagnostics.d.ts.map +1 -0
  80. package/dist/runtime/diagnostics.js +128 -0
  81. package/dist/runtime/diagnostics.js.map +1 -0
  82. package/dist/runtime/generated.d.ts +2 -0
  83. package/dist/runtime/generated.d.ts.map +1 -0
  84. package/dist/runtime/generated.js +5 -0
  85. package/dist/runtime/generated.js.map +1 -0
  86. package/dist/runtime/index.d.ts +7 -0
  87. package/dist/runtime/index.d.ts.map +1 -0
  88. package/dist/runtime/index.js +264 -0
  89. package/dist/runtime/index.js.map +1 -0
  90. package/dist/runtime/morph.d.ts +51 -0
  91. package/dist/runtime/morph.d.ts.map +1 -0
  92. package/dist/runtime/morph.js +251 -0
  93. package/dist/runtime/morph.js.map +1 -0
  94. package/dist/runtime/screenshot.d.ts +23 -0
  95. package/dist/runtime/screenshot.d.ts.map +1 -0
  96. package/dist/runtime/screenshot.js +188 -0
  97. package/dist/runtime/screenshot.js.map +1 -0
  98. package/dist/runtime/size.d.ts +10 -0
  99. package/dist/runtime/size.d.ts.map +1 -0
  100. package/dist/runtime/size.js +21 -0
  101. package/dist/runtime/size.js.map +1 -0
  102. package/dist/runtime/storage.d.ts +4 -0
  103. package/dist/runtime/storage.d.ts.map +1 -0
  104. package/dist/runtime/storage.js +38 -0
  105. package/dist/runtime/storage.js.map +1 -0
  106. package/dist/spec/actions.d.ts +40 -0
  107. package/dist/spec/actions.d.ts.map +1 -0
  108. package/dist/spec/actions.js +53 -0
  109. package/dist/spec/actions.js.map +1 -0
  110. package/dist/spec/assistant-ui/thread.d.ts +66 -0
  111. package/dist/spec/assistant-ui/thread.d.ts.map +1 -0
  112. package/dist/spec/assistant-ui/thread.js +252 -0
  113. package/dist/spec/assistant-ui/thread.js.map +1 -0
  114. package/dist/spec/assistant-ui/toolkit.d.ts +33 -0
  115. package/dist/spec/assistant-ui/toolkit.d.ts.map +1 -0
  116. package/dist/spec/assistant-ui/toolkit.js +142 -0
  117. package/dist/spec/assistant-ui/toolkit.js.map +1 -0
  118. package/dist/spec/assistant-ui/web.d.ts +6 -0
  119. package/dist/spec/assistant-ui/web.d.ts.map +1 -0
  120. package/dist/spec/assistant-ui/web.js +48 -0
  121. package/dist/spec/assistant-ui/web.js.map +1 -0
  122. package/dist/spec/assistant-ui.d.ts +3 -0
  123. package/dist/spec/assistant-ui.d.ts.map +1 -0
  124. package/dist/spec/assistant-ui.js +1 -0
  125. package/dist/spec/calc.d.ts +8 -0
  126. package/dist/spec/calc.d.ts.map +1 -0
  127. package/dist/spec/calc.js +498 -0
  128. package/dist/spec/calc.js.map +1 -0
  129. package/dist/spec/catalog.d.ts +52 -0
  130. package/dist/spec/catalog.d.ts.map +1 -0
  131. package/dist/spec/catalog.js +104 -0
  132. package/dist/spec/catalog.js.map +1 -0
  133. package/dist/spec/expressions.d.ts +45 -0
  134. package/dist/spec/expressions.d.ts.map +1 -0
  135. package/dist/spec/expressions.js +199 -0
  136. package/dist/spec/expressions.js.map +1 -0
  137. package/dist/spec/patch.d.ts +20 -0
  138. package/dist/spec/patch.d.ts.map +1 -0
  139. package/dist/spec/patch.js +77 -0
  140. package/dist/spec/patch.js.map +1 -0
  141. package/dist/spec/pending.d.ts +15 -0
  142. package/dist/spec/pending.d.ts.map +1 -0
  143. package/dist/spec/pending.js +33 -0
  144. package/dist/spec/pending.js.map +1 -0
  145. package/dist/spec/pointer.d.ts +17 -0
  146. package/dist/spec/pointer.d.ts.map +1 -0
  147. package/dist/spec/pointer.js +88 -0
  148. package/dist/spec/pointer.js.map +1 -0
  149. package/dist/spec/prompt.d.ts +29 -0
  150. package/dist/spec/prompt.d.ts.map +1 -0
  151. package/dist/spec/prompt.js +145 -0
  152. package/dist/spec/prompt.js.map +1 -0
  153. package/dist/spec/render-spec.d.ts +50 -0
  154. package/dist/spec/render-spec.d.ts.map +1 -0
  155. package/dist/spec/render-spec.js +104 -0
  156. package/dist/spec/render-spec.js.map +1 -0
  157. package/dist/spec/resolve.d.ts +39 -0
  158. package/dist/spec/resolve.d.ts.map +1 -0
  159. package/dist/spec/resolve.js +51 -0
  160. package/dist/spec/resolve.js.map +1 -0
  161. package/dist/spec/resolved.d.ts +3 -0
  162. package/dist/spec/resolved.d.ts.map +1 -0
  163. package/dist/spec/resolved.js +36 -0
  164. package/dist/spec/resolved.js.map +1 -0
  165. package/dist/spec/state.d.ts +34 -0
  166. package/dist/spec/state.d.ts.map +1 -0
  167. package/dist/spec/state.js +111 -0
  168. package/dist/spec/state.js.map +1 -0
  169. package/dist/spec/stream.d.ts +50 -0
  170. package/dist/spec/stream.d.ts.map +1 -0
  171. package/dist/spec/stream.js +168 -0
  172. package/dist/spec/stream.js.map +1 -0
  173. package/dist/spec/tools.d.ts +3 -0
  174. package/dist/spec/tools.d.ts.map +1 -0
  175. package/dist/spec/tools.js +3 -0
  176. package/dist/spec/tree.d.ts +21 -0
  177. package/dist/spec/tree.d.ts.map +1 -0
  178. package/dist/spec/tree.js +27 -0
  179. package/dist/spec/tree.js.map +1 -0
  180. package/dist/spec/types.d.ts +66 -0
  181. package/dist/spec/types.d.ts.map +1 -0
  182. package/dist/spec/types.js +9 -0
  183. package/dist/spec/types.js.map +1 -0
  184. package/dist/spec/validate.d.ts +32 -0
  185. package/dist/spec/validate.d.ts.map +1 -0
  186. package/dist/spec/validate.js +264 -0
  187. package/dist/spec/validate.js.map +1 -0
  188. package/dist/spec/vue/SpecRenderer.d.ts +43 -0
  189. package/dist/spec/vue/SpecRenderer.d.ts.map +1 -0
  190. package/dist/spec/vue/SpecRenderer.js +357 -0
  191. package/dist/spec/vue/SpecRenderer.js.map +1 -0
  192. package/dist/spec/vue/useSpecStream.d.ts +19 -0
  193. package/dist/spec/vue/useSpecStream.d.ts.map +1 -0
  194. package/dist/spec/vue/useSpecStream.js +115 -0
  195. package/dist/spec/vue/useSpecStream.js.map +1 -0
  196. package/dist/spec/vue.d.ts +3 -0
  197. package/dist/spec/vue.d.ts.map +1 -0
  198. package/dist/spec/vue.js +3 -0
  199. package/dist/spec.d.ts +12 -0
  200. package/dist/spec.d.ts.map +1 -0
  201. package/dist/spec.js +8 -0
  202. package/dist/theme.d.ts +38 -0
  203. package/dist/theme.d.ts.map +1 -0
  204. package/dist/theme.js +370 -0
  205. package/dist/theme.js.map +1 -0
  206. package/dist/tools/define.d.ts +43 -0
  207. package/dist/tools/define.d.ts.map +1 -0
  208. package/dist/tools/define.js +31 -0
  209. package/dist/tools/define.js.map +1 -0
  210. package/dist/tools/edits.d.ts +19 -0
  211. package/dist/tools/edits.d.ts.map +1 -0
  212. package/dist/tools/edits.js +55 -0
  213. package/dist/tools/edits.js.map +1 -0
  214. package/dist/tools/registry.d.ts +19 -0
  215. package/dist/tools/registry.d.ts.map +1 -0
  216. package/dist/tools/registry.js +33 -0
  217. package/dist/tools/registry.js.map +1 -0
  218. package/dist/tools/tools.d.ts +88 -0
  219. package/dist/tools/tools.d.ts.map +1 -0
  220. package/dist/tools/tools.js +235 -0
  221. package/dist/tools/tools.js.map +1 -0
  222. package/dist/tools.d.ts +5 -0
  223. package/dist/tools.d.ts.map +1 -0
  224. package/dist/tools.js +5 -0
  225. package/dist/version.d.ts +2 -0
  226. package/dist/version.d.ts.map +1 -0
  227. package/dist/version.js +5 -0
  228. package/dist/version.js.map +1 -0
  229. package/dist/vue/Widget.d.ts +16 -0
  230. package/dist/vue/Widget.d.ts.map +1 -0
  231. package/dist/vue/Widget.js +58 -0
  232. package/dist/vue/Widget.js.map +1 -0
  233. package/dist/vue/sync.d.ts +19 -0
  234. package/dist/vue/sync.d.ts.map +1 -0
  235. package/dist/vue/sync.js +31 -0
  236. package/dist/vue/sync.js.map +1 -0
  237. package/dist/vue/themeStore.d.ts +23 -0
  238. package/dist/vue/themeStore.d.ts.map +1 -0
  239. package/dist/vue/themeStore.js +115 -0
  240. package/dist/vue/themeStore.js.map +1 -0
  241. package/dist/vue/useThemeTokens.d.ts +6 -0
  242. package/dist/vue/useThemeTokens.d.ts.map +1 -0
  243. package/dist/vue/useThemeTokens.js +49 -0
  244. package/dist/vue/useThemeTokens.js.map +1 -0
  245. package/dist/vue/useWidget.d.ts +10 -0
  246. package/dist/vue/useWidget.d.ts.map +1 -0
  247. package/dist/vue/useWidget.js +81 -0
  248. package/dist/vue/useWidget.js.map +1 -0
  249. package/dist/vue.d.ts +5 -0
  250. package/dist/vue.d.ts.map +1 -0
  251. package/dist/vue.js +4 -0
  252. package/dist/widget.d.ts +129 -0
  253. package/dist/widget.d.ts.map +1 -0
  254. package/dist/widget.js +655 -0
  255. package/dist/widget.js.map +1 -0
  256. package/package.json +125 -3
  257. package/src/assistant-ui/entry.ts +124 -0
  258. package/src/assistant-ui/extension.ts +19 -0
  259. package/src/assistant-ui/history.ts +63 -0
  260. package/src/assistant-ui/modelContext.ts +162 -0
  261. package/src/assistant-ui/snapshotSaver.ts +116 -0
  262. package/src/assistant-ui/tool-calls.ts +115 -0
  263. package/src/assistant-ui/toolkit.tsx +747 -0
  264. package/src/assistant-ui/useSharedContext.ts +40 -0
  265. package/src/assistant-ui.ts +14 -0
  266. package/src/bootstrap.ts +112 -0
  267. package/src/csp.ts +96 -0
  268. package/src/index.ts +44 -0
  269. package/src/json-schema.ts +353 -0
  270. package/src/preview.ts +120 -0
  271. package/src/prompts/guidance.ts +369 -0
  272. package/src/prompts.ts +8 -0
  273. package/src/protocol.ts +141 -0
  274. package/src/repair/repair.ts +202 -0
  275. package/src/repair.ts +11 -0
  276. package/src/rpc.ts +228 -0
  277. package/src/runtime/diagnostics.ts +165 -0
  278. package/src/runtime/generated.ts +2 -0
  279. package/src/runtime/index.ts +343 -0
  280. package/src/runtime/morph.ts +352 -0
  281. package/src/runtime/screenshot.ts +288 -0
  282. package/src/runtime/size.ts +17 -0
  283. package/src/runtime/storage.ts +49 -0
  284. package/src/spec/actions.ts +111 -0
  285. package/src/spec/assistant-ui/thread.ts +381 -0
  286. package/src/spec/assistant-ui/toolkit.tsx +295 -0
  287. package/src/spec/assistant-ui/web.tsx +63 -0
  288. package/src/spec/assistant-ui.ts +2 -0
  289. package/src/spec/calc.ts +598 -0
  290. package/src/spec/catalog.ts +196 -0
  291. package/src/spec/expressions.ts +301 -0
  292. package/src/spec/patch.ts +112 -0
  293. package/src/spec/pending.ts +36 -0
  294. package/src/spec/pointer.ts +170 -0
  295. package/src/spec/prompt.ts +243 -0
  296. package/src/spec/render-spec.ts +144 -0
  297. package/src/spec/resolve.ts +123 -0
  298. package/src/spec/resolved.ts +53 -0
  299. package/src/spec/state.ts +193 -0
  300. package/src/spec/stream.ts +230 -0
  301. package/src/spec/tools.ts +17 -0
  302. package/src/spec/tree.ts +37 -0
  303. package/src/spec/types.ts +51 -0
  304. package/src/spec/validate.ts +429 -0
  305. package/src/spec/vue/SpecRenderer.tsx +544 -0
  306. package/src/spec/vue/__tests__/harness.ts +50 -0
  307. package/src/spec/vue/useSpecStream.ts +162 -0
  308. package/src/spec/vue.ts +13 -0
  309. package/src/spec.ts +55 -0
  310. package/src/theme.ts +423 -0
  311. package/src/tools/define.ts +65 -0
  312. package/src/tools/edits.ts +73 -0
  313. package/src/tools/registry.ts +44 -0
  314. package/src/tools/tools.ts +361 -0
  315. package/src/tools.ts +32 -0
  316. package/src/version.ts +4 -0
  317. package/src/vue/Widget.tsx +74 -0
  318. package/src/vue/sync.ts +36 -0
  319. package/src/vue/themeStore.ts +157 -0
  320. package/src/vue/useThemeTokens.ts +71 -0
  321. package/src/vue/useWidget.ts +117 -0
  322. package/src/vue.ts +8 -0
  323. package/src/widget.ts +1080 -0
package/src/widget.ts ADDED
@@ -0,0 +1,1080 @@
1
+ import {
2
+ SafeContentFrame,
3
+ type RenderedFrame,
4
+ } from "@ulearning/ai-safe-content-frame";
5
+ import { buildBootstrapHtml, themeContext } from "./bootstrap";
6
+ import type { CspOptions } from "./csp";
7
+ import {
8
+ GENFRAME_PROTOCOL_VERSION,
9
+ INIT_MESSAGE,
10
+ METHODS,
11
+ READY_MESSAGE,
12
+ type ClearStorageResult,
13
+ type ConsoleEntry,
14
+ type DisplayMode,
15
+ type EndResult,
16
+ type FrameInspection,
17
+ type HostContext,
18
+ type InitMessage,
19
+ type McpAppInitializeParams,
20
+ type ScreenshotOptions,
21
+ type WidgetError,
22
+ type WidgetSize,
23
+ } from "./protocol";
24
+ import {
25
+ createRpcPeer,
26
+ isJsonRpcMessage,
27
+ RPC_ERROR,
28
+ RpcError,
29
+ type RpcPeer,
30
+ } from "./rpc";
31
+ import { DEFAULT_LIGHT_TOKENS, type ThemeTokens } from "./theme";
32
+ import { VERSION } from "./version";
33
+
34
+ export type UiMessageParams = {
35
+ role?: string;
36
+ content?: { type: string; text?: string }[];
37
+ [key: string]: unknown;
38
+ };
39
+
40
+ export type ToolCallRequest = {
41
+ name: string;
42
+ arguments?: Record<string, unknown>;
43
+ };
44
+
45
+ export type WidgetHandlers = {
46
+ /** A widget called `sendPrompt(text)`, possibly without a user gesture; let the user review it before it is sent as their message. */
47
+ onPrompt?: (text: string) => unknown;
48
+ /** Raw MCP Apps `ui/message`. Takes precedence over `onPrompt`. */
49
+ onMessage?: (params: UiMessageParams) => unknown;
50
+ /** Defaults to opening an `http(s)` URL in a new tab with `noopener`. */
51
+ onOpenLink?: (url: string) => unknown;
52
+ onCallTool?: (call: ToolCallRequest) => unknown;
53
+ onInitialized?: (params: McpAppInitializeParams) => void;
54
+ onRequestDisplayMode?: (request: { mode: DisplayMode }) => unknown;
55
+ onUpdateModelContext?: (params: unknown) => unknown;
56
+ /** A widget called `genframe.copyText(text)` or `navigator.clipboard.writeText(text)`. The request alone does not show the user asked for the copy, so confirm with them, such as through a Copy control the host renders, before writing the clipboard. Without it, copying fails in the widget. */
57
+ onCopyText?: (text: string) => unknown;
58
+ onWidgetState?: (state: unknown) => void;
59
+ onResize?: (size: WidgetSize) => void;
60
+ onError?: (error: WidgetError) => void;
61
+ onLog?: (entry: ConsoleEntry) => void;
62
+ };
63
+
64
+ export type CreateWidgetOptions = WidgetHandlers & {
65
+ container: HTMLElement;
66
+ /** Scopes the frame origin, see `SafeContentFrame`. Defaults to `"generative-frame"`. */
67
+ product?: string;
68
+ /** A preconfigured `SafeContentFrame`, for example with `useShadowDom`. */
69
+ frame?: SafeContentFrame;
70
+ /**
71
+ * Loads the Safe Content Frame shim from a domain you host instead of
72
+ * the default service. Widgets are isolated from your app and from each
73
+ * other only when the domain is a public suffix on the Public Suffix List.
74
+ */
75
+ unsafeShimDomain?: string;
76
+ /**
77
+ * Renders in a `sandbox="allow-scripts"` frame with an opaque (`null`)
78
+ * origin instead of Safe Content Frame: no shim domain, and no storage or
79
+ * cookies. Cannot be combined with `id`, `frame`, or `unsafeShimDomain`.
80
+ */
81
+ opaqueOrigin?: boolean;
82
+ /**
83
+ * Gives the widget a stable origin, so its localStorage, IndexedDB, and
84
+ * cookies persist across reloads for this id on this host origin. Choose
85
+ * it on the host (never from model output); widgets with the same id share
86
+ * storage. Without an id, every frame gets a fresh origin.
87
+ */
88
+ id?: string;
89
+ csp?: CspOptions | string;
90
+ tokens?: ThemeTokens;
91
+ /** Extra host context fields (locale, display mode, …) merged over the theme. */
92
+ context?: HostContext;
93
+ animate?: boolean;
94
+ css?: string;
95
+ /** The iframe never grows past this height; taller content scrolls inside it. */
96
+ maxHeight?: number;
97
+ minHeight?: number;
98
+ readyTimeoutMs?: number;
99
+ initialState?: unknown;
100
+ /** Complete code to render immediately, as `write(code)` then `end()`. */
101
+ code?: string;
102
+ };
103
+
104
+ export type WidgetInspection = FrameInspection & { code: string };
105
+
106
+ export type Screenshot = { dataUrl: string; width: number; height: number };
107
+
108
+ export type WidgetEventMap = {
109
+ ready: undefined;
110
+ resize: WidgetSize;
111
+ error: WidgetError;
112
+ log: ConsoleEntry;
113
+ end: EndResult;
114
+ };
115
+
116
+ export type WidgetHandle = {
117
+ /** Resolves once the frame runtime is connected; rejects if it never connects. */
118
+ readonly ready: Promise<void>;
119
+ /** All code written so far. */
120
+ readonly code: string;
121
+ readonly ended: boolean;
122
+ readonly iframe: HTMLIFrameElement | undefined;
123
+ /** Appends streamed code. Rendering is coalesced to animation frames. */
124
+ write(chunk: string): void;
125
+ /** Marks the code complete and runs held scripts in order. */
126
+ end(): Promise<EndResult>;
127
+ /** Renders new complete code, morphing in place or remounting when scripts already ran. A newer call removes a remount still in progress, whose promise rejects. */
128
+ replace(code: string): Promise<EndResult>;
129
+ setTheme(tokens: ThemeTokens): void;
130
+ setContext(context: HostContext): void;
131
+ notifyToolInput(
132
+ args: Record<string, unknown>,
133
+ options?: { partial?: boolean },
134
+ ): void;
135
+ notifyToolResult(result: unknown): void;
136
+ screenshot(options?: ScreenshotOptions): Promise<Screenshot>;
137
+ inspect(): Promise<WidgetInspection>;
138
+ on<K extends keyof WidgetEventMap>(
139
+ event: K,
140
+ listener: (payload: WidgetEventMap[K]) => void,
141
+ ): () => void;
142
+ dispose(): void;
143
+ };
144
+
145
+ const DEFAULT_PRODUCT = "generative-frame";
146
+
147
+ /** The Safe Content Frame salt behind a widget id's stable origin. */
148
+ export const widgetStorageSalt = (id: string) => `genframe:v1:${id}`;
149
+
150
+ type FrameRenderer = {
151
+ renderHtml(html: string, container: HTMLElement): Promise<RenderedFrame>;
152
+ };
153
+
154
+ /**
155
+ * An opaque origin cannot be named as a `postMessage` target, so messages to
156
+ * the frame use `"*"`. A sandboxed frame keeps its opaque origin and window
157
+ * when it navigates, so after a navigated document loads, `origin` stops
158
+ * matching and nothing is posted to the frame. Loads are counted from the
159
+ * bootstrap's first ready message, so an extra initial `about:blank` load in
160
+ * some browser cannot cut off the bootstrap itself. A navigated document can
161
+ * still post before its own `load`; that grants it nothing the widget code
162
+ * could not already do.
163
+ */
164
+ const opaqueFrame: FrameRenderer = {
165
+ async renderHtml(html, container) {
166
+ const iframe = container.ownerDocument.createElement("iframe");
167
+ iframe.setAttribute("sandbox", "allow-scripts");
168
+ iframe.style.cssText = "border:none;width:100%;height:100%";
169
+ iframe.srcdoc = html;
170
+ let ready = false;
171
+ let loads = 0;
172
+ const onMessage = (event: MessageEvent) => {
173
+ if (
174
+ event.source === iframe.contentWindow &&
175
+ (event.data as { type?: unknown } | null)?.type === READY_MESSAGE
176
+ ) {
177
+ ready = true;
178
+ }
179
+ };
180
+ window.addEventListener("message", onMessage);
181
+ iframe.addEventListener("load", () => {
182
+ if (ready) loads++;
183
+ });
184
+ container.appendChild(iframe);
185
+ return {
186
+ iframe,
187
+ get origin() {
188
+ return loads > 1 ? "" : "null";
189
+ },
190
+ sendMessage: (data, transfer) => {
191
+ if (loads > 1) return;
192
+ iframe.contentWindow?.postMessage(data, "*", transfer);
193
+ },
194
+ fullyLoadedPromiseWithTimeout: async () => {},
195
+ dispose: () => {
196
+ window.removeEventListener("message", onMessage);
197
+ iframe.remove();
198
+ },
199
+ };
200
+ },
201
+ };
202
+
203
+ const resolveFrame = (
204
+ options: Pick<
205
+ CreateWidgetOptions,
206
+ "frame" | "id" | "product" | "unsafeShimDomain" | "opaqueOrigin"
207
+ >,
208
+ ): FrameRenderer => {
209
+ if (options.opaqueOrigin) {
210
+ if (
211
+ options.frame ||
212
+ options.id !== undefined ||
213
+ options.unsafeShimDomain !== undefined
214
+ ) {
215
+ throw new TypeError(
216
+ "`opaqueOrigin` renders without Safe Content Frame, so it cannot be combined with `frame`, `id`, or `unsafeShimDomain`.",
217
+ );
218
+ }
219
+ return opaqueFrame;
220
+ }
221
+ if (
222
+ options.frame &&
223
+ (options.id !== undefined || options.unsafeShimDomain !== undefined)
224
+ ) {
225
+ throw new TypeError(
226
+ "`frame` cannot be combined with `id` or `unsafeShimDomain`; configure the `SafeContentFrame` instead, with `salt: widgetStorageSalt(id)` and `unsafeShimDomain` as needed.",
227
+ );
228
+ }
229
+ return (
230
+ options.frame ??
231
+ new SafeContentFrame(options.product ?? DEFAULT_PRODUCT, {
232
+ ...(options.id !== undefined
233
+ ? { salt: widgetStorageSalt(options.id) }
234
+ : {}),
235
+ ...(options.unsafeShimDomain !== undefined
236
+ ? { unsafeShimDomain: options.unsafeShimDomain }
237
+ : {}),
238
+ })
239
+ );
240
+ };
241
+
242
+ /** Lets `clearWidgetStorage` send a request no public method exposes. */
243
+ const internalRequests = new WeakMap<
244
+ WidgetHandle,
245
+ <T>(method: string, params?: unknown) => Promise<T>
246
+ >();
247
+ const DEFAULT_READY_TIMEOUT_MS = 15_000;
248
+ const REQUEST_TIMEOUT_MS = 30_000;
249
+ const SCRIPT_TAG = /<script[\s>]/i;
250
+ const REVEAL_MS = 120;
251
+
252
+ const prefersReducedMotion = () =>
253
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
254
+
255
+ const promptText = (params: UiMessageParams) => {
256
+ const content = params.content ?? [];
257
+ return Object.keys(content)
258
+ .filter((key) => {
259
+ const index = Number(key);
260
+ return (
261
+ Number.isInteger(index) &&
262
+ index >= 0 &&
263
+ index < content.length &&
264
+ String(index) === key
265
+ );
266
+ })
267
+ .map((key) => content[Number(key)])
268
+ .filter(
269
+ (part): part is { type: string; text: string } =>
270
+ typeof part === "object" &&
271
+ part !== null &&
272
+ part.type === "text" &&
273
+ typeof part.text === "string",
274
+ )
275
+ .map((part) => part.text)
276
+ .join("\n");
277
+ };
278
+
279
+ const VALID_DISPLAY_MODES = [
280
+ "inline",
281
+ "fullscreen",
282
+ "pip",
283
+ ] as const satisfies readonly DisplayMode[];
284
+
285
+ const isDisplayMode = (value: unknown): value is DisplayMode =>
286
+ VALID_DISPLAY_MODES.some((mode) => mode === value);
287
+
288
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
289
+ typeof value === "object" && value !== null && !Array.isArray(value);
290
+
291
+ /** How many characters a widget's state may take as JSON. A shared object counts once for each place it appears, as it does once serialized, so checking and saving a state stay bounded however the widget shares objects. */
292
+ const MAX_STATE_LENGTH = 1_000_000;
293
+
294
+ /** The length of `text` once JSON quotes and escapes it. */
295
+ const jsonStringLength = (text: string) => {
296
+ let length = text.length + 2;
297
+ for (let index = 0; index < text.length; index++) {
298
+ const code = text.charCodeAt(index);
299
+ if (code === 0x22 || code === 0x5c) length += 1;
300
+ else if (code < 0x20)
301
+ length +=
302
+ code === 0x08 ||
303
+ code === 0x09 ||
304
+ code === 0x0a ||
305
+ code === 0x0c ||
306
+ code === 0x0d
307
+ ? 1
308
+ : 5;
309
+ else if (code >= 0xd800 && code <= 0xdbff) {
310
+ const next = text.charCodeAt(index + 1);
311
+ if (next >= 0xdc00 && next <= 0xdfff) index++;
312
+ else length += 5;
313
+ } else if (code >= 0xdc00 && code <= 0xdfff) length += 5;
314
+ }
315
+ return length;
316
+ };
317
+
318
+ const isPlainJson = (state: unknown): boolean => {
319
+ let budget = MAX_STATE_LENGTH;
320
+ const spend = (length: number) => (budget -= length) >= 0;
321
+ const spendText = (text: string) =>
322
+ text.length + 2 <= budget && spend(jsonStringLength(text));
323
+ const ancestors = new Set<object>();
324
+ const check = (value: unknown): boolean => {
325
+ if (value === null || typeof value === "boolean") return spend(5);
326
+ if (typeof value === "string") return spendText(value);
327
+ if (typeof value === "number")
328
+ return Number.isFinite(value) && spend(String(value).length);
329
+ if (typeof value !== "object" || ancestors.has(value) || !spend(2))
330
+ return false;
331
+
332
+ const array = Array.isArray(value);
333
+ const prototype = Object.getPrototypeOf(value);
334
+ if (
335
+ array
336
+ ? prototype !== Array.prototype
337
+ : prototype !== Object.prototype && prototype !== null
338
+ )
339
+ return false;
340
+
341
+ ancestors.add(value);
342
+ const keys = Reflect.ownKeys(value);
343
+ if (array && keys.length !== value.length + 1) return false;
344
+ if (array) {
345
+ for (let index = 0; index < value.length; index++) {
346
+ const property = Object.getOwnPropertyDescriptor(value, String(index));
347
+ if (
348
+ !property ||
349
+ !("value" in property) ||
350
+ !spend(1) ||
351
+ !check(property.value)
352
+ )
353
+ return false;
354
+ }
355
+ } else {
356
+ for (const key of keys) {
357
+ if (typeof key !== "string") return false;
358
+ const property = Object.getOwnPropertyDescriptor(value, key);
359
+ if (
360
+ !property ||
361
+ !("value" in property) ||
362
+ !property.enumerable ||
363
+ !spendText(key) ||
364
+ !spend(2) ||
365
+ !check(property.value)
366
+ )
367
+ return false;
368
+ }
369
+ }
370
+ ancestors.delete(value);
371
+ return true;
372
+ };
373
+ return check(state);
374
+ };
375
+
376
+ const isAppInfo = (
377
+ value: unknown,
378
+ ): value is NonNullable<McpAppInitializeParams["appInfo"]> =>
379
+ isRecord(value) &&
380
+ typeof value["name"] === "string" &&
381
+ typeof value["version"] === "string";
382
+
383
+ const getAppCapabilities = (
384
+ value: unknown,
385
+ ): McpAppInitializeParams["appCapabilities"] => {
386
+ if (!isRecord(value)) return undefined;
387
+ const { availableDisplayModes, ...rest } = value;
388
+ return {
389
+ ...rest,
390
+ ...(Array.isArray(availableDisplayModes)
391
+ ? {
392
+ availableDisplayModes: Object.values(availableDisplayModes).filter(
393
+ isDisplayMode,
394
+ ),
395
+ }
396
+ : {}),
397
+ };
398
+ };
399
+
400
+ const getInitializeParams = (params: unknown): McpAppInitializeParams => {
401
+ if (!isRecord(params)) return {};
402
+ const appCapabilities = getAppCapabilities(params["appCapabilities"]);
403
+ return {
404
+ ...(isAppInfo(params["appInfo"]) ? { appInfo: params["appInfo"] } : {}),
405
+ ...(appCapabilities ? { appCapabilities } : {}),
406
+ ...(typeof params["protocolVersion"] === "string"
407
+ ? { protocolVersion: params["protocolVersion"] }
408
+ : {}),
409
+ };
410
+ };
411
+
412
+ const defaultOpenLink = (url: string) => {
413
+ window.open(url, "_blank", "noopener,noreferrer");
414
+ };
415
+
416
+ type Session = {
417
+ slot: HTMLElement;
418
+ ready: Promise<void>;
419
+ peer: () => RpcPeer;
420
+ iframe: () => HTMLIFrameElement | undefined;
421
+ /** Shows the iframe once the runtime has themed and laid out its document. */
422
+ reveal(): void;
423
+ /** Delivers the frame's MCP Apps initialization if it is the shown frame. */
424
+ deliverInitialized(): void;
425
+ dispose(): void;
426
+ };
427
+
428
+ /**
429
+ * Mounts a widget frame in `container`. The frame loads a bootstrap document
430
+ * once through Safe Content Frame; code then streams in over a private
431
+ * `MessagePort`, so every chunk renders without reloading the frame.
432
+ */
433
+ export function createWidget(options: CreateWidgetOptions): WidgetHandle {
434
+ const { container } = options;
435
+ const frame = resolveFrame(options);
436
+ const minHeight = options.minHeight ?? 0;
437
+ const maxHeight = options.maxHeight ?? Number.POSITIVE_INFINITY;
438
+ const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
439
+
440
+ let tokens = options.tokens ?? DEFAULT_LIGHT_TOKENS;
441
+ let context: HostContext = {
442
+ ...themeContext(tokens),
443
+ platform: "web",
444
+ ...(Number.isFinite(maxHeight)
445
+ ? { containerDimensions: { maxHeight } }
446
+ : {}),
447
+ ...options.context,
448
+ };
449
+ let code = "";
450
+ let widgetState: unknown = options.initialState ?? null;
451
+ let ended = false;
452
+ let disposed = false;
453
+ let lastToolInput:
454
+ | { args: Record<string, unknown>; partial: boolean }
455
+ | undefined;
456
+ let lastToolResult: { result: unknown } | undefined;
457
+ const listeners = new Map<
458
+ keyof WidgetEventMap,
459
+ Set<(payload: never) => void>
460
+ >();
461
+
462
+ const emit = <K extends keyof WidgetEventMap>(
463
+ event: K,
464
+ payload: WidgetEventMap[K],
465
+ ) => {
466
+ for (const listener of listeners.get(event) ?? []) {
467
+ try {
468
+ (listener as (payload: WidgetEventMap[K]) => void)(payload);
469
+ } catch (error) {
470
+ console.error(error);
471
+ }
472
+ }
473
+ };
474
+
475
+ const reportError = (error: WidgetError) => {
476
+ options.onError?.(error);
477
+ emit("error", error);
478
+ };
479
+
480
+ const onRequest = async (method: string, params: unknown) => {
481
+ const p = (params ?? {}) as Record<string, unknown>;
482
+ switch (method) {
483
+ case METHODS.initialize:
484
+ return {
485
+ protocolVersion:
486
+ typeof p["protocolVersion"] === "string"
487
+ ? p["protocolVersion"]
488
+ : GENFRAME_PROTOCOL_VERSION,
489
+ hostInfo: { name: "generative-frame", version: VERSION },
490
+ hostCapabilities: {
491
+ openLinks: {},
492
+ ...(options.onCallTool ? { serverTools: {} } : {}),
493
+ ...(options.onMessage || options.onPrompt
494
+ ? { message: { text: {} } }
495
+ : {}),
496
+ ...(options.onUpdateModelContext
497
+ ? { updateModelContext: { text: {}, structuredContent: {} } }
498
+ : {}),
499
+ logging: {},
500
+ },
501
+ hostContext: context,
502
+ };
503
+ case METHODS.message: {
504
+ if (options.onMessage) return options.onMessage(p as UiMessageParams);
505
+ if (options.onPrompt) {
506
+ await options.onPrompt(promptText(p as UiMessageParams));
507
+ return {};
508
+ }
509
+ throw new RpcError(
510
+ RPC_ERROR.methodNotFound,
511
+ "This host does not accept messages",
512
+ );
513
+ }
514
+ case METHODS.openLink: {
515
+ const url = p["url"];
516
+ let protocol = "";
517
+ try {
518
+ protocol = typeof url === "string" ? new URL(url).protocol : "";
519
+ } catch {}
520
+ if (protocol !== "http:" && protocol !== "https:") {
521
+ throw new RpcError(
522
+ RPC_ERROR.invalidParams,
523
+ "ui/open-link requires an http(s) URL",
524
+ );
525
+ }
526
+ await (options.onOpenLink ?? defaultOpenLink)(url as string);
527
+ return {};
528
+ }
529
+ case METHODS.callTool: {
530
+ if (!options.onCallTool) {
531
+ throw new RpcError(
532
+ RPC_ERROR.methodNotFound,
533
+ "This host does not expose tools",
534
+ );
535
+ }
536
+ if (typeof p["name"] !== "string") {
537
+ throw new RpcError(
538
+ RPC_ERROR.invalidParams,
539
+ "tools/call requires a string 'name'",
540
+ );
541
+ }
542
+ const args = p["arguments"];
543
+ if (
544
+ args !== undefined &&
545
+ (typeof args !== "object" || args === null || Array.isArray(args))
546
+ ) {
547
+ throw new RpcError(
548
+ RPC_ERROR.invalidParams,
549
+ "tools/call 'arguments' must be an object",
550
+ );
551
+ }
552
+ return options.onCallTool({
553
+ name: p["name"],
554
+ ...(args !== undefined
555
+ ? { arguments: args as Record<string, unknown> }
556
+ : {}),
557
+ });
558
+ }
559
+ case METHODS.requestDisplayMode: {
560
+ const mode = p["mode"];
561
+ if (!isDisplayMode(mode)) {
562
+ throw new RpcError(
563
+ RPC_ERROR.invalidParams,
564
+ "ui/request-display-mode requires a valid 'mode'",
565
+ );
566
+ }
567
+ if (!options.onRequestDisplayMode)
568
+ return { mode: context.displayMode ?? "inline" };
569
+ return (await options.onRequestDisplayMode({ mode })) ?? { mode };
570
+ }
571
+ case METHODS.copyText: {
572
+ if (!options.onCopyText) {
573
+ throw new RpcError(
574
+ RPC_ERROR.methodNotFound,
575
+ "This host does not copy text",
576
+ );
577
+ }
578
+ if (typeof p["text"] !== "string") {
579
+ throw new RpcError(
580
+ RPC_ERROR.invalidParams,
581
+ "genframe/copy-text requires a string 'text'",
582
+ );
583
+ }
584
+ await options.onCopyText(p["text"]);
585
+ return {};
586
+ }
587
+ case METHODS.updateModelContext:
588
+ if (!options.onUpdateModelContext) {
589
+ throw new RpcError(
590
+ RPC_ERROR.methodNotFound,
591
+ "This host does not accept model context",
592
+ );
593
+ }
594
+ return options.onUpdateModelContext(params);
595
+ default:
596
+ throw new RpcError(
597
+ RPC_ERROR.methodNotFound,
598
+ `Unknown method: ${method}`,
599
+ );
600
+ }
601
+ };
602
+
603
+ let activeSession: Session | undefined;
604
+ let pendingSession: Session | undefined;
605
+ let pendingWidgetState: unknown;
606
+ let hasPendingWidgetState = false;
607
+ let heldWidgetState: unknown;
608
+ let hasHeldWidgetState = false;
609
+
610
+ const applySize = (session: Session, size: WidgetSize) => {
611
+ const iframe = session.iframe();
612
+ if (iframe) {
613
+ const height = Math.min(Math.max(size.height, minHeight), maxHeight);
614
+ iframe.style.height = `${height}px`;
615
+ session.reveal();
616
+ }
617
+ if (session === activeSession) {
618
+ options.onResize?.(size);
619
+ emit("resize", size);
620
+ }
621
+ };
622
+
623
+ const createSession = (hidden: boolean): Session => {
624
+ const slot = container.ownerDocument.createElement("div");
625
+ slot.dataset["generativeFrame"] = "";
626
+ // Safe Content Frame appends the iframe at once, and its shim paints an
627
+ // opaque canvas before the runtime themes the document, so the slot stays
628
+ // transparent (but laid out) until the runtime's first size report.
629
+ slot.style.cssText = hidden
630
+ ? "display:block;height:0;overflow:hidden;visibility:hidden;pointer-events:none;opacity:0"
631
+ : "display:block;opacity:0";
632
+ container.appendChild(slot);
633
+
634
+ let rendered: RenderedFrame | undefined;
635
+ let peer: RpcPeer | undefined;
636
+ let initSent = false;
637
+ let initializeParams: McpAppInitializeParams = {};
638
+ let pendingInitialized: McpAppInitializeParams | undefined;
639
+ let sessionDisposed = false;
640
+ let revealed = false;
641
+ let resolveReady!: () => void;
642
+ let rejectReady!: (error: unknown) => void;
643
+ const ready = new Promise<void>((resolve, reject) => {
644
+ resolveReady = resolve;
645
+ rejectReady = reject;
646
+ });
647
+ void ready.catch(() => {});
648
+
649
+ const session: Session = {
650
+ slot,
651
+ ready,
652
+ peer: () => {
653
+ if (!peer) throw new Error("Widget frame is not connected");
654
+ return peer;
655
+ },
656
+ iframe: () => rendered?.iframe,
657
+ reveal() {
658
+ if (revealed) return;
659
+ revealed = true;
660
+ if (!prefersReducedMotion()) {
661
+ slot.style.transition = `opacity ${REVEAL_MS}ms ease-out`;
662
+ }
663
+ slot.style.opacity = "1";
664
+ },
665
+ deliverInitialized() {
666
+ if (session !== activeSession || !pendingInitialized) return;
667
+ const params = pendingInitialized;
668
+ pendingInitialized = undefined;
669
+ try {
670
+ options.onInitialized?.(params);
671
+ } catch {
672
+ // A host handler error must not fail a remount that already swapped in.
673
+ }
674
+ },
675
+ dispose() {
676
+ if (sessionDisposed) return;
677
+ sessionDisposed = true;
678
+ clearTimeout(timer);
679
+ window.removeEventListener("message", onWindowMessage);
680
+ peer?.dispose();
681
+ rendered?.dispose();
682
+ slot.remove();
683
+ rejectReady(new Error("Widget disposed"));
684
+ },
685
+ };
686
+
687
+ const fail = (message: string) => {
688
+ if (sessionDisposed) return;
689
+ rejectReady(new Error(message));
690
+ reportError({ kind: "error", message });
691
+ };
692
+ const timer = setTimeout(
693
+ () => fail(`Widget frame did not connect within ${readyTimeoutMs}ms`),
694
+ readyTimeoutMs,
695
+ );
696
+
697
+ const sendInit = () => {
698
+ if (initSent || !rendered) return;
699
+ initSent = true;
700
+ const channel = new MessageChannel();
701
+ peer = createRpcPeer(channel.port1, {
702
+ onRequest: (method, params) => {
703
+ if (method === METHODS.initialize) {
704
+ initializeParams = getInitializeParams(params);
705
+ }
706
+ return onRequest(method, params);
707
+ },
708
+ onNotification: (method, params) => {
709
+ const p = (params ?? {}) as Record<string, unknown>;
710
+ switch (method) {
711
+ case METHODS.ready:
712
+ clearTimeout(timer);
713
+ resolveReady();
714
+ return;
715
+ case METHODS.initialized:
716
+ pendingInitialized = initializeParams;
717
+ session.deliverInitialized();
718
+ return;
719
+ case METHODS.sizeChanged:
720
+ if (
721
+ typeof p["height"] === "number" &&
722
+ typeof p["width"] === "number"
723
+ ) {
724
+ applySize(session, { width: p["width"], height: p["height"] });
725
+ }
726
+ return;
727
+ case METHODS.error:
728
+ if (session === activeSession) reportError(p as WidgetError);
729
+ return;
730
+ case METHODS.log:
731
+ if (session === activeSession) {
732
+ options.onLog?.(p as ConsoleEntry);
733
+ emit("log", p as ConsoleEntry);
734
+ }
735
+ return;
736
+ case METHODS.widgetState:
737
+ if (session !== pendingSession && session !== activeSession)
738
+ return;
739
+ try {
740
+ if (!isPlainJson(p["state"])) return;
741
+ } catch {
742
+ return;
743
+ }
744
+ if (session === pendingSession) {
745
+ pendingWidgetState = p["state"];
746
+ hasPendingWidgetState = true;
747
+ return;
748
+ }
749
+ if (pendingSession) {
750
+ heldWidgetState = p["state"];
751
+ hasHeldWidgetState = true;
752
+ return;
753
+ }
754
+ widgetState = p["state"];
755
+ options.onWidgetState?.(widgetState);
756
+ return;
757
+ default:
758
+ return;
759
+ }
760
+ },
761
+ });
762
+ const init: InitMessage = {
763
+ type: INIT_MESSAGE,
764
+ context,
765
+ state: widgetState,
766
+ };
767
+ rendered.sendMessage(init, [channel.port2]);
768
+ };
769
+
770
+ // Widgets built on the MCP Apps SDK talk to `window.parent` rather than the port.
771
+ function onWindowMessage(event: MessageEvent) {
772
+ if (!rendered) return;
773
+ if (
774
+ event.source !== rendered.iframe.contentWindow ||
775
+ event.origin !== rendered.origin
776
+ ) {
777
+ return;
778
+ }
779
+ const data: unknown = event.data;
780
+ if ((data as { type?: unknown } | null)?.type === READY_MESSAGE) {
781
+ sendInit();
782
+ return;
783
+ }
784
+ if (peer && isJsonRpcMessage(data)) {
785
+ const target = rendered;
786
+ peer.receive(data, (message) => target.sendMessage(message));
787
+ }
788
+ }
789
+ window.addEventListener("message", onWindowMessage);
790
+
791
+ const html = buildBootstrapHtml({
792
+ hostOrigin: window.location.origin,
793
+ tokens,
794
+ ...(options.csp !== undefined ? { csp: options.csp } : {}),
795
+ ...(options.animate !== undefined ? { animate: options.animate } : {}),
796
+ ...(options.css !== undefined ? { css: options.css } : {}),
797
+ });
798
+
799
+ frame.renderHtml(html, slot).then(
800
+ (result) => {
801
+ if (sessionDisposed) {
802
+ result.dispose();
803
+ return;
804
+ }
805
+ rendered = result;
806
+ const style = result.iframe.style;
807
+ style.display = "block";
808
+ style.height = `${minHeight}px`;
809
+ style.colorScheme = tokens.colorScheme;
810
+ result.iframe.setAttribute("title", "Generated widget");
811
+ result
812
+ .fullyLoadedPromiseWithTimeout(readyTimeoutMs)
813
+ .catch((error: unknown) =>
814
+ fail(error instanceof Error ? error.message : String(error)),
815
+ );
816
+ },
817
+ (error: unknown) =>
818
+ fail(error instanceof Error ? error.message : String(error)),
819
+ );
820
+
821
+ return session;
822
+ };
823
+
824
+ activeSession = createSession(false);
825
+
826
+ type Op = { kind: "write"; chunk: string } | { kind: "run"; run: () => void };
827
+ let queue: Op[] | undefined = [];
828
+ activeSession.ready.then(
829
+ () => {
830
+ const ops = queue ?? [];
831
+ queue = undefined;
832
+ emit("ready", undefined);
833
+ for (const op of ops) {
834
+ if (op.kind === "write")
835
+ activeSession?.peer().notify(METHODS.write, { chunk: op.chunk });
836
+ else op.run();
837
+ }
838
+ },
839
+ () => {},
840
+ );
841
+
842
+ const enqueue = (run: () => void) => {
843
+ if (queue) queue.push({ kind: "run", run });
844
+ else run();
845
+ };
846
+
847
+ const request = <T>(method: string, params?: unknown) =>
848
+ new Promise<T>((resolve, reject) => {
849
+ const session = activeSession;
850
+ if (!session || disposed) {
851
+ reject(new Error("Widget disposed"));
852
+ return;
853
+ }
854
+ session.ready.then(
855
+ () =>
856
+ enqueue(() => {
857
+ session
858
+ .peer()
859
+ .request(method, params, { timeoutMs: REQUEST_TIMEOUT_MS })
860
+ .then((value) => resolve(value as T), reject);
861
+ }),
862
+ reject,
863
+ );
864
+ });
865
+
866
+ const notify = (method: string, params: unknown) => {
867
+ const session = activeSession;
868
+ if (!session || disposed) return;
869
+ enqueue(() => {
870
+ try {
871
+ session.peer().notify(method, params);
872
+ } catch {}
873
+ });
874
+ };
875
+
876
+ const replayState = (session: Session) => {
877
+ const peer = session.peer();
878
+ if (lastToolInput) {
879
+ peer.notify(
880
+ lastToolInput.partial ? METHODS.toolInputPartial : METHODS.toolInput,
881
+ { arguments: lastToolInput.args },
882
+ );
883
+ }
884
+ if (lastToolResult) peer.notify(METHODS.toolResult, lastToolResult.result);
885
+ };
886
+
887
+ const remount = async (next: string): Promise<EndResult> => {
888
+ const previous = activeSession;
889
+ // The newest replacement wins, so one still booting retires now rather than reaching the host until it settles.
890
+ pendingSession?.dispose();
891
+ const session = createSession(true);
892
+ pendingSession = session;
893
+ hasPendingWidgetState = false;
894
+ let result: EndResult;
895
+ try {
896
+ await session.ready;
897
+ if (disposed || pendingSession !== session)
898
+ throw new Error("Widget replacement superseded");
899
+ replayState(session);
900
+ result = (await session
901
+ .peer()
902
+ .request(
903
+ METHODS.replace,
904
+ { code: next },
905
+ { timeoutMs: REQUEST_TIMEOUT_MS },
906
+ )) as EndResult;
907
+ if (disposed || pendingSession !== session)
908
+ throw new Error("Widget replacement superseded");
909
+ } catch (error) {
910
+ const superseded = !disposed && pendingSession !== session;
911
+ if (pendingSession === session) {
912
+ pendingSession = undefined;
913
+ hasPendingWidgetState = false;
914
+ if (hasHeldWidgetState && !disposed) {
915
+ hasHeldWidgetState = false;
916
+ widgetState = heldWidgetState;
917
+ try {
918
+ options.onWidgetState?.(widgetState);
919
+ } catch {}
920
+ }
921
+ }
922
+ session.dispose();
923
+ if (disposed) throw new Error("Widget disposed");
924
+ throw superseded ? new Error("Widget replacement superseded") : error;
925
+ }
926
+ pendingSession = undefined;
927
+ hasHeldWidgetState = false;
928
+ activeSession = session;
929
+ if (hasPendingWidgetState) widgetState = pendingWidgetState;
930
+ // The new frame has rendered completely, so it swaps in without a fade.
931
+ session.slot.style.cssText = "display:block";
932
+ previous?.dispose();
933
+ if (hasPendingWidgetState) {
934
+ hasPendingWidgetState = false;
935
+ try {
936
+ options.onWidgetState?.(widgetState);
937
+ } catch {}
938
+ }
939
+ session.deliverInitialized();
940
+ applySize(session, result.size);
941
+ return result;
942
+ };
943
+
944
+ const handle: WidgetHandle = {
945
+ ready: activeSession.ready,
946
+ get code() {
947
+ return code;
948
+ },
949
+ get ended() {
950
+ return ended;
951
+ },
952
+ get iframe() {
953
+ return activeSession?.iframe();
954
+ },
955
+ write(chunk) {
956
+ if (disposed || !chunk) return;
957
+ if (ended) throw new Error("Cannot write after end(); use replace()");
958
+ code += chunk;
959
+ const last = queue?.[queue.length - 1];
960
+ if (queue) {
961
+ if (last?.kind === "write") last.chunk += chunk;
962
+ else queue.push({ kind: "write", chunk });
963
+ return;
964
+ }
965
+ activeSession?.peer().notify(METHODS.write, { chunk });
966
+ },
967
+ async end() {
968
+ ended = true;
969
+ const result = await request<EndResult>(METHODS.end);
970
+ emit("end", result);
971
+ return result;
972
+ },
973
+ async replace(next) {
974
+ // While a remount is pending, the frame on screen is still the one whose scripts ran.
975
+ const scriptsRan =
976
+ pendingSession !== undefined || (ended && SCRIPT_TAG.test(code));
977
+ code = next;
978
+ ended = true;
979
+ const result = scriptsRan
980
+ ? await remount(next)
981
+ : await request<EndResult>(METHODS.replace, { code: next });
982
+ emit("end", result);
983
+ return result;
984
+ },
985
+ setTheme(next) {
986
+ tokens = next;
987
+ context = { ...context, ...themeContext(next) };
988
+ notify(METHODS.hostContextChanged, themeContext(next));
989
+ // An iframe whose color-scheme differs from its document's paints an
990
+ // opaque canvas, so the element follows only after the frame switched.
991
+ const session = activeSession;
992
+ const followTheme = () => {
993
+ const iframe = session?.iframe();
994
+ if (iframe && tokens === next)
995
+ iframe.style.colorScheme = next.colorScheme;
996
+ };
997
+ request(METHODS.inspect).then(followTheme, followTheme);
998
+ },
999
+ setContext(partial) {
1000
+ context = { ...context, ...partial };
1001
+ notify(METHODS.hostContextChanged, partial);
1002
+ },
1003
+ notifyToolInput(args, notifyOptions) {
1004
+ const partial = notifyOptions?.partial ?? false;
1005
+ lastToolInput = { args, partial };
1006
+ notify(partial ? METHODS.toolInputPartial : METHODS.toolInput, {
1007
+ arguments: args,
1008
+ });
1009
+ },
1010
+ notifyToolResult(result) {
1011
+ lastToolResult = { result };
1012
+ notify(METHODS.toolResult, result);
1013
+ },
1014
+ screenshot(screenshotOptions) {
1015
+ return request<Screenshot>(METHODS.screenshot, screenshotOptions ?? {});
1016
+ },
1017
+ async inspect() {
1018
+ const inspection = await request<FrameInspection>(METHODS.inspect);
1019
+ return { ...inspection, code };
1020
+ },
1021
+ on(event, listener) {
1022
+ let set = listeners.get(event);
1023
+ if (!set) {
1024
+ set = new Set();
1025
+ listeners.set(event, set);
1026
+ }
1027
+ set.add(listener as (payload: never) => void);
1028
+ return () => set.delete(listener as (payload: never) => void);
1029
+ },
1030
+ dispose() {
1031
+ if (disposed) return;
1032
+ disposed = true;
1033
+ queue = undefined;
1034
+ pendingSession?.dispose();
1035
+ pendingSession = undefined;
1036
+ activeSession?.dispose();
1037
+ activeSession = undefined;
1038
+ listeners.clear();
1039
+ },
1040
+ };
1041
+
1042
+ internalRequests.set(handle, request);
1043
+
1044
+ if (options.code !== undefined) {
1045
+ handle.write(options.code);
1046
+ void handle.end().catch(() => {});
1047
+ }
1048
+
1049
+ return handle;
1050
+ }
1051
+
1052
+ /**
1053
+ * Clears what widgets with this `id` stored (localStorage, sessionStorage,
1054
+ * IndexedDB, Cache Storage, and cookies) by loading a hidden frame on the
1055
+ * same origin. Resolves with the storage kinds that were cleared.
1056
+ */
1057
+ export async function clearWidgetStorage(
1058
+ id: string,
1059
+ options: Pick<
1060
+ CreateWidgetOptions,
1061
+ "product" | "unsafeShimDomain" | "readyTimeoutMs"
1062
+ > = {},
1063
+ ): Promise<ClearStorageResult> {
1064
+ const container = document.createElement("div");
1065
+ container.setAttribute("aria-hidden", "true");
1066
+ container.style.cssText =
1067
+ "position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;";
1068
+ document.body.appendChild(container);
1069
+ let widget: WidgetHandle | undefined;
1070
+ try {
1071
+ widget = createWidget({ ...options, container, id });
1072
+ await widget.ready;
1073
+ return await internalRequests.get(widget)!<ClearStorageResult>(
1074
+ METHODS.clearStorage,
1075
+ );
1076
+ } finally {
1077
+ widget?.dispose();
1078
+ container.remove();
1079
+ }
1080
+ }