@assistant-ui/react 0.15.24 → 0.15.25

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 (90) 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 +7 -8
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +10 -4
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -3
  8. package/dist/model-context/makeAssistantVisible.d.ts +1 -0
  9. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  10. package/dist/model-context/makeAssistantVisible.js +31 -12
  11. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  12. package/dist/primitives/composer/ComposerInput.d.ts +1 -1
  13. package/dist/primitives/composer/ComposerInput.js +1 -1
  14. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  15. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
  16. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  17. package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
  18. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  19. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
  20. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
  21. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
  22. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  23. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
  24. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  25. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  26. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
  27. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
  28. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
  29. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  30. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
  31. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  32. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  33. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
  34. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
  35. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  36. package/dist/primitives/composer/trigger/index.d.ts +1 -1
  37. package/dist/primitives/composer.d.ts +20 -1
  38. package/dist/primitives/composer.d.ts.map +1 -1
  39. package/dist/primitives/composer.js +8 -1
  40. package/dist/primitives/composer.js.map +1 -1
  41. package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
  42. package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
  43. package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
  44. package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
  45. package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
  46. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  47. package/dist/primitives/thread/ThreadMessages.js +2 -2
  48. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  49. package/dist/primitives/thread/useThreadViewportAutoScroll.js +111 -2
  50. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  51. package/dist/primitives/thread.d.ts +2 -0
  52. package/dist/primitives/thread.d.ts.map +1 -1
  53. package/dist/primitives/thread.js +5 -2
  54. package/dist/primitives/thread.js.map +1 -1
  55. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  56. package/dist/sandbox-host/SandboxHost.js +6 -1
  57. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  58. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  59. package/dist/unstable/useMessageStallDetection.js +10 -6
  60. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  61. package/dist/utils/smooth/useSmooth.js +6 -6
  62. package/dist/utils/smooth/useSmooth.js.map +1 -1
  63. package/package.json +5 -4
  64. package/src/hooks/useToolCallElapsed.test.tsx +35 -0
  65. package/src/hooks/useToolCallElapsed.ts +9 -7
  66. package/src/index.ts +28 -3
  67. package/src/model-context/makeAssistantVisible.test.tsx +315 -5
  68. package/src/model-context/makeAssistantVisible.tsx +43 -7
  69. package/src/primitives/composer/ComposerInput.tsx +1 -1
  70. package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
  71. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
  72. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
  73. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
  74. package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
  75. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
  76. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
  77. package/src/primitives/composer.ts +21 -1
  78. package/src/primitives/message/MessageRoot.test.tsx +125 -2
  79. package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
  80. package/src/primitives/thread/ThreadMessages.tsx +1 -0
  81. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +256 -5
  82. package/src/primitives/thread/useThreadViewportAutoScroll.ts +147 -1
  83. package/src/primitives/thread.ts +2 -0
  84. package/src/sandbox-host/SandboxHost.test.tsx +34 -0
  85. package/src/sandbox-host/SandboxHost.tsx +8 -4
  86. package/src/tests/toolCallTiming.test.tsx +19 -2
  87. package/src/unstable/useMessageStallDetection.test.tsx +40 -0
  88. package/src/unstable/useMessageStallDetection.ts +10 -6
  89. package/src/utils/smooth/useSmooth.test.tsx +16 -5
  90. package/src/utils/smooth/useSmooth.ts +7 -7
package/src/index.ts CHANGED
@@ -333,6 +333,11 @@ export * as ThreadListItemMorePrimitive from "./primitives/threadListItemMore";
333
333
  export * as SelectionToolbarPrimitive from "./primitives/selectionToolbar";
334
334
 
335
335
  export { groupPartByType, type GroupByContext } from "@assistant-ui/core/react";
336
+ export {
337
+ createThreadRowsSelector,
338
+ type ThreadRow,
339
+ type ThreadRowsOptions,
340
+ } from "@assistant-ui/core/react";
336
341
  export { unstable_useThreadMessageIds } from "@assistant-ui/core/react";
337
342
  export { useMessagePartText } from "./primitives/messagePart/useMessagePartText";
338
343
  export { useMessagePartReasoning } from "./primitives/messagePart/useMessagePartReasoning";
@@ -375,8 +380,12 @@ export type {
375
380
  ToolApprovalDisplay,
376
381
  ToolApprovalOption,
377
382
  ToolApprovalOptionKind,
383
+ ToolApprovalAnswer,
384
+ ToolApprovalQuestion,
385
+ ToolApprovalQuestionOption,
378
386
  ToolApprovalResponse,
379
387
  ToolCallMessagePart,
388
+ MessagePartTiming,
380
389
  ToolCallTiming,
381
390
  ToolModelContentPart,
382
391
  MessageStatus,
@@ -494,16 +503,32 @@ export {
494
503
  useTriggerPopoverTriggers as unstable_useTriggerPopoverTriggers,
495
504
  useTriggerPopoverTriggersOptional as unstable_useTriggerPopoverTriggersOptional,
496
505
  type RegisteredTrigger as Unstable_RegisteredTrigger,
497
- type TriggerMatch as Unstable_TriggerMatch,
498
- type TriggerMatcher as Unstable_TriggerMatcher,
506
+ type TriggerMatch,
507
+ type TriggerMatcher,
499
508
  type TriggerBehavior as Unstable_TriggerBehavior,
500
509
  } from "./primitives/composer/trigger";
510
+ import type {
511
+ TriggerMatch,
512
+ TriggerMatcher,
513
+ } from "./primitives/composer/trigger";
514
+ /** @deprecated Use `TriggerMatch` instead. */
515
+ export type Unstable_TriggerMatch = TriggerMatch;
516
+ /** @deprecated Use `TriggerMatcher` instead. */
517
+ export type Unstable_TriggerMatcher = TriggerMatcher;
501
518
  export type {
519
+ DirectiveFormatter,
520
+ DirectiveSegment,
521
+ TriggerAdapter,
522
+ TriggerCategory,
523
+ TriggerItem,
502
524
  Unstable_DirectiveFormatter,
503
525
  Unstable_DirectiveSegment,
504
526
  Unstable_TriggerItem,
505
527
  } from "@assistant-ui/core";
506
- export { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
528
+ export {
529
+ defaultDirectiveFormatter,
530
+ unstable_defaultDirectiveFormatter,
531
+ } from "@assistant-ui/core";
507
532
 
508
533
  // Unstable - composer input history (terminal-style ArrowUp/ArrowDown recall)
509
534
  export {
@@ -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>
@@ -119,7 +119,7 @@ export namespace ComposerPrimitiveInput {
119
119
  * keyboard shortcuts, file paste support, and intelligent focus management.
120
120
  * It integrates with the composer context to manage message state and submission.
121
121
  *
122
- * When rendered inside `Unstable_TriggerPopoverRoot` and a popover is open, the
122
+ * When rendered inside `TriggerPopoverRoot` and a popover is open, the
123
123
  * underlying `<textarea>` automatically receives `aria-controls`,
124
124
  * `aria-expanded`, `aria-haspopup`, and `aria-activedescendant` for the
125
125
  * combobox relationship. These computed attributes override user-provided
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useAui, useAuiState } from "@assistant-ui/store";
4
4
  import { useResource } from "@assistant-ui/tap";
5
- import type { Unstable_TriggerAdapter } from "@assistant-ui/core";
5
+ import type { TriggerAdapter } from "@assistant-ui/core";
6
6
  import {
7
7
  createContext,
8
8
  forwardRef,
@@ -74,7 +74,7 @@ export namespace ComposerPrimitiveTriggerPopover {
74
74
  /** Overrides trigger detection for both textarea and Lexical inputs. `endOffset` is the exclusive replace bound. */
75
75
  readonly matcher?: TriggerMatcher | undefined;
76
76
  /** Adapter providing categories and items. */
77
- readonly adapter?: Unstable_TriggerAdapter | undefined;
77
+ readonly adapter?: TriggerAdapter | undefined;
78
78
  /** Whether the adapter is resolving items, surfaced to the popover scope for async sources. @default false */
79
79
  readonly isLoading?: boolean | undefined;
80
80
  };
@@ -89,22 +89,22 @@ export namespace ComposerPrimitiveTriggerPopover {
89
89
  * `<TriggerPopover.Directive>` or `<TriggerPopover.Action>` as a child. Without
90
90
  * a behavior the trigger stays closed.
91
91
  *
92
- * Must be placed inside `ComposerPrimitive.Unstable_TriggerPopoverRoot`.
92
+ * Must be placed inside `ComposerPrimitive.TriggerPopoverRoot`.
93
93
  *
94
94
  * @example
95
95
  * ```tsx
96
- * <ComposerPrimitive.Unstable_TriggerPopover
96
+ * <ComposerPrimitive.TriggerPopover
97
97
  * char="@"
98
98
  * adapter={mentionAdapter}
99
99
  * >
100
- * <ComposerPrimitive.Unstable_TriggerPopover.Directive formatter={formatter} />
101
- * <ComposerPrimitive.Unstable_TriggerPopoverCategories>
100
+ * <ComposerPrimitive.TriggerPopover.Directive formatter={formatter} />
101
+ * <ComposerPrimitive.TriggerPopoverCategories>
102
102
  * {(cats) => cats.map(...)}
103
- * </ComposerPrimitive.Unstable_TriggerPopoverCategories>
104
- * <ComposerPrimitive.Unstable_TriggerPopoverItems>
103
+ * </ComposerPrimitive.TriggerPopoverCategories>
104
+ * <ComposerPrimitive.TriggerPopoverItems>
105
105
  * {(items) => items.map(...)}
106
- * </ComposerPrimitive.Unstable_TriggerPopoverItems>
107
- * </ComposerPrimitive.Unstable_TriggerPopover>
106
+ * </ComposerPrimitive.TriggerPopoverItems>
107
+ * </ComposerPrimitive.TriggerPopover>
108
108
  * ```
109
109
  */
110
110
  export const ComposerPrimitiveTriggerPopover = forwardRef<
@@ -196,7 +196,7 @@ export const ComposerPrimitiveTriggerPopover = forwardRef<
196
196
  ...(behavior ? { behavior } : {}),
197
197
  resource: getResource(),
198
198
  });
199
- }, [root, char, hasMatcher, behavior]);
199
+ }, [root, char, hasMatcher, registeredMatcher, behavior]);
200
200
 
201
201
  const pluginRegistry = useComposerInputPluginRegistryOptional();
202
202
  useEffect(() => {
@@ -1,20 +1,17 @@
1
1
  "use client";
2
2
 
3
- import type {
4
- Unstable_DirectiveFormatter,
5
- Unstable_TriggerItem,
6
- } from "@assistant-ui/core";
7
- import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
3
+ import type { DirectiveFormatter, TriggerItem } from "@assistant-ui/core";
4
+ import { defaultDirectiveFormatter } from "@assistant-ui/core";
8
5
  import { useEffect, useInsertionEffect, useRef, type FC } from "react";
9
6
  import { useTriggerBehaviorRegistration } from "./TriggerPopover";
10
7
  import type { TriggerBehavior } from "./triggerSelectionResource";
11
8
 
12
9
  export namespace ComposerPrimitiveTriggerPopoverAction {
13
10
  export type Props = {
14
- /** Defaults to `unstable_defaultDirectiveFormatter`. */
15
- readonly formatter?: Unstable_DirectiveFormatter | undefined;
11
+ /** Defaults to `defaultDirectiveFormatter`. */
12
+ readonly formatter?: DirectiveFormatter | undefined;
16
13
  /** Fires the moment an item is selected; runs regardless of `removeOnExecute`. */
17
- readonly onExecute: (item: Unstable_TriggerItem) => void;
14
+ readonly onExecute: (item: TriggerItem) => void;
18
15
  /** When true, strips the trigger text after executing. Defaults to `false` (keeps audit-trail chip). */
19
16
  readonly removeOnExecute?: boolean | undefined;
20
17
  };
@@ -25,16 +22,16 @@ export namespace ComposerPrimitiveTriggerPopoverAction {
25
22
  * optionally leaving a directive chip behind as an audit trail. Render exactly
26
23
  * one behavior sub-primitive per `<TriggerPopover>`.
27
24
  *
28
- * Exposed as `ComposerPrimitive.Unstable_TriggerPopover.Action`.
25
+ * Exposed as `ComposerPrimitive.TriggerPopover.Action`.
29
26
  *
30
27
  * @example
31
28
  * ```tsx
32
- * <ComposerPrimitive.Unstable_TriggerPopover char="/" adapter={slashAdapter}>
33
- * <ComposerPrimitive.Unstable_TriggerPopover.Action
29
+ * <ComposerPrimitive.TriggerPopover char="/" adapter={slashAdapter}>
30
+ * <ComposerPrimitive.TriggerPopover.Action
34
31
  * onExecute={(item) => commandHandlers[item.id]?.()}
35
32
  * removeOnExecute={false}
36
33
  * />
37
- * </ComposerPrimitive.Unstable_TriggerPopover>
34
+ * </ComposerPrimitive.TriggerPopover>
38
35
  * ```
39
36
  */
40
37
  export const ComposerPrimitiveTriggerPopoverAction: FC<
@@ -49,7 +46,7 @@ export const ComposerPrimitiveTriggerPopoverAction: FC<
49
46
  useEffect(() => {
50
47
  const behavior: TriggerBehavior = {
51
48
  kind: "action",
52
- formatter: formatter ?? unstable_defaultDirectiveFormatter,
49
+ formatter: formatter ?? defaultDirectiveFormatter,
53
50
  onExecute: (item) => onExecuteRef.current(item),
54
51
  ...(removeOnExecute !== undefined ? { removeOnExecute } : {}),
55
52
  };
@@ -10,7 +10,7 @@ import {
10
10
  } from "react";
11
11
  import { composeEventHandlers } from "radix-ui/internal";
12
12
  import { useTriggerPopoverScopeContext } from "./TriggerPopover";
13
- import type { Unstable_TriggerCategory } from "@assistant-ui/core";
13
+ import type { TriggerCategory } from "@assistant-ui/core";
14
14
 
15
15
  export namespace ComposerPrimitiveTriggerPopoverCategories {
16
16
  export type Element = ComponentRef<typeof Primitive.div>;
@@ -18,7 +18,7 @@ export namespace ComposerPrimitiveTriggerPopoverCategories {
18
18
  ComponentPropsWithoutRef<typeof Primitive.div>,
19
19
  "children"
20
20
  > & {
21
- children: (categories: readonly Unstable_TriggerCategory[]) => ReactNode;
21
+ children: (categories: readonly TriggerCategory[]) => ReactNode;
22
22
  };
23
23
  }
24
24