@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.
- package/dist/hooks/useToolCallElapsed.d.ts +3 -1
- package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
- package/dist/hooks/useToolCallElapsed.js +7 -8
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/model-context/makeAssistantVisible.d.ts +1 -0
- package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +31 -12
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +1 -1
- package/dist/primitives/composer/ComposerInput.js +1 -1
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/index.d.ts +1 -1
- package/dist/primitives/composer.d.ts +20 -1
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +8 -1
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
- package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
- package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
- package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +2 -2
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +111 -2
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/thread.d.ts +2 -0
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/thread.js +5 -2
- package/dist/primitives/thread.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +6 -1
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +10 -6
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/utils/smooth/useSmooth.js +6 -6
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +5 -4
- package/src/hooks/useToolCallElapsed.test.tsx +35 -0
- package/src/hooks/useToolCallElapsed.ts +9 -7
- package/src/index.ts +28 -3
- package/src/model-context/makeAssistantVisible.test.tsx +315 -5
- package/src/model-context/makeAssistantVisible.tsx +43 -7
- package/src/primitives/composer/ComposerInput.tsx +1 -1
- package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
- package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
- package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
- package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
- package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
- package/src/primitives/composer.ts +21 -1
- package/src/primitives/message/MessageRoot.test.tsx +125 -2
- package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
- package/src/primitives/thread/ThreadMessages.tsx +1 -0
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +256 -5
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +147 -1
- package/src/primitives/thread.ts +2 -0
- package/src/sandbox-host/SandboxHost.test.tsx +34 -0
- package/src/sandbox-host/SandboxHost.tsx +8 -4
- package/src/tests/toolCallTiming.test.tsx +19 -2
- package/src/unstable/useMessageStallDetection.test.tsx +40 -0
- package/src/unstable/useMessageStallDetection.ts +10 -6
- package/src/utils/smooth/useSmooth.test.tsx +16 -5
- 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
|
|
498
|
-
type 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 {
|
|
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
|
|
38
|
+
type ClickTool = {
|
|
39
|
+
execute: (args: { clickId: string }) => Promise<unknown>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type VisibleToolsProvider = {
|
|
34
43
|
getModelContext: () => {
|
|
35
44
|
tools: {
|
|
36
|
-
edit
|
|
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
|
|
90
|
-
|
|
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
|
|
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?: {
|
|
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 `
|
|
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 {
|
|
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?:
|
|
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.
|
|
92
|
+
* Must be placed inside `ComposerPrimitive.TriggerPopoverRoot`.
|
|
93
93
|
*
|
|
94
94
|
* @example
|
|
95
95
|
* ```tsx
|
|
96
|
-
* <ComposerPrimitive.
|
|
96
|
+
* <ComposerPrimitive.TriggerPopover
|
|
97
97
|
* char="@"
|
|
98
98
|
* adapter={mentionAdapter}
|
|
99
99
|
* >
|
|
100
|
-
* <ComposerPrimitive.
|
|
101
|
-
* <ComposerPrimitive.
|
|
100
|
+
* <ComposerPrimitive.TriggerPopover.Directive formatter={formatter} />
|
|
101
|
+
* <ComposerPrimitive.TriggerPopoverCategories>
|
|
102
102
|
* {(cats) => cats.map(...)}
|
|
103
|
-
* </ComposerPrimitive.
|
|
104
|
-
* <ComposerPrimitive.
|
|
103
|
+
* </ComposerPrimitive.TriggerPopoverCategories>
|
|
104
|
+
* <ComposerPrimitive.TriggerPopoverItems>
|
|
105
105
|
* {(items) => items.map(...)}
|
|
106
|
-
* </ComposerPrimitive.
|
|
107
|
-
* </ComposerPrimitive.
|
|
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
|
-
|
|
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 `
|
|
15
|
-
readonly formatter?:
|
|
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:
|
|
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.
|
|
25
|
+
* Exposed as `ComposerPrimitive.TriggerPopover.Action`.
|
|
29
26
|
*
|
|
30
27
|
* @example
|
|
31
28
|
* ```tsx
|
|
32
|
-
* <ComposerPrimitive.
|
|
33
|
-
* <ComposerPrimitive.
|
|
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.
|
|
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 ??
|
|
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 {
|
|
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
|
|
21
|
+
children: (categories: readonly TriggerCategory[]) => ReactNode;
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|