@assistant-ui/core 0.3.12 → 0.3.13
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/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +5 -1
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +5 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +25 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +64 -11
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +14 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +31 -0
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +1 -6
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -4
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts +15 -0
- package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
- package/dist/runtime/queue/message-queue.js +23 -10
- package/dist/runtime/queue/message-queue.js.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +7 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +3 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +49 -13
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +2 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/store/clients/external-thread.d.ts +7 -0
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +460 -369
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
- package/dist/store/clients/runtime-adapter.js +5 -2
- package/dist/store/clients/runtime-adapter.js.map +1 -1
- package/dist/store/clients/single-thread-list.d.ts.map +1 -1
- package/dist/store/clients/single-thread-list.js +31 -27
- package/dist/store/clients/single-thread-list.js.map +1 -1
- package/dist/store/clients/suggestions.d.ts +3 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +65 -37
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/index.d.ts +2 -2
- package/dist/store/index.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +64 -50
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +98 -82
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scope-registration.d.ts +2 -2
- package/dist/store/scopes/suggestions.d.ts +6 -1
- package/dist/store/scopes/suggestions.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +17 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/types/error.d.ts +15 -1
- package/dist/types/error.d.ts.map +1 -1
- package/dist/types/error.js +18 -1
- package/dist/types/error.js.map +1 -1
- package/dist/types/message.d.ts +2 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/react/client/interactable-model-context.ts +6 -1
- package/src/react/runtimes/useExternalStoreRuntime.suggestions.test.tsx +114 -0
- package/src/runtime/api/thread-runtime.ts +8 -1
- package/src/runtime/base/base-composer-runtime-core.ts +100 -8
- package/src/runtime/base/base-thread-runtime-core.ts +36 -0
- package/src/runtime/base/default-edit-composer-runtime-core.ts +1 -15
- package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -11
- package/src/runtime/queue/external-thread-queue-adapter.ts +17 -0
- package/src/runtime/queue/message-queue.ts +26 -9
- package/src/runtimes/external-store/external-store-adapter.ts +7 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +100 -26
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
- package/src/store/clients/external-thread.ts +119 -16
- package/src/store/clients/runtime-adapter.ts +5 -2
- package/src/store/clients/single-thread-list.ts +2 -1
- package/src/store/clients/suggestions.ts +39 -13
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -28
- package/src/store/runtime-clients/thread-runtime-client.ts +5 -0
- package/src/store/scopes/suggestions.ts +6 -0
- package/src/store/scopes/thread.ts +17 -0
- package/src/tests/append-interactable-snapshots.test.ts +350 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +126 -0
- package/src/tests/base-composer-runtime-core.test.ts +108 -1
- package/src/tests/default-edit-composer-runtime-core.test.ts +0 -68
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +300 -0
- package/src/tests/external-thread-attachments.test.tsx +214 -0
- package/src/tests/external-thread-parity.test.tsx +54 -0
- package/src/tests/external-thread-refetch.test.tsx +117 -0
- package/src/tests/external-thread-suggestions.test.tsx +59 -0
- package/src/tests/message-queue.test.ts +26 -0
- package/src/tests/thread-message-like.test.ts +39 -0
- package/src/tests/thread-switch-events.test.tsx +136 -0
- package/src/types/error.ts +24 -0
- package/src/types/message.ts +2 -0
- package/src/tests/default-thread-composer-runtime-core.test.ts +0 -82
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { cleanup, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { FC } from "react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { AuiProvider, useAui } from "@assistant-ui/store";
|
|
8
|
+
import { ExternalThread } from "../store/clients/external-thread";
|
|
9
|
+
import type { ThreadMessage } from "../types/message";
|
|
10
|
+
|
|
11
|
+
const message = (id: string, role: "user" | "assistant"): ThreadMessage =>
|
|
12
|
+
({
|
|
13
|
+
id,
|
|
14
|
+
role,
|
|
15
|
+
content: [{ type: "text", text: `text of ${id}` }],
|
|
16
|
+
createdAt: new Date(1718000000000),
|
|
17
|
+
...(role === "assistant"
|
|
18
|
+
? { status: { type: "complete", reason: "stop" } }
|
|
19
|
+
: { attachments: [] }),
|
|
20
|
+
metadata: { custom: {} },
|
|
21
|
+
}) as ThreadMessage;
|
|
22
|
+
|
|
23
|
+
let aui!: ReturnType<typeof useAui>;
|
|
24
|
+
|
|
25
|
+
const Capture: FC = () => {
|
|
26
|
+
aui = useAui();
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const renderThread = (props: Parameters<typeof ExternalThread>[0]) => {
|
|
31
|
+
const App: FC = () => {
|
|
32
|
+
const value = useAui({ thread: ExternalThread(props) });
|
|
33
|
+
return (
|
|
34
|
+
<AuiProvider value={value}>
|
|
35
|
+
<Capture />
|
|
36
|
+
</AuiProvider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
render(<App />);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
cleanup();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("ExternalThread refetch", () => {
|
|
47
|
+
it("routes reloadMainThread to onRefetchThread and reports the capability", async () => {
|
|
48
|
+
const onRefetchThread = vi.fn(async () => {});
|
|
49
|
+
renderThread({ messages: [], onRefetchThread });
|
|
50
|
+
|
|
51
|
+
await waitFor(() =>
|
|
52
|
+
expect(
|
|
53
|
+
aui.threads.thread("main").getState().capabilities.refetchThread,
|
|
54
|
+
).toBe(true),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await aui.threads.reloadMainThread();
|
|
58
|
+
expect(onRefetchThread).toHaveBeenCalledTimes(1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("resolves reloadMainThread without a callback and reports the capability off", async () => {
|
|
62
|
+
renderThread({ messages: [] });
|
|
63
|
+
|
|
64
|
+
await waitFor(() =>
|
|
65
|
+
expect(
|
|
66
|
+
aui.threads.thread("main").getState().capabilities.refetchThread,
|
|
67
|
+
).toBe(false),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await expect(aui.threads.reloadMainThread()).resolves.toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("propagates a refetch rejection to the caller", async () => {
|
|
74
|
+
const onRefetchThread = vi.fn(async () => {
|
|
75
|
+
throw new Error("refetch failed");
|
|
76
|
+
});
|
|
77
|
+
renderThread({ messages: [], onRefetchThread });
|
|
78
|
+
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(
|
|
81
|
+
aui.threads.thread("main").getState().capabilities.refetchThread,
|
|
82
|
+
).toBe(true),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await expect(aui.threads.reloadMainThread()).rejects.toThrow(
|
|
86
|
+
"refetch failed",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("applies refetched messages in place with the composer draft intact", async () => {
|
|
91
|
+
const App: FC = () => {
|
|
92
|
+
const [messages, setMessages] = useState<readonly ThreadMessage[]>([]);
|
|
93
|
+
const value = useAui({
|
|
94
|
+
thread: ExternalThread({
|
|
95
|
+
messages,
|
|
96
|
+
onRefetchThread: async () => {
|
|
97
|
+
setMessages([message("a1", "assistant")]);
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
return (
|
|
102
|
+
<AuiProvider value={value}>
|
|
103
|
+
<Capture />
|
|
104
|
+
</AuiProvider>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
render(<App />);
|
|
108
|
+
|
|
109
|
+
aui.thread.composer().setText("draft");
|
|
110
|
+
await aui.threads.reloadMainThread();
|
|
111
|
+
|
|
112
|
+
await waitFor(() =>
|
|
113
|
+
expect(aui.thread.getState().messages.map((m) => m.id)).toEqual(["a1"]),
|
|
114
|
+
);
|
|
115
|
+
expect(aui.thread.composer().getState().text).toBe("draft");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { cleanup, render } from "@testing-library/react";
|
|
4
|
+
import type { FC, ReactNode } from "react";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { AuiProvider, useAui } from "@assistant-ui/store";
|
|
7
|
+
import { ExternalThread } from "../store/clients/external-thread";
|
|
8
|
+
import { SingleThreadList } from "../store/clients/single-thread-list";
|
|
9
|
+
|
|
10
|
+
let childAui!: ReturnType<typeof useAui>;
|
|
11
|
+
|
|
12
|
+
const CaptureChild: FC = () => {
|
|
13
|
+
childAui = useAui();
|
|
14
|
+
return null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Child: FC = () => {
|
|
18
|
+
const aui = useAui({ thread: ExternalThread({ messages: [] }) });
|
|
19
|
+
return (
|
|
20
|
+
<AuiProvider value={aui}>
|
|
21
|
+
<CaptureChild />
|
|
22
|
+
</AuiProvider>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const Parent: FC<{ children: ReactNode }> = ({ children }) => {
|
|
27
|
+
const aui = useAui({
|
|
28
|
+
threads: SingleThreadList({ thread: ExternalThread({ messages: [] }) }),
|
|
29
|
+
});
|
|
30
|
+
return <AuiProvider value={aui}>{children}</AuiProvider>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
cleanup();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("ExternalThread suggestions scope", () => {
|
|
38
|
+
it("derives the scope from its own thread when standalone", () => {
|
|
39
|
+
render(<Child />);
|
|
40
|
+
|
|
41
|
+
expect(childAui.suggestions.getState()).toEqual({ suggestions: [] });
|
|
42
|
+
expect(childAui.suggestions.getState()).toBe(
|
|
43
|
+
childAui.thread.suggestions().getState(),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("derives the scope from its own thread when nested under a threads parent", () => {
|
|
48
|
+
render(
|
|
49
|
+
<Parent>
|
|
50
|
+
<Child />
|
|
51
|
+
</Parent>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(childAui.suggestions.getState()).toEqual({ suggestions: [] });
|
|
55
|
+
expect(childAui.suggestions.getState()).toBe(
|
|
56
|
+
childAui.thread.suggestions().getState(),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -626,3 +626,29 @@ describe("createMessageQueue", () => {
|
|
|
626
626
|
expect(adapter.items).toHaveLength(0);
|
|
627
627
|
});
|
|
628
628
|
});
|
|
629
|
+
|
|
630
|
+
describe("createMessageQueue interrupt with a runtime-routed cancel", () => {
|
|
631
|
+
it("keeps draining when the driver's cancel notifies back", () => {
|
|
632
|
+
const runs: string[] = [];
|
|
633
|
+
const controller = createMessageQueue({
|
|
634
|
+
run: (message) => {
|
|
635
|
+
runs.push(
|
|
636
|
+
message.content[0]!.type === "text" ? message.content[0].text : "",
|
|
637
|
+
);
|
|
638
|
+
},
|
|
639
|
+
cancel: () => controller.notifyCancelled(),
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
controller.adapter.enqueue(msg("first"));
|
|
643
|
+
controller.adapter.enqueue(msg("second"));
|
|
644
|
+
controller.adapter.steer(msg("steered"));
|
|
645
|
+
|
|
646
|
+
expect(runs).toEqual(["first", "steered"]);
|
|
647
|
+
|
|
648
|
+
// the interrupted run and the steer run each settle once
|
|
649
|
+
controller.notifyIdle();
|
|
650
|
+
controller.notifyIdle();
|
|
651
|
+
|
|
652
|
+
expect(runs).toEqual(["first", "steered", "second"]);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
@@ -382,3 +382,42 @@ describe("fromThreadMessageLike", () => {
|
|
|
382
382
|
});
|
|
383
383
|
});
|
|
384
384
|
});
|
|
385
|
+
|
|
386
|
+
describe("fromThreadMessageLike provider metadata", () => {
|
|
387
|
+
it("keeps it on image and file parts", () => {
|
|
388
|
+
const result = fromThreadMessageLike(
|
|
389
|
+
{
|
|
390
|
+
role: "user",
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "image",
|
|
394
|
+
image: "https://example.com/cat.png",
|
|
395
|
+
providerMetadata: { agui: { file_id: "f_1" } },
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
type: "file",
|
|
399
|
+
data: "https://example.com/spec.pdf",
|
|
400
|
+
mimeType: "application/pdf",
|
|
401
|
+
providerMetadata: { agui: { file_id: "f_2" } },
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
fallbackId,
|
|
406
|
+
fallbackStatus,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(result.content).toEqual([
|
|
410
|
+
{
|
|
411
|
+
type: "image",
|
|
412
|
+
image: "https://example.com/cat.png",
|
|
413
|
+
providerMetadata: { agui: { file_id: "f_1" } },
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
type: "file",
|
|
417
|
+
data: "https://example.com/spec.pdf",
|
|
418
|
+
mimeType: "application/pdf",
|
|
419
|
+
providerMetadata: { agui: { file_id: "f_2" } },
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { AuiProvider, useAui, useAuiEvent } from "@assistant-ui/store";
|
|
6
|
+
import { RuntimeAdapter } from "../react/RuntimeAdapter";
|
|
7
|
+
import { AssistantRuntimeImpl } from "../runtime/api/assistant-runtime";
|
|
8
|
+
import { ExternalStoreRuntimeCore } from "../runtimes/external-store/external-store-runtime-core";
|
|
9
|
+
import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
|
|
10
|
+
|
|
11
|
+
type DemoMessage = { id: string; role: "user" | "assistant"; text: string };
|
|
12
|
+
|
|
13
|
+
const createRuntime = () => {
|
|
14
|
+
const threads = [
|
|
15
|
+
{
|
|
16
|
+
id: "t1",
|
|
17
|
+
title: "one",
|
|
18
|
+
messages: [{ id: "m1", role: "user" as const, text: "a" }],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "t2",
|
|
22
|
+
title: "two",
|
|
23
|
+
messages: [{ id: "m2", role: "user" as const, text: "b" }],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
let currentId = "t1";
|
|
27
|
+
const makeAdapter = (): ExternalStoreAdapter<DemoMessage> => ({
|
|
28
|
+
messages: threads.find((t) => t.id === currentId)!.messages,
|
|
29
|
+
convertMessage: (m) => ({
|
|
30
|
+
id: m.id,
|
|
31
|
+
role: m.role,
|
|
32
|
+
content: [{ type: "text", text: m.text }],
|
|
33
|
+
}),
|
|
34
|
+
onNew: async () => {},
|
|
35
|
+
adapters: {
|
|
36
|
+
threadList: {
|
|
37
|
+
threadId: currentId,
|
|
38
|
+
threads: threads.map((t) => ({
|
|
39
|
+
status: "regular" as const,
|
|
40
|
+
id: t.id,
|
|
41
|
+
title: t.title,
|
|
42
|
+
})),
|
|
43
|
+
onSwitchToThread: (threadId: string) => {
|
|
44
|
+
currentId = threadId;
|
|
45
|
+
sync();
|
|
46
|
+
},
|
|
47
|
+
onSwitchToNewThread: () => {},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const core = new ExternalStoreRuntimeCore(makeAdapter());
|
|
52
|
+
const runtime = new AssistantRuntimeImpl(core);
|
|
53
|
+
const sync = () => core.setAdapter(makeAdapter());
|
|
54
|
+
return runtime;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("thread switch events", () => {
|
|
58
|
+
it("delivers switchedTo to default-scope, star-scope, and aui.on listeners", async () => {
|
|
59
|
+
const runtime = createRuntime();
|
|
60
|
+
const defaultScope = vi.fn();
|
|
61
|
+
const starScope = vi.fn();
|
|
62
|
+
const auiOn = vi.fn();
|
|
63
|
+
const switchedAwayStar = vi.fn();
|
|
64
|
+
let aui!: ReturnType<typeof useAui>;
|
|
65
|
+
const Consumer = () => {
|
|
66
|
+
useAuiEvent("threadListItem.switchedTo" as never, defaultScope as never);
|
|
67
|
+
useAuiEvent(
|
|
68
|
+
{ scope: "*", event: "threadListItem.switchedTo" } as never,
|
|
69
|
+
starScope as never,
|
|
70
|
+
);
|
|
71
|
+
useAuiEvent(
|
|
72
|
+
{ scope: "*", event: "threadListItem.switchedAway" } as never,
|
|
73
|
+
switchedAwayStar as never,
|
|
74
|
+
);
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
const Harness = () => {
|
|
78
|
+
aui = useAui({ threads: RuntimeAdapter(runtime) } as never);
|
|
79
|
+
return (
|
|
80
|
+
<AuiProvider value={aui}>
|
|
81
|
+
<Consumer />
|
|
82
|
+
</AuiProvider>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
render(<Harness />);
|
|
86
|
+
await act(async () => {});
|
|
87
|
+
|
|
88
|
+
aui.on("threadListItem.switchedTo" as never, auiOn as never);
|
|
89
|
+
|
|
90
|
+
await act(async () => {
|
|
91
|
+
aui.threads.item({ index: 1 }).switchTo();
|
|
92
|
+
});
|
|
93
|
+
await act(async () => {});
|
|
94
|
+
|
|
95
|
+
expect(defaultScope).toHaveBeenCalledExactlyOnceWith({ threadId: "t2" });
|
|
96
|
+
expect(starScope).toHaveBeenCalledExactlyOnceWith({ threadId: "t2" });
|
|
97
|
+
expect(auiOn).toHaveBeenCalledExactlyOnceWith({ threadId: "t2" });
|
|
98
|
+
expect(switchedAwayStar).toHaveBeenCalledExactlyOnceWith({
|
|
99
|
+
threadId: "t1",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await act(async () => {
|
|
103
|
+
aui.threads.item({ index: 0 }).switchTo();
|
|
104
|
+
});
|
|
105
|
+
await act(async () => {});
|
|
106
|
+
|
|
107
|
+
expect(defaultScope).toHaveBeenCalledTimes(2);
|
|
108
|
+
expect(defaultScope).toHaveBeenLastCalledWith({ threadId: "t1" });
|
|
109
|
+
expect(switchedAwayStar).toHaveBeenCalledTimes(2);
|
|
110
|
+
expect(switchedAwayStar).toHaveBeenLastCalledWith({ threadId: "t2" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not emit for the initially selected thread on mount", async () => {
|
|
114
|
+
const runtime = createRuntime();
|
|
115
|
+
const anySwitch = vi.fn();
|
|
116
|
+
const Consumer = () => {
|
|
117
|
+
useAuiEvent(
|
|
118
|
+
{ scope: "*", event: "threadListItem.switchedTo" } as never,
|
|
119
|
+
anySwitch as never,
|
|
120
|
+
);
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
const Harness = () => {
|
|
124
|
+
const aui = useAui({ threads: RuntimeAdapter(runtime) } as never);
|
|
125
|
+
return (
|
|
126
|
+
<AuiProvider value={aui}>
|
|
127
|
+
<Consumer />
|
|
128
|
+
</AuiProvider>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
render(<Harness />);
|
|
132
|
+
await act(async () => {});
|
|
133
|
+
|
|
134
|
+
expect(anySwitch).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
package/src/types/error.ts
CHANGED
|
@@ -52,6 +52,30 @@ export const isAssistantError = (value: unknown): value is AssistantError => {
|
|
|
52
52
|
return true;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
const MESSAGE_NOT_SENT = Symbol.for("assistant-ui.message-not-sent");
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Rejection reason for a send that never reached the backend, so nothing ran
|
|
59
|
+
* and nothing is recoverable from the thread. A runtime adapter throws it from
|
|
60
|
+
* `onNew` to hand the message back to the thread composer, which restores the
|
|
61
|
+
* draft it cleared at dispatch time when nothing has claimed the composer
|
|
62
|
+
* since. An edit composer closes at dispatch, so a rejected edit is not
|
|
63
|
+
* restored.
|
|
64
|
+
*/
|
|
65
|
+
export class MessageNotSentError extends Error {
|
|
66
|
+
readonly [MESSAGE_NOT_SENT] = true;
|
|
67
|
+
|
|
68
|
+
constructor(message = "The message was not sent.") {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "MessageNotSentError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const isMessageNotSentError = (
|
|
75
|
+
error: unknown,
|
|
76
|
+
): error is MessageNotSentError =>
|
|
77
|
+
typeof error === "object" && error !== null && MESSAGE_NOT_SENT in error;
|
|
78
|
+
|
|
55
79
|
export const toAssistantError = (error: unknown): AssistantError => {
|
|
56
80
|
if (isAssistantError(error)) return error;
|
|
57
81
|
if (error instanceof Error) {
|
package/src/types/message.ts
CHANGED
|
@@ -56,6 +56,7 @@ export type ImageMessagePart = {
|
|
|
56
56
|
readonly type: "image";
|
|
57
57
|
readonly image: string;
|
|
58
58
|
readonly filename?: string;
|
|
59
|
+
readonly providerMetadata?: PartProviderMetadata;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
export type FileMessagePart = {
|
|
@@ -65,6 +66,7 @@ export type FileMessagePart = {
|
|
|
65
66
|
readonly mimeType: string;
|
|
66
67
|
/** How `data` goes on the wire: a url or id reference; omitted = inferred (http(s) → url, else base64). "url" is honored by the LangChain-family, A2A, AG-UI, and Google ADK runtimes; "id" by the LangChain family only. */
|
|
67
68
|
readonly sourceType?: "url" | "id";
|
|
69
|
+
readonly providerMetadata?: PartProviderMetadata;
|
|
68
70
|
readonly parentId?: string;
|
|
69
71
|
};
|
|
70
72
|
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { DefaultThreadComposerRuntimeCore } from "../runtime/base/default-thread-composer-runtime-core";
|
|
3
|
-
import type { ThreadRuntimeCore } from "../runtime/interfaces/thread-runtime-core";
|
|
4
|
-
import type { AppendMessage, ThreadMessage } from "../types/message";
|
|
5
|
-
|
|
6
|
-
const makeRuntime = (options?: {
|
|
7
|
-
messages?: ThreadMessage[];
|
|
8
|
-
composerMetadata?: Record<string, unknown>;
|
|
9
|
-
}) => {
|
|
10
|
-
const append = vi.fn();
|
|
11
|
-
const runtime = {
|
|
12
|
-
append,
|
|
13
|
-
getModelContext: () => ({
|
|
14
|
-
...(options?.composerMetadata
|
|
15
|
-
? { unstable_composerMetadata: options.composerMetadata }
|
|
16
|
-
: {}),
|
|
17
|
-
}),
|
|
18
|
-
subscribe: () => () => {},
|
|
19
|
-
capabilities: { cancel: false },
|
|
20
|
-
messages: options?.messages ?? [],
|
|
21
|
-
isSendDisabled: false,
|
|
22
|
-
composer: { runConfig: {} },
|
|
23
|
-
} as unknown as Omit<ThreadRuntimeCore, "composer">;
|
|
24
|
-
return { runtime, append };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const snapshotMessage = (id: string, state: unknown): ThreadMessage =>
|
|
28
|
-
({
|
|
29
|
-
id,
|
|
30
|
-
role: "user",
|
|
31
|
-
createdAt: new Date(),
|
|
32
|
-
content: [{ type: "text", text: "old" }],
|
|
33
|
-
attachments: [],
|
|
34
|
-
metadata: {
|
|
35
|
-
custom: { interactables: [{ id: "n1", name: "note", state }] },
|
|
36
|
-
},
|
|
37
|
-
}) as ThreadMessage;
|
|
38
|
-
|
|
39
|
-
const live = (state: unknown) => ({
|
|
40
|
-
interactables: [{ id: "n1", name: "note", state }],
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const send = async (runtime: Omit<ThreadRuntimeCore, "composer">) => {
|
|
44
|
-
const composer = new DefaultThreadComposerRuntimeCore(runtime);
|
|
45
|
-
composer.setText("hello");
|
|
46
|
-
await composer.send();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
describe("DefaultThreadComposerRuntimeCore interactables snapshot gating", () => {
|
|
50
|
-
it("stamps a baseline on the first send of the thread", async () => {
|
|
51
|
-
const { runtime, append } = makeRuntime({
|
|
52
|
-
composerMetadata: live({ v: 1 }),
|
|
53
|
-
});
|
|
54
|
-
await send(runtime);
|
|
55
|
-
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
56
|
-
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
57
|
-
{ id: "n1", name: "note", state: { v: 1 } },
|
|
58
|
-
]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("stamps nothing when the history already carries the live state", async () => {
|
|
62
|
-
const { runtime, append } = makeRuntime({
|
|
63
|
-
messages: [snapshotMessage("msg-1", { v: 1 })],
|
|
64
|
-
composerMetadata: live({ v: 1 }),
|
|
65
|
-
});
|
|
66
|
-
await send(runtime);
|
|
67
|
-
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
68
|
-
expect(appended.metadata?.custom?.interactables).toBeUndefined();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("stamps the new state when it diverged from the latest snapshot", async () => {
|
|
72
|
-
const { runtime, append } = makeRuntime({
|
|
73
|
-
messages: [snapshotMessage("msg-1", { v: 1 })],
|
|
74
|
-
composerMetadata: live({ v: 2 }),
|
|
75
|
-
});
|
|
76
|
-
await send(runtime);
|
|
77
|
-
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
78
|
-
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
79
|
-
{ id: "n1", name: "note", state: { v: 2 } },
|
|
80
|
-
]);
|
|
81
|
-
});
|
|
82
|
-
});
|