@assistant-ui/react 0.15.9 → 0.15.11

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.
@@ -322,6 +322,130 @@ describe("ExternalThread composer", () => {
322
322
  expect(steer).not.toHaveBeenCalled();
323
323
  });
324
324
 
325
+ it("dispatches a same-tick beginEdit + setText + send sequence", async () => {
326
+ const onEdit = vi.fn();
327
+ const { aui } = renderThread({
328
+ messages: [
329
+ {
330
+ id: "u1",
331
+ role: "user",
332
+ content: [{ type: "text", text: "hi" }],
333
+ createdAt: new Date(0),
334
+ attachments: [],
335
+ metadata: { custom: {} },
336
+ } as unknown as ExternalThreadMessage,
337
+ ],
338
+ isRunning: false,
339
+ onEdit,
340
+ });
341
+
342
+ const composer = () => aui().thread.message({ id: "u1" }).composer();
343
+ composer().beginEdit();
344
+ composer().setText("edited");
345
+ composer().send();
346
+
347
+ await waitFor(() => expect(onEdit).toHaveBeenCalledTimes(1));
348
+ expect(onEdit.mock.calls[0]![0]).toMatchObject({
349
+ sourceId: "u1",
350
+ content: [{ type: "text", text: "edited" }],
351
+ });
352
+ await waitFor(() => expect(composer().getState().isEditing).toBe(false));
353
+ });
354
+
355
+ it("prefills the edit composer from the message on beginEdit", async () => {
356
+ const onEdit = vi.fn();
357
+ const { aui } = renderThread({
358
+ messages: [
359
+ {
360
+ id: "a1",
361
+ role: "assistant",
362
+ content: [{ type: "text", text: "original answer" }],
363
+ createdAt: new Date(0),
364
+ attachments: [
365
+ {
366
+ id: "att1",
367
+ type: "file",
368
+ name: "a.txt",
369
+ contentType: "text/plain",
370
+ status: { type: "complete" },
371
+ content: [],
372
+ },
373
+ ],
374
+ metadata: { custom: {} },
375
+ } as unknown as ExternalThreadMessage,
376
+ ],
377
+ isRunning: false,
378
+ onEdit,
379
+ });
380
+
381
+ const composer = () => aui().thread.message({ id: "a1" }).composer();
382
+ composer().beginEdit();
383
+ await waitFor(() => {
384
+ const state = composer().getState();
385
+ expect(state.text).toBe("original answer");
386
+ expect(state.role).toBe("assistant");
387
+ expect(state.attachments).toHaveLength(1);
388
+ });
389
+ expect(() => composer().beginEdit()).toThrow("Edit already in progress");
390
+
391
+ composer().send();
392
+ await waitFor(() => expect(onEdit).toHaveBeenCalledTimes(1));
393
+ expect(onEdit.mock.calls[0]![0]).toMatchObject({
394
+ sourceId: "a1",
395
+ content: [{ type: "text", text: "original answer" }],
396
+ });
397
+ });
398
+
399
+ it("throws on edit-composer send before beginEdit", () => {
400
+ const { aui } = renderThread({
401
+ messages: [
402
+ {
403
+ id: "u1",
404
+ role: "user",
405
+ content: [{ type: "text", text: "hi" }],
406
+ createdAt: new Date(0),
407
+ attachments: [],
408
+ metadata: { custom: {} },
409
+ } as unknown as ExternalThreadMessage,
410
+ ],
411
+ isRunning: false,
412
+ onEdit: vi.fn(),
413
+ });
414
+
415
+ expect(() => aui().thread.message({ id: "u1" }).composer().send()).toThrow(
416
+ "Composer is not available",
417
+ );
418
+ });
419
+
420
+ it("throws on beginEdit when the runtime has no edit handler", () => {
421
+ const { aui } = renderThread({
422
+ messages: [
423
+ {
424
+ id: "u1",
425
+ role: "user",
426
+ content: [{ type: "text", text: "hi" }],
427
+ createdAt: new Date(0),
428
+ attachments: [],
429
+ metadata: { custom: {} },
430
+ } as unknown as ExternalThreadMessage,
431
+ ],
432
+ isRunning: false,
433
+ queue: {
434
+ items: [],
435
+ steerItems: [],
436
+ enqueue: vi.fn(),
437
+ steer: vi.fn(),
438
+ move: vi.fn(),
439
+ edit: vi.fn(),
440
+ remove: vi.fn(),
441
+ },
442
+ });
443
+
444
+ expect(() =>
445
+ aui().thread.message({ id: "u1" }).composer().beginEdit(),
446
+ ).toThrow("Runtime does not support editing.");
447
+ });
448
+
325
449
  it("still refuses to send an empty composer synchronously after a send", async () => {
326
450
  const onNew = vi.fn();
327
451
  const { aui } = renderThread({ messages: [], isRunning: false, onNew });
@@ -0,0 +1,55 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { render, waitFor } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { describe, it, expect } from "vitest";
6
+ import { useAui, AuiProvider } from "@assistant-ui/store";
7
+ import { InMemoryThreadList } from "../client/InMemoryThreadList";
8
+ import { ExternalThread } from "../client/ExternalThread";
9
+
10
+ const renderThreads = () => {
11
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
12
+ const Capture: FC = () => {
13
+ captured.aui = useAui();
14
+ return null;
15
+ };
16
+ const App: FC = () => {
17
+ const aui = useAui({
18
+ threads: InMemoryThreadList({
19
+ thread: () => ExternalThread({ messages: [] }),
20
+ }),
21
+ });
22
+ return (
23
+ <AuiProvider value={aui}>
24
+ <Capture />
25
+ </AuiProvider>
26
+ );
27
+ };
28
+ render(<App />);
29
+ return { aui: () => captured.aui! };
30
+ };
31
+
32
+ describe("InMemoryThreadList delete", () => {
33
+ it("falls back to a live thread when the switch target is deleted in the same tick", async () => {
34
+ const { aui } = renderThreads();
35
+
36
+ aui().threads.switchToNewThread();
37
+ await waitFor(() =>
38
+ expect(aui().threads.getState().mainThreadId).not.toBe("main"),
39
+ );
40
+ const newId = aui().threads.getState().mainThreadId;
41
+ aui().threads.switchToThread("main");
42
+ await waitFor(() =>
43
+ expect(aui().threads.getState().mainThreadId).toBe("main"),
44
+ );
45
+
46
+ aui().threads.switchToThread(newId);
47
+ aui().threads.item({ id: newId }).delete();
48
+
49
+ await waitFor(() => {
50
+ const state = aui().threads.getState();
51
+ expect(state.mainThreadId).toBe("main");
52
+ expect(state.threadIds).not.toContain(newId);
53
+ });
54
+ });
55
+ });