@assistant-ui/react-generative-ui 0.0.24 → 0.1.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 (338) hide show
  1. package/README.md +2 -0
  2. package/dist/a2ui.d.ts +1 -4
  3. package/dist/a2ui.d.ts.map +1 -1
  4. package/dist/a2ui.js +1 -4
  5. package/dist/index.d.ts +1 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -10
  8. package/dist/index.server.d.ts +1 -2
  9. package/dist/index.server.d.ts.map +1 -1
  10. package/dist/index.server.js +1 -10
  11. package/dist/ir.d.ts +1 -111
  12. package/dist/ir.d.ts.map +1 -1
  13. package/dist/ir.js +1 -175
  14. package/dist/slack.d.ts +1 -4
  15. package/dist/slack.d.ts.map +1 -1
  16. package/dist/slack.js +1 -4
  17. package/dist/teams.d.ts +1 -4
  18. package/dist/teams.d.ts.map +1 -1
  19. package/dist/teams.js +1 -4
  20. package/package.json +4 -5
  21. package/src/a2ui.ts +1 -22
  22. package/src/index.server.ts +1 -2
  23. package/src/index.test.ts +75 -0
  24. package/src/index.ts +1 -2
  25. package/src/ir.ts +1 -279
  26. package/src/slack.ts +1 -39
  27. package/src/teams.ts +1 -35
  28. package/dist/JSONGenerativeUI.client.d.ts +0 -28
  29. package/dist/JSONGenerativeUI.client.d.ts.map +0 -1
  30. package/dist/JSONGenerativeUI.client.js +0 -101
  31. package/dist/JSONGenerativeUI.client.js.map +0 -1
  32. package/dist/JSONGenerativeUI.server.d.ts +0 -21
  33. package/dist/JSONGenerativeUI.server.d.ts.map +0 -1
  34. package/dist/JSONGenerativeUI.server.js +0 -32
  35. package/dist/JSONGenerativeUI.server.js.map +0 -1
  36. package/dist/JSONGenerativeUI.shared.d.ts +0 -63
  37. package/dist/JSONGenerativeUI.shared.d.ts.map +0 -1
  38. package/dist/JSONGenerativeUI.shared.js +0 -31
  39. package/dist/JSONGenerativeUI.shared.js.map +0 -1
  40. package/dist/RadioGroupScope.d.ts +0 -6
  41. package/dist/RadioGroupScope.d.ts.map +0 -1
  42. package/dist/RadioGroupScope.js +0 -25
  43. package/dist/RadioGroupScope.js.map +0 -1
  44. package/dist/a2ui/BindingContext.d.ts +0 -11
  45. package/dist/a2ui/BindingContext.d.ts.map +0 -1
  46. package/dist/a2ui/BindingContext.js +0 -12
  47. package/dist/a2ui/BindingContext.js.map +0 -1
  48. package/dist/a2ui/PresentRenderer.d.ts +0 -11
  49. package/dist/a2ui/PresentRenderer.d.ts.map +0 -1
  50. package/dist/a2ui/PresentRenderer.js +0 -39
  51. package/dist/a2ui/PresentRenderer.js.map +0 -1
  52. package/dist/a2ui/convert.d.ts +0 -15
  53. package/dist/a2ui/convert.d.ts.map +0 -1
  54. package/dist/a2ui/convert.js +0 -728
  55. package/dist/a2ui/convert.js.map +0 -1
  56. package/dist/a2ui/dataModel.d.ts +0 -9
  57. package/dist/a2ui/dataModel.d.ts.map +0 -1
  58. package/dist/a2ui/dataModel.js +0 -44
  59. package/dist/a2ui/dataModel.js.map +0 -1
  60. package/dist/a2ui/reducer.d.ts +0 -8
  61. package/dist/a2ui/reducer.d.ts.map +0 -1
  62. package/dist/a2ui/reducer.js +0 -242
  63. package/dist/a2ui/reducer.js.map +0 -1
  64. package/dist/a2ui/snapshot.d.ts +0 -3
  65. package/dist/a2ui/snapshot.d.ts.map +0 -1
  66. package/dist/a2ui/snapshot.js +0 -33
  67. package/dist/a2ui/snapshot.js.map +0 -1
  68. package/dist/a2ui/types.d.ts +0 -88
  69. package/dist/a2ui/types.d.ts.map +0 -1
  70. package/dist/a2ui/types.js +0 -6
  71. package/dist/a2ui/types.js.map +0 -1
  72. package/dist/a2ui/valueFunctions.d.ts +0 -16
  73. package/dist/a2ui/valueFunctions.d.ts.map +0 -1
  74. package/dist/a2ui/valueFunctions.js +0 -332
  75. package/dist/a2ui/valueFunctions.js.map +0 -1
  76. package/dist/actionRegistry.d.ts +0 -29
  77. package/dist/actionRegistry.d.ts.map +0 -1
  78. package/dist/actionRegistry.js +0 -41
  79. package/dist/actionRegistry.js.map +0 -1
  80. package/dist/answeredValues.d.ts +0 -16
  81. package/dist/answeredValues.d.ts.map +0 -1
  82. package/dist/answeredValues.js +0 -37
  83. package/dist/answeredValues.js.map +0 -1
  84. package/dist/buildPresentParameters.d.ts +0 -21
  85. package/dist/buildPresentParameters.d.ts.map +0 -1
  86. package/dist/buildPresentParameters.js +0 -95
  87. package/dist/buildPresentParameters.js.map +0 -1
  88. package/dist/constants.d.ts +0 -15
  89. package/dist/constants.d.ts.map +0 -1
  90. package/dist/constants.js +0 -19
  91. package/dist/constants.js.map +0 -1
  92. package/dist/convert/boundSpec.d.ts +0 -48
  93. package/dist/convert/boundSpec.d.ts.map +0 -1
  94. package/dist/convert/boundSpec.js +0 -108
  95. package/dist/convert/boundSpec.js.map +0 -1
  96. package/dist/convert/copyBounded.d.ts +0 -25
  97. package/dist/convert/copyBounded.d.ts.map +0 -1
  98. package/dist/convert/copyBounded.js +0 -38
  99. package/dist/convert/copyBounded.js.map +0 -1
  100. package/dist/convert/isElement.d.ts +0 -3
  101. package/dist/convert/isElement.d.ts.map +0 -1
  102. package/dist/convert/isElement.js +0 -6
  103. package/dist/convert/isElement.js.map +0 -1
  104. package/dist/convert/takeRun.d.ts +0 -6
  105. package/dist/convert/takeRun.d.ts.map +0 -1
  106. package/dist/convert/takeRun.js +0 -20
  107. package/dist/convert/takeRun.js.map +0 -1
  108. package/dist/defineGenerativeComponents.d.ts +0 -29
  109. package/dist/defineGenerativeComponents.d.ts.map +0 -1
  110. package/dist/defineGenerativeComponents.js +0 -34
  111. package/dist/defineGenerativeComponents.js.map +0 -1
  112. package/dist/fieldReferences.d.ts +0 -9
  113. package/dist/fieldReferences.d.ts.map +0 -1
  114. package/dist/fieldReferences.js +0 -53
  115. package/dist/fieldReferences.js.map +0 -1
  116. package/dist/generativeUIToJSX.d.ts +0 -18
  117. package/dist/generativeUIToJSX.d.ts.map +0 -1
  118. package/dist/generativeUIToJSX.js +0 -76
  119. package/dist/generativeUIToJSX.js.map +0 -1
  120. package/dist/index.shared.d.ts +0 -15
  121. package/dist/index.shared.d.ts.map +0 -1
  122. package/dist/index.shared.js +0 -9
  123. package/dist/ir.js.map +0 -1
  124. package/dist/renderGenerativeUI.d.ts +0 -14
  125. package/dist/renderGenerativeUI.d.ts.map +0 -1
  126. package/dist/renderGenerativeUI.js +0 -83
  127. package/dist/renderGenerativeUI.js.map +0 -1
  128. package/dist/scopeSchema.d.ts +0 -6
  129. package/dist/scopeSchema.d.ts.map +0 -1
  130. package/dist/scopeSchema.js +0 -56
  131. package/dist/scopeSchema.js.map +0 -1
  132. package/dist/slack/constants.d.ts +0 -93
  133. package/dist/slack/constants.d.ts.map +0 -1
  134. package/dist/slack/constants.js +0 -146
  135. package/dist/slack/constants.js.map +0 -1
  136. package/dist/slack/decodeBlockAction.d.ts +0 -10
  137. package/dist/slack/decodeBlockAction.d.ts.map +0 -1
  138. package/dist/slack/decodeBlockAction.js +0 -129
  139. package/dist/slack/decodeBlockAction.js.map +0 -1
  140. package/dist/slack/fromSlackBlocks.d.ts +0 -27
  141. package/dist/slack/fromSlackBlocks.d.ts.map +0 -1
  142. package/dist/slack/fromSlackBlocks.js +0 -483
  143. package/dist/slack/fromSlackBlocks.js.map +0 -1
  144. package/dist/slack/toSlackBlocks.d.ts +0 -4
  145. package/dist/slack/toSlackBlocks.d.ts.map +0 -1
  146. package/dist/slack/toSlackBlocks.js +0 -901
  147. package/dist/slack/toSlackBlocks.js.map +0 -1
  148. package/dist/slack/types.d.ts +0 -204
  149. package/dist/slack/types.d.ts.map +0 -1
  150. package/dist/slack/types.js +0 -0
  151. package/dist/teams/constants.d.ts +0 -34
  152. package/dist/teams/constants.d.ts.map +0 -1
  153. package/dist/teams/constants.js +0 -68
  154. package/dist/teams/constants.js.map +0 -1
  155. package/dist/teams/decodeSubmitData.d.ts +0 -26
  156. package/dist/teams/decodeSubmitData.d.ts.map +0 -1
  157. package/dist/teams/decodeSubmitData.js +0 -70
  158. package/dist/teams/decodeSubmitData.js.map +0 -1
  159. package/dist/teams/temporalId.d.ts +0 -5
  160. package/dist/teams/temporalId.d.ts.map +0 -1
  161. package/dist/teams/temporalId.js +0 -28
  162. package/dist/teams/temporalId.js.map +0 -1
  163. package/dist/teams/toAdaptiveCard.d.ts +0 -45
  164. package/dist/teams/toAdaptiveCard.d.ts.map +0 -1
  165. package/dist/teams/toAdaptiveCard.js +0 -642
  166. package/dist/teams/toAdaptiveCard.js.map +0 -1
  167. package/dist/teams/toTeamsAttachments.d.ts +0 -10
  168. package/dist/teams/toTeamsAttachments.d.ts.map +0 -1
  169. package/dist/teams/toTeamsAttachments.js +0 -99
  170. package/dist/teams/toTeamsAttachments.js.map +0 -1
  171. package/dist/teams/types.d.ts +0 -224
  172. package/dist/teams/types.d.ts.map +0 -1
  173. package/dist/teams/types.js +0 -0
  174. package/dist/temporal.d.ts +0 -33
  175. package/dist/temporal.d.ts.map +0 -1
  176. package/dist/temporal.js +0 -153
  177. package/dist/temporal.js.map +0 -1
  178. package/dist/types.d.ts +0 -99
  179. package/dist/types.d.ts.map +0 -1
  180. package/dist/types.js +0 -0
  181. package/dist/vocabulary/alert.d.ts +0 -32
  182. package/dist/vocabulary/alert.d.ts.map +0 -1
  183. package/dist/vocabulary/alert.js +0 -160
  184. package/dist/vocabulary/alert.js.map +0 -1
  185. package/dist/vocabulary/collectFormValues.d.ts +0 -23
  186. package/dist/vocabulary/collectFormValues.d.ts.map +0 -1
  187. package/dist/vocabulary/collectFormValues.js +0 -44
  188. package/dist/vocabulary/collectFormValues.js.map +0 -1
  189. package/dist/vocabulary/data.d.ts +0 -66
  190. package/dist/vocabulary/data.d.ts.map +0 -1
  191. package/dist/vocabulary/data.js +0 -503
  192. package/dist/vocabulary/data.js.map +0 -1
  193. package/dist/vocabulary/dispatch.d.ts +0 -9
  194. package/dist/vocabulary/dispatch.d.ts.map +0 -1
  195. package/dist/vocabulary/dispatch.js +0 -50
  196. package/dist/vocabulary/dispatch.js.map +0 -1
  197. package/dist/vocabulary/fact.d.ts +0 -19
  198. package/dist/vocabulary/fact.d.ts.map +0 -1
  199. package/dist/vocabulary/fact.js +0 -51
  200. package/dist/vocabulary/fact.js.map +0 -1
  201. package/dist/vocabulary/form.d.ts +0 -11
  202. package/dist/vocabulary/form.d.ts.map +0 -1
  203. package/dist/vocabulary/form.js +0 -23
  204. package/dist/vocabulary/form.js.map +0 -1
  205. package/dist/vocabulary/formatValue.d.ts +0 -7
  206. package/dist/vocabulary/formatValue.d.ts.map +0 -1
  207. package/dist/vocabulary/formatValue.js +0 -76
  208. package/dist/vocabulary/formatValue.js.map +0 -1
  209. package/dist/vocabulary/icon.d.ts +0 -41
  210. package/dist/vocabulary/icon.d.ts.map +0 -1
  211. package/dist/vocabulary/icon.js +0 -272
  212. package/dist/vocabulary/icon.js.map +0 -1
  213. package/dist/vocabulary/index.d.ts +0 -12
  214. package/dist/vocabulary/index.d.ts.map +0 -1
  215. package/dist/vocabulary/index.js +0 -36
  216. package/dist/vocabulary/index.js.map +0 -1
  217. package/dist/vocabulary/interactive.d.ts +0 -230
  218. package/dist/vocabulary/interactive.d.ts.map +0 -1
  219. package/dist/vocabulary/interactive.js +0 -584
  220. package/dist/vocabulary/interactive.js.map +0 -1
  221. package/dist/vocabulary/layout.d.ts +0 -79
  222. package/dist/vocabulary/layout.d.ts.map +0 -1
  223. package/dist/vocabulary/layout.js +0 -139
  224. package/dist/vocabulary/layout.js.map +0 -1
  225. package/dist/vocabulary/list.d.ts +0 -14
  226. package/dist/vocabulary/list.d.ts.map +0 -1
  227. package/dist/vocabulary/list.js +0 -50
  228. package/dist/vocabulary/list.js.map +0 -1
  229. package/dist/vocabulary/media.d.ts +0 -25
  230. package/dist/vocabulary/media.d.ts.map +0 -1
  231. package/dist/vocabulary/media.js +0 -42
  232. package/dist/vocabulary/media.js.map +0 -1
  233. package/dist/vocabulary/text.d.ts +0 -57
  234. package/dist/vocabulary/text.d.ts.map +0 -1
  235. package/dist/vocabulary/text.js +0 -49
  236. package/dist/vocabulary/text.js.map +0 -1
  237. package/dist/vocabulary/toTextContent.d.ts +0 -2
  238. package/dist/vocabulary/toTextContent.d.ts.map +0 -1
  239. package/dist/vocabulary/toTextContent.js +0 -6
  240. package/dist/vocabulary/toTextContent.js.map +0 -1
  241. package/src/JSONGenerativeUI.client.tsx +0 -168
  242. package/src/JSONGenerativeUI.server.tsx +0 -39
  243. package/src/JSONGenerativeUI.shared.ts +0 -90
  244. package/src/JSONGenerativeUI.test.tsx +0 -1298
  245. package/src/RadioGroupScope.server.test.tsx +0 -24
  246. package/src/RadioGroupScope.tsx +0 -34
  247. package/src/a2ui/BindingContext.ts +0 -26
  248. package/src/a2ui/PresentRenderer.tsx +0 -83
  249. package/src/a2ui/convert.test.ts +0 -2923
  250. package/src/a2ui/convert.ts +0 -1207
  251. package/src/a2ui/dataModel.ts +0 -69
  252. package/src/a2ui/reducer.test.ts +0 -422
  253. package/src/a2ui/reducer.ts +0 -363
  254. package/src/a2ui/snapshot.test.ts +0 -52
  255. package/src/a2ui/snapshot.ts +0 -52
  256. package/src/a2ui/types.ts +0 -114
  257. package/src/a2ui/valueFunctions.test.ts +0 -271
  258. package/src/a2ui/valueFunctions.ts +0 -523
  259. package/src/actionRegistry.test.ts +0 -90
  260. package/src/actionRegistry.ts +0 -88
  261. package/src/answeredValues.test.ts +0 -33
  262. package/src/answeredValues.tsx +0 -70
  263. package/src/buildPresentParameters.test.ts +0 -246
  264. package/src/buildPresentParameters.ts +0 -130
  265. package/src/constants.ts +0 -17
  266. package/src/convert/boundSpec.test.ts +0 -192
  267. package/src/convert/boundSpec.ts +0 -134
  268. package/src/convert/copyBounded.test.ts +0 -131
  269. package/src/convert/copyBounded.ts +0 -38
  270. package/src/convert/isElement.ts +0 -6
  271. package/src/convert/takeRun.test.ts +0 -64
  272. package/src/convert/takeRun.ts +0 -19
  273. package/src/defineGenerativeComponents.ts +0 -38
  274. package/src/fieldReferences.test.ts +0 -66
  275. package/src/fieldReferences.ts +0 -83
  276. package/src/generativeUIToJSX.test.ts +0 -268
  277. package/src/generativeUIToJSX.ts +0 -152
  278. package/src/index.server.test.ts +0 -106
  279. package/src/index.shared.ts +0 -61
  280. package/src/ir.test.ts +0 -203
  281. package/src/renderGenerativeUI.test.tsx +0 -373
  282. package/src/renderGenerativeUI.tsx +0 -165
  283. package/src/scopeSchema.test.ts +0 -72
  284. package/src/scopeSchema.ts +0 -71
  285. package/src/slack/a2uiSlack.test.ts +0 -198
  286. package/src/slack/constants.ts +0 -194
  287. package/src/slack/datePicker.test.ts +0 -612
  288. package/src/slack/decodeBlockAction.test.ts +0 -233
  289. package/src/slack/decodeBlockAction.ts +0 -232
  290. package/src/slack/fromSlackBlocks.test.ts +0 -1465
  291. package/src/slack/fromSlackBlocks.ts +0 -789
  292. package/src/slack/toSlackBlocks.test.ts +0 -3065
  293. package/src/slack/toSlackBlocks.ts +0 -1818
  294. package/src/slack/types.ts +0 -264
  295. package/src/teams/constants.ts +0 -93
  296. package/src/teams/decodeSubmitData.test.ts +0 -397
  297. package/src/teams/decodeSubmitData.ts +0 -110
  298. package/src/teams/temporal.test.ts +0 -766
  299. package/src/teams/temporalId.ts +0 -56
  300. package/src/teams/toAdaptiveCard.test.ts +0 -1897
  301. package/src/teams/toAdaptiveCard.ts +0 -1057
  302. package/src/teams/toTeamsAttachments.test.ts +0 -74
  303. package/src/teams/toTeamsAttachments.ts +0 -133
  304. package/src/teams/types.ts +0 -266
  305. package/src/temporal.test.ts +0 -337
  306. package/src/temporal.ts +0 -263
  307. package/src/types.ts +0 -119
  308. package/src/vocabulary/alert.test.tsx +0 -427
  309. package/src/vocabulary/alert.tsx +0 -217
  310. package/src/vocabulary/collectFormValues.test.ts +0 -389
  311. package/src/vocabulary/collectFormValues.ts +0 -116
  312. package/src/vocabulary/data.test.tsx +0 -858
  313. package/src/vocabulary/data.tsx +0 -819
  314. package/src/vocabulary/datepicker.dom.test.tsx +0 -1457
  315. package/src/vocabulary/dispatch.ts +0 -83
  316. package/src/vocabulary/fact.test.tsx +0 -94
  317. package/src/vocabulary/fact.tsx +0 -54
  318. package/src/vocabulary/form.test.tsx +0 -113
  319. package/src/vocabulary/form.tsx +0 -39
  320. package/src/vocabulary/formatValue.test.ts +0 -72
  321. package/src/vocabulary/formatValue.ts +0 -127
  322. package/src/vocabulary/icon.test.tsx +0 -64
  323. package/src/vocabulary/icon.tsx +0 -174
  324. package/src/vocabulary/index.test.ts +0 -65
  325. package/src/vocabulary/index.ts +0 -33
  326. package/src/vocabulary/interactive.dispatch.test.tsx +0 -454
  327. package/src/vocabulary/interactive.dom.test.tsx +0 -1747
  328. package/src/vocabulary/interactive.test.tsx +0 -377
  329. package/src/vocabulary/interactive.tsx +0 -1109
  330. package/src/vocabulary/layout.test.tsx +0 -323
  331. package/src/vocabulary/layout.tsx +0 -247
  332. package/src/vocabulary/list.test.tsx +0 -222
  333. package/src/vocabulary/list.tsx +0 -50
  334. package/src/vocabulary/media.test.tsx +0 -85
  335. package/src/vocabulary/media.tsx +0 -56
  336. package/src/vocabulary/text.test.tsx +0 -102
  337. package/src/vocabulary/text.tsx +0 -60
  338. package/src/vocabulary/toTextContent.ts +0 -2
@@ -1,1298 +0,0 @@
1
- import { convertSurfaceToUISpec } from "./a2ui/convert";
2
- import { surfaceToOperations } from "./a2ui/snapshot";
3
- import { applyA2uiOperations } from "./a2ui/reducer";
4
- /** @vitest-environment jsdom */
5
- import { describe, it, expect, vi } from "vitest";
6
- import { act } from "react";
7
- import { createRoot } from "react-dom/client";
8
- import { renderToStaticMarkup } from "react-dom/server";
9
- import { z } from "zod";
10
- import { parsePartialJsonObject } from "assistant-stream/utils";
11
- import { JSONGenerativeUI as ClientGenUI } from "./JSONGenerativeUI.client";
12
- import { JSONGenerativeUI as ServerGenUI } from "./JSONGenerativeUI.server";
13
- import { defineGenerativeComponents } from "./defineGenerativeComponents";
14
- import { createActionRegistry, type ActionRegistry } from "./actionRegistry";
15
- import type { GenerativeUIDispatch, GenerativeUILibrary } from "./types";
16
- import { defaultGenerativeUILibrary } from "./vocabulary";
17
-
18
- const { evaluateValue } = vi.hoisted(() => ({ evaluateValue: vi.fn() }));
19
- vi.mock("./a2ui/valueFunctions", async (importOriginal) => {
20
- const original =
21
- await importOriginal<typeof import("./a2ui/valueFunctions")>();
22
- return {
23
- ...original,
24
- evaluateA2uiValueFunction: (
25
- ...args: Parameters<typeof original.evaluateA2uiValueFunction>
26
- ) => {
27
- evaluateValue(...args);
28
- return original.evaluateA2uiValueFunction(...args);
29
- },
30
- };
31
- });
32
-
33
- (
34
- globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
35
- ).IS_REACT_ACT_ENVIRONMENT = true;
36
-
37
- const library: GenerativeUILibrary = {
38
- Card: {
39
- description: "A card.",
40
- properties: z.object({ title: z.string() }),
41
- render: ({ title, children }: any) => (
42
- <section data-title={title}>{children}</section>
43
- ),
44
- },
45
- Button: {
46
- description: "A button.",
47
- properties: z.object({ label: z.string() }),
48
- render: ({ label }: any) => <button>{label}</button>,
49
- },
50
- };
51
-
52
- const renderTool = (
53
- tool: any,
54
- args: unknown,
55
- props: Record<string, unknown> = {},
56
- ) =>
57
- renderToStaticMarkup(
58
- <>{tool.render({ args, status: { type: "complete" }, ...props })}</>,
59
- );
60
-
61
- const captureActionDispatch = (
62
- toolType: "present" | "promptUser",
63
- actions: ActionRegistry | undefined,
64
- props: Record<string, unknown> = {},
65
- ): GenerativeUIDispatch => {
66
- let dispatch: GenerativeUIDispatch | undefined;
67
- const ui = new ClientGenUI({
68
- library: {
69
- Action: {
70
- description: "An action target.",
71
- properties: z.object({}),
72
- render: ({ $dispatch }: any) => {
73
- dispatch = $dispatch;
74
- return null;
75
- },
76
- },
77
- },
78
- ...(actions ? { actions } : {}),
79
- });
80
-
81
- renderTool(
82
- toolType === "present" ? ui.present() : ui.promptUser(),
83
- { $type: "Action", $action: { type: "answer" } },
84
- props,
85
- );
86
- if (!dispatch) throw new Error("Expected an action dispatch.");
87
- return dispatch;
88
- };
89
-
90
- describe("JSONGenerativeUI — client build", () => {
91
- const ui = new ClientGenUI({ library });
92
-
93
- it("present is a frontend tool with matching parameters, render, and execute", () => {
94
- const tool = ui.present();
95
- expect(tool.type).toBe("frontend");
96
- expect(typeof tool.execute).toBe("function");
97
- expect(typeof tool.render).toBe("function");
98
- expect(tool.unstable_backendDefault).toEqual({ parameters: true });
99
- expect((tool.parameters as any).properties.$type.enum).toEqual([
100
- "Card",
101
- "Button",
102
- ]);
103
- });
104
-
105
- it("present renders the model's tree against the library", () => {
106
- const html = renderTool(ui.present(), { $type: "Card", title: "Hi" });
107
- expect(html).toContain('<section data-title="Hi"></section>');
108
- });
109
-
110
- it("wraps the tree so top-level siblings are spaced by the surface", () => {
111
- const html = renderTool(ui.present(), [
112
- { $type: "Card", title: "One" },
113
- { $type: "Card", title: "Two" },
114
- ]);
115
- expect(html.match(/data-aui="root"/g)).toHaveLength(1);
116
- expect(html.indexOf('data-title="One"')).toBeLessThan(
117
- html.indexOf('data-title="Two"'),
118
- );
119
- });
120
-
121
- it("leaves the surface childless until the stream produces a node", () => {
122
- // `:empty` is what hides it, so the contract is that it has no children.
123
- expect(renderTool(ui.present(), undefined)).toBe(
124
- '<div data-aui="root"></div>',
125
- );
126
- expect(renderTool(ui.present(), [])).toBe('<div data-aui="root"></div>');
127
- });
128
-
129
- it("prompt_user is a human tool that renders the tree (no execute)", () => {
130
- const tool = ui.promptUser();
131
- expect(tool.type).toBe("human");
132
- expect(tool.unstable_backendDefault).toEqual({ parameters: true });
133
- expect((tool as any).execute).toBeUndefined();
134
- const html = renderTool(tool, { $type: "Button", label: "ok" });
135
- expect(html).toContain("<button>ok</button>");
136
- });
137
-
138
- it("leaves present output live when the tool call has a result", () => {
139
- const interactive = new ClientGenUI({
140
- library: defaultGenerativeUILibrary,
141
- });
142
- const html = renderTool(
143
- interactive.present(),
144
- { $type: "Input", name: "note", defaultValue: "model default" },
145
- {
146
- result: { submitted: true },
147
- unstable_interactions: {
148
- entries: [
149
- {
150
- type: "action",
151
- occurredAt: 1,
152
- payload: { $input: { note: "submitted" } },
153
- },
154
- ],
155
- },
156
- },
157
- );
158
-
159
- expect(html).not.toContain('data-aui="answered"');
160
- expect(html).toContain('value="model default"');
161
- });
162
-
163
- it("locks an answered prompt without recorded actions at its defaults", () => {
164
- const interactive = new ClientGenUI({
165
- library: defaultGenerativeUILibrary,
166
- });
167
- const html = renderTool(
168
- interactive.promptUser(),
169
- { $type: "Input", name: "note", defaultValue: "model default" },
170
- { result: { submitted: true } },
171
- );
172
-
173
- expect(html).toContain('<fieldset disabled="" data-aui="answered">');
174
- expect(html).toContain('value="model default"');
175
- });
176
-
177
- it("locks an answered prompt and restores submitted Form values", async () => {
178
- const handler = vi.fn();
179
- const interactive = new ClientGenUI({
180
- library: defaultGenerativeUILibrary,
181
- actions: createActionRegistry({ save: handler }),
182
- });
183
- const container = document.createElement("div");
184
- const root = createRoot(container);
185
-
186
- try {
187
- await act(async () => {
188
- root.render(
189
- (interactive.promptUser() as any).render({
190
- args: {
191
- $type: "Form",
192
- $action: { type: "save" },
193
- children: [
194
- {
195
- $type: "Input",
196
- name: "message",
197
- defaultValue: "model message",
198
- },
199
- {
200
- $type: "Select",
201
- name: "role",
202
- defaultValue: "viewer",
203
- options: [
204
- { label: "Viewer", value: "viewer" },
205
- { label: "Editor", value: "editor" },
206
- ],
207
- },
208
- {
209
- $type: "DatePicker",
210
- name: "date",
211
- value: "2026-01-01",
212
- },
213
- {
214
- $type: "Checkbox",
215
- name: "updates",
216
- label: "Updates",
217
- defaultChecked: false,
218
- },
219
- {
220
- $type: "RadioGroup",
221
- name: "plan",
222
- defaultValue: "basic",
223
- options: [
224
- { label: "Basic", value: "basic" },
225
- { label: "Pro", value: "pro" },
226
- ],
227
- },
228
- {
229
- $type: "CheckboxGroup",
230
- name: "toppings",
231
- defaultValue: ["olives"],
232
- options: [
233
- { label: "Basil", value: "basil" },
234
- { label: "Olives", value: "olives" },
235
- { label: "Onion", value: "onion" },
236
- ],
237
- },
238
- { $type: "Button", label: "Save", submit: true },
239
- ],
240
- },
241
- status: { type: "complete" },
242
- toolCallId: "answered-form",
243
- toolName: "prompt_user",
244
- argsText: "",
245
- addResult: vi.fn(),
246
- result: { saved: true },
247
- unstable_interactions: {
248
- entries: [
249
- {
250
- type: "action",
251
- occurredAt: 1,
252
- payload: { $input: { message: "older value" } },
253
- },
254
- {
255
- type: "action",
256
- occurredAt: 2,
257
- payload: {
258
- $input: {
259
- message: "submitted message",
260
- role: "editor",
261
- date: "2026-02-03",
262
- updates: true,
263
- plan: "pro",
264
- toppings: ["basil", "onion"],
265
- },
266
- },
267
- },
268
- ],
269
- },
270
- }),
271
- );
272
- });
273
-
274
- const answered = container.querySelector('fieldset[data-aui="answered"]');
275
- const form = container.querySelector("form");
276
- const message = container.querySelector<HTMLInputElement>(
277
- 'input[name="message"]',
278
- );
279
- const role = container.querySelector<HTMLSelectElement>(
280
- 'select[name="role"]',
281
- );
282
- const date =
283
- container.querySelector<HTMLInputElement>('input[name="date"]');
284
- const updates = container.querySelector<HTMLInputElement>(
285
- 'input[name="updates"]',
286
- );
287
- const pro = container.querySelector<HTMLInputElement>(
288
- 'input[type="radio"][value="pro"]',
289
- );
290
- const toppings = Array.from(
291
- container.querySelectorAll<HTMLInputElement>('input[name="toppings"]'),
292
- );
293
- const button = container.querySelector("button");
294
- if (
295
- !answered ||
296
- !form ||
297
- !message ||
298
- !role ||
299
- !date ||
300
- !updates ||
301
- !pro ||
302
- !button
303
- ) {
304
- throw new Error("Expected answered prompt controls to render.");
305
- }
306
-
307
- expect(message.value).toBe("submitted message");
308
- expect(role.value).toBe("editor");
309
- expect(date.value).toBe("2026-02-03");
310
- expect(updates.checked).toBe(true);
311
- expect(pro.checked).toBe(true);
312
- expect(toppings.map((input) => input.checked)).toEqual([
313
- true,
314
- false,
315
- true,
316
- ]);
317
- for (const control of [
318
- ...container.querySelectorAll("input, select, textarea, button"),
319
- ]) {
320
- expect(control.matches(":disabled")).toBe(true);
321
- }
322
-
323
- await act(async () => {
324
- button.click();
325
- form.dispatchEvent(
326
- new Event("submit", { bubbles: true, cancelable: true }),
327
- );
328
- });
329
-
330
- expect(handler).not.toHaveBeenCalled();
331
- } finally {
332
- await act(async () => root.unmount());
333
- }
334
- });
335
-
336
- it("keeps an unanswered prompt live", async () => {
337
- const handler = vi.fn();
338
- const interactive = new ClientGenUI({
339
- library: defaultGenerativeUILibrary,
340
- actions: createActionRegistry({ save: handler }),
341
- });
342
- const container = document.createElement("div");
343
- const root = createRoot(container);
344
-
345
- try {
346
- await act(async () => {
347
- root.render(
348
- (interactive.promptUser() as any).render({
349
- args: {
350
- $type: "Button",
351
- label: "Save",
352
- $action: { type: "save" },
353
- },
354
- status: { type: "complete" },
355
- toolCallId: "unanswered-button",
356
- toolName: "prompt_user",
357
- argsText: "",
358
- addResult: vi.fn(),
359
- }),
360
- );
361
- });
362
-
363
- const button = container.querySelector("button");
364
- if (!button) throw new Error("Expected an unanswered prompt button.");
365
- expect(container.querySelector('[data-aui="answered"]')).toBeNull();
366
- await act(async () => button.click());
367
- expect(handler).toHaveBeenCalledWith({ payload: { type: "save" } });
368
- } finally {
369
- await act(async () => root.unmount());
370
- }
371
- });
372
-
373
- it.each(["running", "incomplete"])(
374
- "holds prompt controls back for %s arguments",
375
- (type) => {
376
- expect(
377
- renderTool(
378
- ui.promptUser(),
379
- { $type: "Button", label: "Answer" },
380
- { status: { type } },
381
- ),
382
- ).toBe('<div data-aui="root"></div>');
383
- },
384
- );
385
-
386
- it("holds back a pending prompt whose cancelled argument stream is partial", () => {
387
- const args = parsePartialJsonObject('{"$type":"Button","label":"Ans');
388
- expect(
389
- renderTool(ui.promptUser(), args, {
390
- status: { type: "requires-action", reason: "tool-calls" },
391
- }),
392
- ).toBe('<div data-aui="root"></div>');
393
- expect(
394
- renderTool(
395
- ui.promptUser(),
396
- parsePartialJsonObject('{"$type":"Button","label":"Answer"}'),
397
- { status: { type: "requires-action", reason: "tool-calls" } },
398
- ),
399
- ).toContain("<button>Answer</button>");
400
- });
401
-
402
- it("records actions before dispatching them without waiting for recording", () => {
403
- const recording = vi.fn(() => new Promise<void>(() => {}));
404
- const handler = vi.fn(() => "accepted");
405
- const dispatch = captureActionDispatch(
406
- "present",
407
- createActionRegistry({ answer: handler }),
408
- { unstable_recordInteraction: recording },
409
- );
410
- const action = { type: "answer", $input: { choice: "yes" } };
411
-
412
- expect(dispatch(action)).toBe("accepted");
413
- expect(recording).toHaveBeenCalledWith({
414
- type: "action",
415
- payload: action,
416
- });
417
- expect(handler).toHaveBeenCalledWith({ payload: action });
418
- expect(recording.mock.invocationCallOrder[0]).toBeLessThan(
419
- handler.mock.invocationCallOrder[0]!,
420
- );
421
- });
422
-
423
- it("keeps dispatch intact when action recording rejects", async () => {
424
- const recording = vi.fn(() => Promise.reject(new Error("unavailable")));
425
- const handler = vi.fn(() => "accepted");
426
- const dispatch = captureActionDispatch(
427
- "present",
428
- createActionRegistry({ answer: handler }),
429
- { unstable_recordInteraction: recording },
430
- );
431
- const action = { type: "answer" };
432
-
433
- expect(dispatch(action)).toBe("accepted");
434
- await Promise.resolve();
435
- expect(handler).toHaveBeenCalledWith({ payload: action });
436
- });
437
-
438
- it("keeps dispatch intact when action recording is unavailable", () => {
439
- const handler = vi.fn(() => "accepted");
440
- const dispatch = captureActionDispatch(
441
- "present",
442
- createActionRegistry({ answer: handler }),
443
- );
444
- const action = { type: "answer" };
445
-
446
- expect(dispatch(action)).toBe("accepted");
447
- expect(handler).toHaveBeenCalledWith({ payload: action });
448
- });
449
-
450
- it("does not record actions when no registry is configured", () => {
451
- const recording = vi.fn(() => Promise.resolve());
452
- let dispatch: GenerativeUIDispatch | undefined;
453
- const ui = new ClientGenUI({
454
- library: {
455
- Action: {
456
- description: "An action target.",
457
- properties: z.object({}),
458
- render: ({ $dispatch }: any) => {
459
- dispatch = $dispatch;
460
- return null;
461
- },
462
- },
463
- },
464
- });
465
-
466
- renderTool(
467
- ui.present(),
468
- { $type: "Action", $action: { type: "answer" } },
469
- { unstable_recordInteraction: recording },
470
- );
471
-
472
- expect(dispatch).toBeUndefined();
473
- expect(recording).not.toHaveBeenCalled();
474
- });
475
-
476
- it("completes prompt_user with a synchronous action result", async () => {
477
- const addResult = vi.fn();
478
- const dispatch = captureActionDispatch(
479
- "promptUser",
480
- createActionRegistry({ answer: () => ({ choice: "yes" }) }),
481
- { addResult },
482
- );
483
-
484
- expect(dispatch({ type: "answer" })).toEqual({ choice: "yes" });
485
- await Promise.resolve();
486
- expect(addResult).toHaveBeenCalledWith({ choice: "yes" });
487
- });
488
-
489
- it("completes prompt_user with an asynchronous action result", async () => {
490
- const addResult = vi.fn();
491
- const response = Promise.resolve({ choice: "yes" });
492
- const dispatch = captureActionDispatch(
493
- "promptUser",
494
- createActionRegistry({ answer: () => response }),
495
- { addResult },
496
- );
497
-
498
- expect(dispatch({ type: "answer" })).toBe(response);
499
- await response;
500
- await Promise.resolve();
501
- expect(addResult).toHaveBeenCalledWith({ choice: "yes" });
502
- });
503
-
504
- it("completes prompt_user once when delayed action results overlap", async () => {
505
- const addResult = vi.fn();
506
- let resolveFirst!: (response: { choice: string }) => void;
507
- let resolveSecond!: (response: { choice: string }) => void;
508
- const first = new Promise<{ choice: string }>((resolve) => {
509
- resolveFirst = resolve;
510
- });
511
- const second = new Promise<{ choice: string }>((resolve) => {
512
- resolveSecond = resolve;
513
- });
514
- const responses = [first, second];
515
- const dispatch = captureActionDispatch(
516
- "promptUser",
517
- createActionRegistry({ answer: () => responses.shift() }),
518
- { addResult },
519
- );
520
-
521
- const firstAction = dispatch({ type: "answer", choice: "first" });
522
- const secondAction = dispatch({ type: "answer", choice: "second" });
523
- resolveFirst({ choice: "first" });
524
- await firstAction;
525
- await Promise.resolve();
526
- resolveSecond({ choice: "second" });
527
- await secondAction;
528
- await Promise.resolve();
529
-
530
- expect(addResult).toHaveBeenCalledTimes(1);
531
- expect(addResult).toHaveBeenCalledWith({ choice: "first" });
532
- });
533
-
534
- it("does not complete prompt_user when the action result is undefined", async () => {
535
- const addResult = vi.fn();
536
- const dispatch = captureActionDispatch(
537
- "promptUser",
538
- createActionRegistry({ answer: () => undefined }),
539
- { addResult },
540
- );
541
-
542
- expect(dispatch({ type: "answer" })).toBeUndefined();
543
- await Promise.resolve();
544
- expect(addResult).not.toHaveBeenCalled();
545
- });
546
-
547
- it("does not complete prompt_user when the action handler rejects", async () => {
548
- const addResult = vi.fn();
549
- const response = Promise.reject(new Error("rejected"));
550
- const dispatch = captureActionDispatch(
551
- "promptUser",
552
- createActionRegistry({ answer: () => response }),
553
- { addResult },
554
- );
555
-
556
- await expect(Promise.resolve(dispatch({ type: "answer" }))).rejects.toThrow(
557
- "rejected",
558
- );
559
- await Promise.resolve();
560
- expect(addResult).not.toHaveBeenCalled();
561
- });
562
-
563
- it("does not provide an action dispatch when prompt_user already has a result", () => {
564
- let dispatch: GenerativeUIDispatch | undefined;
565
- const ui = new ClientGenUI({
566
- library: {
567
- Action: {
568
- description: "An action target.",
569
- properties: z.object({}),
570
- render: ({ $dispatch }: any) => {
571
- dispatch = $dispatch;
572
- return null;
573
- },
574
- },
575
- },
576
- actions: createActionRegistry({ answer: vi.fn() }),
577
- });
578
-
579
- renderTool(
580
- ui.promptUser(),
581
- { $type: "Action", $action: { type: "answer" } },
582
- { result: { choice: "existing" } },
583
- );
584
-
585
- expect(dispatch).toBeUndefined();
586
- });
587
-
588
- it("never completes present from an action result", async () => {
589
- const addResult = vi.fn();
590
- const dispatch = captureActionDispatch(
591
- "present",
592
- createActionRegistry({ answer: () => ({ choice: "yes" }) }),
593
- { addResult },
594
- );
595
-
596
- dispatch({ type: "answer" });
597
- await Promise.resolve();
598
- expect(addResult).not.toHaveBeenCalled();
599
- });
600
-
601
- it.each(["complete", "requires-action"])(
602
- "completes %s prompt_user form submissions with named field values",
603
- async (type) => {
604
- const addResult = vi.fn();
605
- const handler = vi.fn(() => ({ submitted: true }));
606
- const ui = new ClientGenUI({
607
- library: defaultGenerativeUILibrary,
608
- actions: createActionRegistry({ submit: handler }),
609
- });
610
- const container = document.createElement("div");
611
- const root = createRoot(container);
612
-
613
- try {
614
- await act(async () => {
615
- root.render(
616
- (ui.promptUser() as any).render({
617
- args: {
618
- $type: "Form",
619
- $action: { type: "submit" },
620
- children: [
621
- { $type: "Input", name: "email" },
622
- { $type: "Checkbox", name: "updates", label: "Updates" },
623
- ],
624
- },
625
- status: {
626
- type,
627
- ...(type === "requires-action" ? { reason: "tool-calls" } : {}),
628
- },
629
- toolCallId: "prompt-form",
630
- toolName: "prompt_user",
631
- argsText: "",
632
- addResult,
633
- }),
634
- );
635
- });
636
-
637
- const form = container.querySelector("form");
638
- const email = container.querySelector<HTMLInputElement>(
639
- 'input[name="email"]',
640
- );
641
- const updates = container.querySelector<HTMLInputElement>(
642
- 'input[name="updates"]',
643
- );
644
- if (!form || !email || !updates) {
645
- throw new Error("Expected the prompt form fields to render.");
646
- }
647
-
648
- email.value = "ada@example.com";
649
- updates.checked = true;
650
- let submitted = true;
651
- await act(async () => {
652
- submitted = form.dispatchEvent(
653
- new Event("submit", { bubbles: true, cancelable: true }),
654
- );
655
- await Promise.resolve();
656
- });
657
-
658
- expect(submitted).toBe(false);
659
- expect(handler).toHaveBeenCalledWith({
660
- payload: {
661
- type: "submit",
662
- $input: { email: "ada@example.com", updates: true },
663
- },
664
- });
665
- expect(addResult).toHaveBeenCalledWith({ submitted: true });
666
- } finally {
667
- await act(async () => root.unmount());
668
- }
669
- },
670
- );
671
- });
672
-
673
- describe("JSONGenerativeUI — server build", () => {
674
- const ui = new ServerGenUI({ library });
675
-
676
- it("present carries only schema (no render/execute) and matches the client schema", () => {
677
- const tool = ui.present() as any;
678
- expect(tool.type).toBe("frontend");
679
- expect(tool.render).toBeUndefined();
680
- expect(tool.execute).toBeUndefined();
681
- expect(tool.unstable_backendDefault).toBeUndefined();
682
- expect(tool.parameters.properties.$type.enum).toEqual(["Card", "Button"]);
683
- expect(tool.parameters).toEqual(
684
- new ClientGenUI({ library }).present().parameters,
685
- );
686
- });
687
-
688
- it("prompt_user carries only schema (no render)", () => {
689
- const tool = ui.promptUser() as any;
690
- expect(tool.type).toBe("human");
691
- expect(tool.render).toBeUndefined();
692
- expect(tool.unstable_backendDefault).toBeUndefined();
693
- expect(tool.parameters.properties.$type.enum).toEqual(["Card", "Button"]);
694
- });
695
- });
696
-
697
- describe("defineGenerativeComponents", () => {
698
- it("throws at runtime — it must be stripped by the compiler, never called", () => {
699
- expect(() => defineGenerativeComponents({})).toThrow(
700
- /no runtime implementation/,
701
- );
702
- });
703
- });
704
-
705
- describe("live A2UI surfaces", () => {
706
- const bindingFixture = ({
707
- inputCount = 2,
708
- customInput = false,
709
- path = "/profile/name",
710
- dataModel = { profile: { name: "Initial" }, other: "Other" },
711
- template = false,
712
- }: {
713
- inputCount?: number;
714
- customInput?: boolean;
715
- path?: string;
716
- dataModel?: unknown;
717
- template?: boolean;
718
- } = {}) => {
719
- const handler = vi.fn();
720
- const ui = new ClientGenUI({
721
- library: {
722
- ...defaultGenerativeUILibrary,
723
- ...(customInput
724
- ? {
725
- Input: {
726
- ...defaultGenerativeUILibrary["Input"]!,
727
- render: ({ name, value, defaultValue }: any) => (
728
- <input name={name} defaultValue={value ?? defaultValue} />
729
- ),
730
- },
731
- }
732
- : {}),
733
- },
734
- actions: createActionRegistry({ "a2ui:action": handler }),
735
- });
736
- const fields = Array.from({ length: inputCount }, (_, index) => ({
737
- id: `field${index}`,
738
- component: "TextField",
739
- text: { path },
740
- }));
741
- const surface = applyA2uiOperations(new Map(), [
742
- {
743
- version: "v1.0",
744
- createSurface: {
745
- surfaceId: "s",
746
- components: [
747
- {
748
- id: "root",
749
- component: "Column",
750
- children: template
751
- ? { componentId: "row", path: "/items" }
752
- : [...fields.map(({ id }) => id), "preview", "submit"],
753
- },
754
- {
755
- id: "row",
756
- component: "Column",
757
- children: [...fields.map(({ id }) => id), "preview", "submit"],
758
- },
759
- ...fields,
760
- {
761
- id: "preview",
762
- component: "Text",
763
- text: { call: "formatString", args: { value: "${/other}" } },
764
- },
765
- {
766
- id: "submit",
767
- component: "Button",
768
- label: "Submit",
769
- action: {
770
- name: "submit",
771
- context: { name: { path } },
772
- },
773
- },
774
- ],
775
- dataModel,
776
- },
777
- },
778
- ]).state.get("s")!;
779
- const container = document.createElement("div");
780
- document.body.append(container);
781
- const root = createRoot(container);
782
- const render = async (dataModel = surface.dataModel) => {
783
- const next = { ...surface, dataModel };
784
- await act(async () => {
785
- root.render(
786
- (ui.present() as any).render({
787
- args: convertSurfaceToUISpec(next).spec,
788
- status: { type: "complete" },
789
- toolCallId: "a2ui:s",
790
- artifact: {
791
- a2ui: structuredClone(surfaceToOperations(next, "s")),
792
- },
793
- }),
794
- );
795
- });
796
- };
797
- const inputs = () => [
798
- ...container.querySelectorAll<HTMLInputElement>("input"),
799
- ];
800
- const edit = async (value: string, notify = true, index = 0) => {
801
- await act(async () => {
802
- const input = inputs()[index]!;
803
- Object.getOwnPropertyDescriptor(
804
- HTMLInputElement.prototype,
805
- "value",
806
- )!.set!.call(input, value);
807
- if (notify) input.dispatchEvent(new Event("input", { bubbles: true }));
808
- });
809
- };
810
- const close = async () => {
811
- await act(async () => root.unmount());
812
- container.remove();
813
- };
814
- return { handler, container, render, inputs, edit, close };
815
- };
816
-
817
- it("preserves edits across unchanged snapshots with new operation identities", async () => {
818
- const fixture = bindingFixture();
819
- try {
820
- await fixture.render();
821
- await fixture.edit("Edited");
822
- await fixture.render();
823
- await fixture.render();
824
- expect(fixture.inputs().map((input) => input.value)).toEqual([
825
- "Edited",
826
- "Edited",
827
- ]);
828
- } finally {
829
- await fixture.close();
830
- }
831
- });
832
-
833
- it("preserves edits when the agent updates a different path", async () => {
834
- const fixture = bindingFixture();
835
- try {
836
- await fixture.render();
837
- await fixture.edit("Edited");
838
- await fixture.render({ profile: { name: "Initial" }, other: "Remote" });
839
- expect(fixture.inputs().map((input) => input.value)).toEqual([
840
- "Edited",
841
- "Edited",
842
- ]);
843
- expect(
844
- fixture.container.querySelector('[data-aui="markdown"]')?.textContent,
845
- ).toBe("Remote");
846
- } finally {
847
- await fixture.close();
848
- }
849
- });
850
-
851
- it.each([
852
- {
853
- label: "array index",
854
- path: "/names/0",
855
- dataModel: { names: ["Initial", "Other"] },
856
- incoming: { names: ["Initial", "Remote"] },
857
- },
858
- {
859
- label: "escaped pointer",
860
- path: "/profile/na~1me~0",
861
- dataModel: { profile: { "na/me~": "Initial", other: "Other" } },
862
- incoming: { profile: { "na/me~": "Initial", other: "Remote" } },
863
- },
864
- {
865
- label: "missing data",
866
- path: "/profile/name",
867
- dataModel: null,
868
- incoming: { other: "Remote" },
869
- },
870
- {
871
- label: "template scope",
872
- path: "name",
873
- template: true,
874
- dataModel: { items: [{ name: "Initial", other: "Other" }] },
875
- incoming: { items: [{ name: "Initial", other: "Remote" }] },
876
- },
877
- ])(
878
- "preserves edits at a $label during unrelated agent updates",
879
- async ({ incoming, ...options }) => {
880
- const fixture = bindingFixture(options);
881
- try {
882
- await fixture.render();
883
- await fixture.edit("Edited");
884
- await fixture.render(incoming);
885
- expect(fixture.inputs().map((input) => input.value)).toEqual([
886
- "Edited",
887
- "Edited",
888
- ]);
889
- await act(async () =>
890
- fixture.container.querySelector("button")!.click(),
891
- );
892
- expect(fixture.handler).toHaveBeenCalledWith({
893
- payload: {
894
- type: "a2ui:action",
895
- name: "submit",
896
- surfaceId: "s",
897
- sourceComponentId: "submit",
898
- context: { name: "Edited" },
899
- },
900
- });
901
- } finally {
902
- await fixture.close();
903
- }
904
- },
905
- );
906
-
907
- it("lets an agent change to the edited path win after unrelated updates", async () => {
908
- const fixture = bindingFixture();
909
- try {
910
- await fixture.render();
911
- await fixture.edit("Edited");
912
- await fixture.render({ profile: { name: "Initial" }, other: "Remote" });
913
- expect(fixture.inputs().map((input) => input.value)).toEqual([
914
- "Edited",
915
- "Edited",
916
- ]);
917
- await fixture.render({ profile: { name: "Agent" }, other: "Remote" });
918
- expect(fixture.inputs().map((input) => input.value)).toEqual([
919
- "Agent",
920
- "Agent",
921
- ]);
922
- await fixture.edit("Again");
923
- await fixture.render({ profile: { name: "Agent" }, other: "Remote" });
924
- expect(fixture.inputs().map((input) => input.value)).toEqual([
925
- "Again",
926
- "Again",
927
- ]);
928
- } finally {
929
- await fixture.close();
930
- }
931
- });
932
-
933
- it.each([
934
- { count: 1, customInput: false },
935
- { count: 2, customInput: false },
936
- { count: 1, customInput: true },
937
- { count: 2, customInput: true },
938
- ])(
939
- "resolves a DOM-only edit through $field with $count bound inputs (custom: $customInput)",
940
- async ({ count, customInput }) => {
941
- const fixture = bindingFixture({ inputCount: count, customInput });
942
- try {
943
- await fixture.render();
944
- await fixture.edit("Autofilled", false, count - 1);
945
- await act(async () =>
946
- fixture.container.querySelector("button")!.click(),
947
- );
948
- expect(fixture.handler).toHaveBeenCalledWith({
949
- payload: {
950
- type: "a2ui:action",
951
- name: "submit",
952
- surfaceId: "s",
953
- sourceComponentId: "submit",
954
- context: { name: "Autofilled" },
955
- },
956
- });
957
- } finally {
958
- await fixture.close();
959
- }
960
- },
961
- );
962
-
963
- it("renders bound input names without actions and retains button field references", async () => {
964
- const fixture = bindingFixture();
965
- try {
966
- await fixture.render();
967
- expect(fixture.inputs().map((input) => input.name)).toEqual([
968
- "/profile/name",
969
- "/profile/name",
970
- ]);
971
- expect(
972
- fixture
973
- .inputs()
974
- .every((input) => !input.hasAttribute("data-aui-action")),
975
- ).toBe(true);
976
- expect(
977
- JSON.parse(
978
- fixture.container
979
- .querySelector("button")!
980
- .getAttribute("data-aui-action")!,
981
- ),
982
- ).toMatchObject({
983
- context: { name: { $field: "/profile/name", fallback: "Initial" } },
984
- });
985
- } finally {
986
- await fixture.close();
987
- }
988
- });
989
-
990
- it("does not reevaluate unrelated function values while typing", async () => {
991
- const fixture = bindingFixture();
992
- try {
993
- const before = evaluateValue.mock.calls.length;
994
- await fixture.render();
995
- expect(evaluateValue.mock.calls.length - before).toBe(2);
996
- await fixture.edit("One");
997
- await fixture.edit("Two");
998
- expect(evaluateValue.mock.calls.length - before).toBe(2);
999
- expect(fixture.inputs().map((input) => input.value)).toEqual([
1000
- "Two",
1001
- "Two",
1002
- ]);
1003
- expect(
1004
- fixture.container.querySelector('[data-aui="markdown"]')?.textContent,
1005
- ).toBe("Other");
1006
- } finally {
1007
- await fixture.close();
1008
- }
1009
- });
1010
-
1011
- it("keeps choice arrays and slider focus while updating live bindings", async () => {
1012
- const handler = vi.fn();
1013
- const ui = new ClientGenUI({
1014
- library: defaultGenerativeUILibrary,
1015
- actions: createActionRegistry({ "a2ui:action": handler }),
1016
- });
1017
- const { state } = applyA2uiOperations(new Map(), [
1018
- {
1019
- version: "v1.0",
1020
- createSurface: {
1021
- surfaceId: "controls",
1022
- components: [
1023
- {
1024
- id: "root",
1025
- component: "Column",
1026
- children: ["plan", "volume", "submit"],
1027
- },
1028
- {
1029
- id: "plan",
1030
- component: "ChoicePicker",
1031
- value: { path: "/plan" },
1032
- options: [
1033
- { label: "Free", value: "free" },
1034
- { label: "Pro", value: "pro" },
1035
- ],
1036
- },
1037
- {
1038
- id: "volume",
1039
- component: "Slider",
1040
- min: 0,
1041
- max: 10,
1042
- steps: 10,
1043
- value: { path: "/volume" },
1044
- },
1045
- {
1046
- id: "submit",
1047
- component: "Button",
1048
- label: "Submit",
1049
- action: {
1050
- name: "submit",
1051
- context: {
1052
- plan: { path: "/plan" },
1053
- volume: { path: "/volume" },
1054
- },
1055
- },
1056
- },
1057
- ],
1058
- dataModel: { plan: ["free"], volume: 2 },
1059
- },
1060
- },
1061
- ]);
1062
- const surface = state.get("controls")!;
1063
- const container = document.createElement("div");
1064
- document.body.append(container);
1065
- const root = createRoot(container);
1066
- try {
1067
- await act(async () => {
1068
- root.render(
1069
- (ui.present() as any).render({
1070
- args: convertSurfaceToUISpec(surface).spec,
1071
- status: { type: "complete" },
1072
- toolCallId: "a2ui:controls",
1073
- toolName: "present",
1074
- argsText: "",
1075
- artifact: { a2ui: surfaceToOperations(surface) },
1076
- }),
1077
- );
1078
- });
1079
- const pro =
1080
- container.querySelector<HTMLInputElement>('input[value="pro"]')!;
1081
- await act(async () => pro.click());
1082
- expect(pro.checked).toBe(true);
1083
- const slider = container.querySelector<HTMLInputElement>(
1084
- 'input[type="range"]',
1085
- )!;
1086
- slider.focus();
1087
- await act(async () => {
1088
- Object.getOwnPropertyDescriptor(
1089
- HTMLInputElement.prototype,
1090
- "value",
1091
- )!.set!.call(slider, "6");
1092
- slider.dispatchEvent(new Event("input", { bubbles: true }));
1093
- });
1094
- expect(container.querySelector('input[type="range"]')).toBe(slider);
1095
- expect(document.activeElement).toBe(slider);
1096
- expect(slider.value).toBe("6");
1097
- expect(container.querySelector("output")?.textContent).toBe("6");
1098
- await act(async () => container.querySelector("button")!.click());
1099
- expect(handler).toHaveBeenCalledWith({
1100
- payload: {
1101
- type: "a2ui:action",
1102
- name: "submit",
1103
- surfaceId: "controls",
1104
- sourceComponentId: "submit",
1105
- context: { plan: ["pro"], volume: 6 },
1106
- },
1107
- });
1108
- } finally {
1109
- await act(async () => root.unmount());
1110
- container.remove();
1111
- }
1112
- });
1113
-
1114
- it("keeps a continuous bound slider mounted as its value becomes an integer", async () => {
1115
- const ui = new ClientGenUI({ library: defaultGenerativeUILibrary });
1116
- const surface = applyA2uiOperations(new Map(), [
1117
- {
1118
- version: "v1.0",
1119
- createSurface: {
1120
- surfaceId: "slider",
1121
- components: [
1122
- {
1123
- id: "root",
1124
- component: "Slider",
1125
- min: 0,
1126
- max: 100,
1127
- value: { path: "/volume" },
1128
- },
1129
- ],
1130
- dataModel: { volume: 37.5 },
1131
- },
1132
- },
1133
- ]).state.get("slider")!;
1134
- const container = document.createElement("div");
1135
- document.body.append(container);
1136
- const root = createRoot(container);
1137
- try {
1138
- await act(async () => {
1139
- root.render(
1140
- (ui.present() as any).render({
1141
- args: convertSurfaceToUISpec(surface).spec,
1142
- status: { type: "complete" },
1143
- toolCallId: "a2ui:slider",
1144
- artifact: { a2ui: surfaceToOperations(surface) },
1145
- }),
1146
- );
1147
- });
1148
- const slider = container.querySelector<HTMLInputElement>(
1149
- 'input[type="range"]',
1150
- )!;
1151
- expect(slider.step).toBe("0.1");
1152
- slider.focus();
1153
- await act(async () => {
1154
- Object.getOwnPropertyDescriptor(
1155
- HTMLInputElement.prototype,
1156
- "value",
1157
- )!.set!.call(slider, "40");
1158
- slider.dispatchEvent(new Event("input", { bubbles: true }));
1159
- });
1160
- expect(container.querySelector('input[type="range"]')).toBe(slider);
1161
- expect(document.activeElement).toBe(slider);
1162
- expect(slider.value).toBe("40");
1163
- expect(slider.step).toBe("0.1");
1164
- expect(container.querySelector("output")?.textContent).toBe("40");
1165
- } finally {
1166
- await act(async () => root.unmount());
1167
- container.remove();
1168
- }
1169
- });
1170
-
1171
- it("keeps A2UI bindings live while rendering a present surface", async () => {
1172
- const handler = vi.fn();
1173
- const ui = new ClientGenUI({
1174
- library: defaultGenerativeUILibrary,
1175
- actions: createActionRegistry({ "a2ui:action": handler }),
1176
- });
1177
- const { state } = applyA2uiOperations(new Map(), [
1178
- {
1179
- version: "v1.0",
1180
- createSurface: {
1181
- surfaceId: "s",
1182
- components: [
1183
- {
1184
- id: "root",
1185
- component: "Column",
1186
- children: ["first", "second", "preview", "derived", "submit"],
1187
- },
1188
- { id: "first", component: "TextField", text: { path: "/name" } },
1189
- { id: "second", component: "TextField", text: { path: "/name" } },
1190
- { id: "preview", component: "Text", text: { path: "/name" } },
1191
- {
1192
- id: "derived",
1193
- component: "Text",
1194
- text: {
1195
- call: "formatString",
1196
- args: { value: "Hello ${/name}" },
1197
- },
1198
- },
1199
- {
1200
- id: "submit",
1201
- component: "Button",
1202
- label: "Submit",
1203
- action: { name: "submit", context: { name: { path: "/name" } } },
1204
- },
1205
- ],
1206
- dataModel: { name: "Initial" },
1207
- },
1208
- },
1209
- ]);
1210
- const surface = state.get("s")!;
1211
- const container = document.createElement("div");
1212
- document.body.append(container);
1213
- const root = createRoot(container);
1214
-
1215
- try {
1216
- await act(async () => {
1217
- root.render(
1218
- (ui.present() as any).render({
1219
- args: convertSurfaceToUISpec(surface).spec,
1220
- status: { type: "complete" },
1221
- toolCallId: "a2ui:s",
1222
- toolName: "present",
1223
- argsText: "",
1224
- artifact: { a2ui: surfaceToOperations(surface) },
1225
- }),
1226
- );
1227
- });
1228
-
1229
- const fields =
1230
- container.querySelectorAll<HTMLInputElement>('[data-aui="input"]');
1231
- expect([...fields].map((field) => field.value)).toEqual([
1232
- "Initial",
1233
- "Initial",
1234
- ]);
1235
-
1236
- await act(async () => {
1237
- Object.getOwnPropertyDescriptor(
1238
- HTMLInputElement.prototype,
1239
- "value",
1240
- )!.set!.call(fields[0]!, "Edited");
1241
- fields[0]!.dispatchEvent(new Event("input", { bubbles: true }));
1242
- });
1243
-
1244
- expect([...fields].map((field) => field.value)).toEqual([
1245
- "Edited",
1246
- "Edited",
1247
- ]);
1248
- expect(
1249
- container.querySelector('[data-aui="markdown"]')?.textContent,
1250
- ).toBe("Edited");
1251
- expect(
1252
- container.querySelectorAll('[data-aui="markdown"]')[1]?.textContent,
1253
- ).toBe("Hello Edited");
1254
-
1255
- await act(async () => container.querySelector("button")!.click());
1256
-
1257
- expect(handler).toHaveBeenCalledWith({
1258
- payload: {
1259
- type: "a2ui:action",
1260
- name: "submit",
1261
- surfaceId: "s",
1262
- sourceComponentId: "submit",
1263
- context: { name: "Edited" },
1264
- },
1265
- });
1266
-
1267
- const remoteSurface = applyA2uiOperations(state, [
1268
- {
1269
- version: "v0.9",
1270
- updateDataModel: {
1271
- surfaceId: "s",
1272
- path: "/name",
1273
- contents: "Remote",
1274
- },
1275
- },
1276
- ]).state.get("s")!;
1277
- await act(async () => {
1278
- root.render(
1279
- (ui.present() as any).render({
1280
- args: convertSurfaceToUISpec(remoteSurface).spec,
1281
- status: { type: "complete" },
1282
- toolCallId: "a2ui:s",
1283
- toolName: "present",
1284
- argsText: "",
1285
- artifact: { a2ui: surfaceToOperations(remoteSurface) },
1286
- }),
1287
- );
1288
- });
1289
- expect([...fields].map((field) => field.value)).toEqual([
1290
- "Remote",
1291
- "Remote",
1292
- ]);
1293
- } finally {
1294
- await act(async () => root.unmount());
1295
- container.remove();
1296
- }
1297
- });
1298
- });