@assistant-ui/react 0.15.24 → 0.15.26

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 (204) hide show
  1. package/dist/hooks/useToolCallElapsed.d.ts +3 -1
  2. package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
  3. package/dist/hooks/useToolCallElapsed.js +8 -8
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +39 -7
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -3
  8. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  10. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +56 -46
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/csp.d.ts +4 -0
  15. package/dist/mcp-apps/csp.d.ts.map +1 -0
  16. package/dist/mcp-apps/csp.js +93 -0
  17. package/dist/mcp-apps/csp.js.map +1 -0
  18. package/dist/mcp-apps/types.d.ts +1 -0
  19. package/dist/mcp-apps/types.d.ts.map +1 -1
  20. package/dist/mcp-apps/types.js.map +1 -1
  21. package/dist/model-context/makeAssistantVisible.d.ts +1 -0
  22. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  23. package/dist/model-context/makeAssistantVisible.js +31 -12
  24. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  25. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
  26. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  27. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  28. package/dist/primitives/attachment.d.ts +3 -1
  29. package/dist/primitives/attachment.d.ts.map +1 -1
  30. package/dist/primitives/attachment.js.map +1 -1
  31. package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
  32. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  33. package/dist/primitives/composer/ComposerCancel.js +14 -1
  34. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  35. package/dist/primitives/composer/ComposerInput.d.ts +15 -1
  36. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  37. package/dist/primitives/composer/ComposerInput.js +5 -4
  38. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  39. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  40. package/dist/primitives/composer/ComposerRoot.js +24 -19
  41. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  42. package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
  43. package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
  44. package/dist/primitives/composer/composerInputSelector.js +7 -0
  45. package/dist/primitives/composer/composerInputSelector.js.map +1 -0
  46. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
  47. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  48. package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
  49. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  50. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
  51. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
  52. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
  53. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  54. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
  55. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  56. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  57. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
  58. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
  59. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
  60. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  61. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
  62. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  64. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
  65. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
  66. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  67. package/dist/primitives/composer/trigger/index.d.ts +1 -1
  68. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
  69. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
  70. package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
  71. package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
  72. package/dist/primitives/composer.d.ts +55 -15
  73. package/dist/primitives/composer.d.ts.map +1 -1
  74. package/dist/primitives/composer.js +8 -1
  75. package/dist/primitives/composer.js.map +1 -1
  76. package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
  77. package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
  78. package/dist/primitives/message/MessagePartsGrouped.js +58 -278
  79. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  80. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  81. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
  82. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  83. package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
  84. package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
  85. package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
  86. package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
  87. package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
  88. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  89. package/dist/primitives/thread/ThreadMessages.js +2 -2
  90. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  91. package/dist/primitives/thread/ThreadViewport.js +4 -3
  92. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  93. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
  94. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
  95. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  96. package/dist/primitives/thread/useThreadViewportAutoScroll.js +195 -17
  97. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  98. package/dist/primitives/thread.d.ts +2 -0
  99. package/dist/primitives/thread.d.ts.map +1 -1
  100. package/dist/primitives/thread.js +5 -2
  101. package/dist/primitives/thread.js.map +1 -1
  102. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  103. package/dist/sandbox-host/SandboxHost.js +9 -3
  104. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  105. package/dist/unstable/useComposerInput.d.ts +7 -4
  106. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  107. package/dist/unstable/useComposerInput.js +4 -4
  108. package/dist/unstable/useComposerInput.js.map +1 -1
  109. package/dist/unstable/useComposerInputHistory.d.ts +3 -2
  110. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
  111. package/dist/unstable/useComposerInputHistory.js +2 -2
  112. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  113. package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
  114. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  115. package/dist/unstable/useLiveCompletionAdapter.js +8 -7
  116. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  117. package/dist/unstable/useMentionAdapter.d.ts +12 -3
  118. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  119. package/dist/unstable/useMentionAdapter.js +2 -2
  120. package/dist/unstable/useMentionAdapter.js.map +1 -1
  121. package/dist/unstable/useMessageStallDetection.d.ts +4 -2
  122. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  123. package/dist/unstable/useMessageStallDetection.js +12 -8
  124. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  125. package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
  126. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  127. package/dist/unstable/useSlashCommandAdapter.js +5 -4
  128. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  129. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
  130. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
  131. package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
  132. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
  133. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  134. package/dist/utils/getSelectionMessageId.js +31 -8
  135. package/dist/utils/getSelectionMessageId.js.map +1 -1
  136. package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
  137. package/dist/utils/hooks/useOnResizeContent.js +2 -22
  138. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  139. package/dist/utils/smooth/useSmooth.js +6 -6
  140. package/dist/utils/smooth/useSmooth.js.map +1 -1
  141. package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  142. package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
  143. package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
  144. package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
  145. package/package.json +12 -11
  146. package/src/hooks/useToolCallElapsed.test.tsx +37 -0
  147. package/src/hooks/useToolCallElapsed.ts +10 -7
  148. package/src/index.ts +47 -5
  149. package/src/mcp-apps/McpAppRenderer.tsx +3 -9
  150. package/src/mcp-apps/app-frame.test.tsx +38 -0
  151. package/src/mcp-apps/app-frame.tsx +5 -8
  152. package/src/mcp-apps/csp.test.ts +112 -0
  153. package/src/mcp-apps/csp.ts +124 -0
  154. package/src/mcp-apps/types.ts +1 -0
  155. package/src/model-context/frame/AssistantFrame.test.ts +45 -0
  156. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
  157. package/src/model-context/makeAssistantVisible.test.tsx +315 -5
  158. package/src/model-context/makeAssistantVisible.tsx +43 -7
  159. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
  160. package/src/primitives/attachment.ts +4 -1
  161. package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
  162. package/src/primitives/composer/ComposerCancel.ts +8 -1
  163. package/src/primitives/composer/ComposerInput.tsx +20 -2
  164. package/src/primitives/composer/ComposerRoot.tsx +6 -5
  165. package/src/primitives/composer/composerInputSelector.ts +4 -0
  166. package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
  167. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
  168. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
  169. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
  170. package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
  171. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
  172. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
  173. package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
  174. package/src/primitives/composer.ts +70 -15
  175. package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
  176. package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
  177. package/src/primitives/message/MessageRoot.test.tsx +125 -2
  178. package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
  179. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
  180. package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
  181. package/src/primitives/thread/ThreadMessages.tsx +1 -0
  182. package/src/primitives/thread/ThreadViewport.tsx +3 -3
  183. package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
  184. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +922 -68
  185. package/src/primitives/thread/useThreadViewportAutoScroll.ts +330 -56
  186. package/src/primitives/thread.ts +2 -0
  187. package/src/sandbox-host/SandboxHost.test.tsx +34 -0
  188. package/src/sandbox-host/SandboxHost.tsx +11 -12
  189. package/src/tests/toolCallTiming.test.tsx +19 -2
  190. package/src/unstable/useComposerInput.ts +7 -4
  191. package/src/unstable/useComposerInputHistory.ts +3 -2
  192. package/src/unstable/useLiveCompletionAdapter.ts +9 -14
  193. package/src/unstable/useMentionAdapter.ts +12 -3
  194. package/src/unstable/useMessageStallDetection.test.tsx +40 -0
  195. package/src/unstable/useMessageStallDetection.ts +14 -8
  196. package/src/unstable/useSlashCommandAdapter.ts +8 -4
  197. package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
  198. package/src/utils/getSelectionMessageId.test.ts +231 -0
  199. package/src/utils/getSelectionMessageId.ts +45 -7
  200. package/src/utils/hooks/useOnResizeContent.ts +2 -29
  201. package/src/utils/smooth/useSmooth.test.tsx +16 -5
  202. package/src/utils/smooth/useSmooth.ts +7 -7
  203. package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
  204. package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
@@ -8,6 +8,11 @@ import {
8
8
  type ChangeEvent,
9
9
  } from "react";
10
10
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
11
+ import {
12
+ mergeModelContexts,
13
+ type ModelContextProvider,
14
+ } from "@assistant-ui/core";
15
+ import type { Tool } from "assistant-stream";
11
16
 
12
17
  const { registerMock, auiMock } = vi.hoisted(() => {
13
18
  const register = vi.fn((_provider: unknown) => () => {});
@@ -30,10 +35,15 @@ type EditTool = {
30
35
  execute: (args: { editId: string; value: string }) => Promise<unknown>;
31
36
  };
32
37
 
33
- type ModelContextProvider = {
38
+ type ClickTool = {
39
+ execute: (args: { clickId: string }) => Promise<unknown>;
40
+ };
41
+
42
+ type VisibleToolsProvider = {
34
43
  getModelContext: () => {
35
44
  tools: {
36
- edit: EditTool;
45
+ edit?: EditTool;
46
+ click?: ClickTool;
37
47
  };
38
48
  };
39
49
  };
@@ -79,6 +89,36 @@ const ControlledTextarea = forwardRef<
79
89
  );
80
90
  });
81
91
 
92
+ const Button = forwardRef<HTMLButtonElement, ComponentProps<"button">>(
93
+ (props, ref) => <button {...props} ref={ref} />,
94
+ );
95
+
96
+ const SplitRefButton = forwardRef<HTMLButtonElement, ComponentProps<"div">>(
97
+ ({ children, ...props }, ref) => (
98
+ <div {...props}>
99
+ <button ref={ref}>{children}</button>
100
+ </div>
101
+ ),
102
+ );
103
+
104
+ const getRegisteredTools = () => {
105
+ const provider = registerMock.mock.calls[0]![0] as VisibleToolsProvider;
106
+ return provider.getModelContext().tools;
107
+ };
108
+
109
+ const executeTool = (tool: Tool | undefined, args: Record<string, unknown>) => {
110
+ if (!tool?.execute) throw new Error("Missing executable tool");
111
+ return Promise.resolve(
112
+ tool.execute(args, {
113
+ toolCallId: "test-call",
114
+ abortSignal: new AbortController().signal,
115
+ human: async () => {
116
+ throw new Error("Unexpected human input request");
117
+ },
118
+ }),
119
+ );
120
+ };
121
+
82
122
  const runEditTool = async (
83
123
  element: HTMLInputElement | HTMLTextAreaElement,
84
124
  value: string,
@@ -86,8 +126,9 @@ const runEditTool = async (
86
126
  const editId = element.dataset.editId;
87
127
  if (editId === undefined) throw new Error("Missing edit id");
88
128
 
89
- const provider = registerMock.mock.calls[0]![0] as ModelContextProvider;
90
- const task = provider.getModelContext().tools.edit.execute({
129
+ const edit = getRegisteredTools().edit;
130
+ if (!edit) throw new Error("Missing edit tool");
131
+ const task = edit.execute({
91
132
  editId,
92
133
  value,
93
134
  });
@@ -95,7 +136,7 @@ const runEditTool = async (
95
136
  await task;
96
137
  };
97
138
 
98
- describe("makeAssistantVisible editable components", () => {
139
+ describe("makeAssistantVisible", () => {
99
140
  beforeEach(() => {
100
141
  registerMock.mockClear();
101
142
  vi.stubGlobal("CSS", { escape: (value: string) => value });
@@ -143,4 +184,273 @@ describe("makeAssistantVisible editable components", () => {
143
184
  expect(textarea.getAttribute("data-react-value")).toBe("updated");
144
185
  expect(onChange).toHaveBeenCalledOnce();
145
186
  });
187
+
188
+ it("preserves the default action settle delay", async () => {
189
+ const EditableInput = makeAssistantVisible(ControlledInput, {
190
+ editable: true,
191
+ });
192
+ const input = render(<EditableInput />).getByRole("textbox");
193
+ const editId = (input as HTMLInputElement).dataset.editId!;
194
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
195
+
196
+ expect(
197
+ (input as HTMLInputElement).dataset.actionSettleDelay,
198
+ ).toBeUndefined();
199
+
200
+ const task = getRegisteredTools().edit!.execute({
201
+ editId,
202
+ value: "updated",
203
+ });
204
+
205
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2000);
206
+ await vi.runAllTimersAsync();
207
+ await task;
208
+ setTimeoutSpy.mockRestore();
209
+ });
210
+
211
+ it("uses the default when the rendered settle delay is invalid", async () => {
212
+ const ClickableButton = makeAssistantVisible(Button, {
213
+ clickable: true,
214
+ settleDelayMs: 25,
215
+ });
216
+ const button = render(<ClickableButton>Run</ClickableButton>).getByRole(
217
+ "button",
218
+ );
219
+ const clickId = (button as HTMLButtonElement).dataset.clickId!;
220
+ (button as HTMLButtonElement).dataset.actionSettleDelay = "invalid";
221
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
222
+
223
+ const task = getRegisteredTools().click!.execute({ clickId });
224
+
225
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2000);
226
+ await vi.runAllTimersAsync();
227
+ await task;
228
+ setTimeoutSpy.mockRestore();
229
+ });
230
+
231
+ it.each(["", " "])(
232
+ "uses the default for a blank rendered settle delay of %j",
233
+ async (renderedDelay) => {
234
+ const VisibleInput = makeAssistantVisible(ControlledInput, {
235
+ clickable: true,
236
+ editable: true,
237
+ settleDelayMs: 25,
238
+ });
239
+ const input = render(<VisibleInput />).getByRole("textbox");
240
+ const clickId = (input as HTMLInputElement).dataset.clickId!;
241
+ const editId = (input as HTMLInputElement).dataset.editId!;
242
+ const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
243
+ input.dataset.actionSettleDelay = renderedDelay;
244
+
245
+ const clickTask = getRegisteredTools().click!.execute({ clickId });
246
+ expect(setTimeoutSpy).toHaveBeenLastCalledWith(
247
+ expect.any(Function),
248
+ 2000,
249
+ );
250
+ await vi.runAllTimersAsync();
251
+ await clickTask;
252
+
253
+ setTimeoutSpy.mockClear();
254
+ const editTask = getRegisteredTools().edit!.execute({
255
+ editId,
256
+ value: "updated",
257
+ });
258
+ expect(setTimeoutSpy).toHaveBeenLastCalledWith(
259
+ expect.any(Function),
260
+ 2000,
261
+ );
262
+ await vi.runAllTimersAsync();
263
+ await editTask;
264
+ setTimeoutSpy.mockRestore();
265
+ },
266
+ );
267
+
268
+ it("uses the configured settle delay for click actions", async () => {
269
+ const ClickableButton = makeAssistantVisible(Button, {
270
+ clickable: true,
271
+ settleDelayMs: 25,
272
+ });
273
+ const onClick = vi.fn();
274
+ const button = render(
275
+ <ClickableButton onClick={onClick}>Run</ClickableButton>,
276
+ ).getByRole("button");
277
+ const clickId = (button as HTMLButtonElement).dataset.clickId!;
278
+ const settled = vi.fn();
279
+
280
+ const task = getRegisteredTools().click!.execute({ clickId });
281
+ void task.then(settled);
282
+
283
+ expect(onClick).toHaveBeenCalledOnce();
284
+ await vi.advanceTimersByTimeAsync(24);
285
+ expect(settled).not.toHaveBeenCalled();
286
+
287
+ await vi.advanceTimersByTimeAsync(1);
288
+ await task;
289
+ expect(settled).toHaveBeenCalledOnce();
290
+ });
291
+
292
+ it("uses the action id when the forwarded ref targets a child", async () => {
293
+ const ClickableButton = makeAssistantVisible(SplitRefButton, {
294
+ clickable: true,
295
+ settleDelayMs: 25,
296
+ });
297
+ const onClick = vi.fn();
298
+ const view = render(
299
+ <ClickableButton data-testid="action" onClick={onClick}>
300
+ Run
301
+ </ClickableButton>,
302
+ );
303
+ const action = view.getByTestId("action");
304
+ const clickId = action.dataset.clickId!;
305
+ const settled = vi.fn();
306
+
307
+ const task = getRegisteredTools().click!.execute({ clickId });
308
+ void task.then(settled);
309
+
310
+ expect(onClick).toHaveBeenCalledOnce();
311
+ await vi.advanceTimersByTimeAsync(24);
312
+ expect(settled).not.toHaveBeenCalled();
313
+
314
+ await vi.advanceTimersByTimeAsync(1);
315
+ await task;
316
+ expect(settled).toHaveBeenCalledOnce();
317
+ });
318
+
319
+ it("shares tools across wrappers while keeping per-element delays", async () => {
320
+ const SlowButton = makeAssistantVisible(Button, {
321
+ clickable: true,
322
+ editable: true,
323
+ settleDelayMs: 25,
324
+ });
325
+ const ImmediateButton = makeAssistantVisible(Button, {
326
+ clickable: true,
327
+ editable: true,
328
+ settleDelayMs: 0,
329
+ });
330
+ const view = render(
331
+ <>
332
+ <SlowButton>Slow</SlowButton>
333
+ <ImmediateButton>Immediate</ImmediateButton>
334
+ </>,
335
+ );
336
+ const providers = registerMock.mock.calls.map(
337
+ ([provider]) => provider as ModelContextProvider,
338
+ );
339
+ const mergedContext = mergeModelContexts(new Set(providers));
340
+
341
+ expect(mergedContext.tools?.click).toBe(
342
+ providers[0]!.getModelContext().tools?.click,
343
+ );
344
+ expect(mergedContext.tools?.click).toBe(
345
+ providers[1]!.getModelContext().tools?.click,
346
+ );
347
+ expect(mergedContext.tools?.edit).toBe(
348
+ providers[0]!.getModelContext().tools?.edit,
349
+ );
350
+ expect(mergedContext.tools?.edit).toBe(
351
+ providers[1]!.getModelContext().tools?.edit,
352
+ );
353
+
354
+ const [slowButton, immediateButton] = view.getAllByRole("button");
355
+ const click = mergedContext.tools!.click!;
356
+ const slowTask = executeTool(click, {
357
+ clickId: (slowButton as HTMLButtonElement).dataset.clickId!,
358
+ });
359
+ const immediateTask = executeTool(click, {
360
+ clickId: (immediateButton as HTMLButtonElement).dataset.clickId!,
361
+ });
362
+
363
+ await immediateTask;
364
+ let slowSettled = false;
365
+ void slowTask.then(() => {
366
+ slowSettled = true;
367
+ });
368
+ await vi.advanceTimersByTimeAsync(24);
369
+ expect(slowSettled).toBe(false);
370
+ await vi.advanceTimersByTimeAsync(1);
371
+ await slowTask;
372
+ });
373
+
374
+ it("shares the edit tool across wrappers while keeping per-element delays", async () => {
375
+ const SlowInput = makeAssistantVisible(ControlledInput, {
376
+ editable: true,
377
+ settleDelayMs: 25,
378
+ });
379
+ const ImmediateInput = makeAssistantVisible(ControlledInput, {
380
+ editable: true,
381
+ settleDelayMs: 0,
382
+ });
383
+ const view = render(
384
+ <>
385
+ <SlowInput aria-label="Slow" />
386
+ <ImmediateInput aria-label="Immediate" />
387
+ </>,
388
+ );
389
+ const providers = registerMock.mock.calls.map(
390
+ ([provider]) => provider as ModelContextProvider,
391
+ );
392
+ const mergedContext = mergeModelContexts(new Set(providers));
393
+
394
+ expect(mergedContext.tools?.edit).toBe(
395
+ providers[0]!.getModelContext().tools?.edit,
396
+ );
397
+ expect(mergedContext.tools?.edit).toBe(
398
+ providers[1]!.getModelContext().tools?.edit,
399
+ );
400
+
401
+ const slowInput = view.getByRole("textbox", { name: "Slow" });
402
+ const immediateInput = view.getByRole("textbox", { name: "Immediate" });
403
+ const edit = mergedContext.tools!.edit!;
404
+ const slowTask = executeTool(edit, {
405
+ editId: (slowInput as HTMLInputElement).dataset.editId!,
406
+ value: "slow update",
407
+ });
408
+ const immediateTask = executeTool(edit, {
409
+ editId: (immediateInput as HTMLInputElement).dataset.editId!,
410
+ value: "immediate update",
411
+ });
412
+
413
+ await immediateTask;
414
+ expect(immediateInput).toHaveProperty("value", "immediate update");
415
+ let slowSettled = false;
416
+ void slowTask.then(() => {
417
+ slowSettled = true;
418
+ });
419
+ await vi.advanceTimersByTimeAsync(24);
420
+ expect(slowSettled).toBe(false);
421
+
422
+ await vi.advanceTimersByTimeAsync(1);
423
+ await slowTask;
424
+ expect(slowInput).toHaveProperty("value", "slow update");
425
+ });
426
+
427
+ it("allows the action settle delay to be disabled", async () => {
428
+ const EditableInput = makeAssistantVisible(ControlledInput, {
429
+ editable: true,
430
+ settleDelayMs: 0,
431
+ });
432
+ const input = render(<EditableInput />).getByRole("textbox");
433
+ const editId = (input as HTMLInputElement).dataset.editId!;
434
+ const timersBefore = vi.getTimerCount();
435
+
436
+ const task = getRegisteredTools().edit!.execute({
437
+ editId,
438
+ value: "updated",
439
+ });
440
+
441
+ expect(vi.getTimerCount()).toBe(timersBefore);
442
+ await task;
443
+ expect(input).toHaveProperty("value", "updated");
444
+ });
445
+
446
+ it.each([-1, Number.NaN, Number.POSITIVE_INFINITY, 2_147_483_648])(
447
+ "rejects an invalid action settle delay of %s",
448
+ (settleDelayMs) => {
449
+ expect(() =>
450
+ makeAssistantVisible(Button, { clickable: true, settleDelayMs }),
451
+ ).toThrow(
452
+ new RangeError("settleDelayMs must be between 0 and 2147483647"),
453
+ );
454
+ },
455
+ );
146
456
  });
@@ -15,6 +15,27 @@ import { useAui } from "@assistant-ui/store";
15
15
  import { useComposedRefs } from "radix-ui/internal";
16
16
  import { tool } from "@assistant-ui/core";
17
17
 
18
+ const DEFAULT_ACTION_SETTLE_DELAY_MS = 2000;
19
+ const MAX_ACTION_SETTLE_DELAY_MS = 2_147_483_647;
20
+
21
+ const waitForSettle = async (settleDelayMs: number) => {
22
+ const delay =
23
+ Number.isFinite(settleDelayMs) &&
24
+ settleDelayMs >= 0 &&
25
+ settleDelayMs <= MAX_ACTION_SETTLE_DELAY_MS
26
+ ? settleDelayMs
27
+ : DEFAULT_ACTION_SETTLE_DELAY_MS;
28
+ if (delay === 0) return;
29
+ await new Promise<void>((resolve) => setTimeout(resolve, delay));
30
+ };
31
+
32
+ const readActionSettleDelay = (element: HTMLElement) => {
33
+ const value = element.dataset.actionSettleDelay;
34
+ return value === undefined || value.trim() === ""
35
+ ? DEFAULT_ACTION_SETTLE_DELAY_MS
36
+ : Number(value);
37
+ };
38
+
18
39
  const click = tool({
19
40
  parameters: {
20
41
  type: "object",
@@ -30,9 +51,7 @@ const click = tool({
30
51
  const el = document.querySelector(`[data-click-id='${escapedClickId}']`);
31
52
  if (el instanceof HTMLElement) {
32
53
  el.click();
33
-
34
- // todo make adjustable
35
- await new Promise((resolve) => setTimeout(resolve, 2000));
54
+ await waitForSettle(readActionSettleDelay(el));
36
55
  return {};
37
56
  } else {
38
57
  return "Element not found";
@@ -77,9 +96,7 @@ const edit = tool({
77
96
  setNativeValue(el, value);
78
97
  el.dispatchEvent(new Event("input", { bubbles: true }));
79
98
  el.dispatchEvent(new Event("change", { bubbles: true }));
80
-
81
- // todo make adjustable
82
- await new Promise((resolve) => setTimeout(resolve, 2000));
99
+ await waitForSettle(readActionSettleDelay(el));
83
100
  return {};
84
101
  } else {
85
102
  return "Element not found";
@@ -91,8 +108,23 @@ const ReadableContext = createContext<boolean>(false);
91
108
 
92
109
  export const makeAssistantVisible = <T extends ComponentType<any>>(
93
110
  Component: T,
94
- config?: { clickable?: boolean | undefined; editable?: boolean | undefined },
111
+ config?: {
112
+ clickable?: boolean | undefined;
113
+ editable?: boolean | undefined;
114
+ settleDelayMs?: number | undefined;
115
+ },
95
116
  ) => {
117
+ const settleDelayMs = config?.settleDelayMs ?? DEFAULT_ACTION_SETTLE_DELAY_MS;
118
+ if (
119
+ !Number.isFinite(settleDelayMs) ||
120
+ settleDelayMs < 0 ||
121
+ settleDelayMs > MAX_ACTION_SETTLE_DELAY_MS
122
+ ) {
123
+ throw new RangeError(
124
+ `settleDelayMs must be between 0 and ${MAX_ACTION_SETTLE_DELAY_MS}`,
125
+ );
126
+ }
127
+
96
128
  const ReadableComponent = forwardRef(
97
129
  (props: PropsWithoutRef<T>, outerRef: ForwardedRef<any>) => {
98
130
  const isNestedReadable = useContext(ReadableContext);
@@ -127,6 +159,10 @@ export const makeAssistantVisible = <T extends ComponentType<any>>(
127
159
  {...(props as any)}
128
160
  {...(config?.clickable ? { "data-click-id": clickId } : {})}
129
161
  {...(config?.editable ? { "data-edit-id": clickId } : {})}
162
+ {...(config?.settleDelayMs !== undefined &&
163
+ (config.clickable || config.editable)
164
+ ? { "data-action-settle-delay": settleDelayMs }
165
+ : {})}
130
166
  ref={ref}
131
167
  />
132
168
  </ReadableContext.Provider>
@@ -7,6 +7,7 @@ import { useAuiEvent } from "@assistant-ui/store";
7
7
 
8
8
  export namespace AssistantModalPrimitiveRoot {
9
9
  export type Props = PopoverPrimitive.PopoverProps & {
10
+ /** @deprecated Experimental since 2024-10-12. Not scheduled for removal; the API may change in any release. */
10
11
  unstable_openOnRunStart?: boolean | undefined;
11
12
  };
12
13
  }
@@ -1,4 +1,7 @@
1
1
  export { AttachmentPrimitiveRoot as Root } from "./attachment/AttachmentRoot";
2
- export { AttachmentPrimitiveThumb as unstable_Thumb } from "./attachment/AttachmentThumb";
2
+ export {
3
+ /** @deprecated Experimental since 2024-10-10. Not scheduled for removal; the API may change in any release. */
4
+ AttachmentPrimitiveThumb as unstable_Thumb,
5
+ } from "./attachment/AttachmentThumb";
3
6
  export { AttachmentPrimitiveName as Name } from "./attachment/AttachmentName";
4
7
  export { AttachmentPrimitiveRemove as Remove } from "./attachment/AttachmentRemove";