@assistant-ui/ai-sdk 0.0.7 → 0.0.9
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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +56 -12
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +76 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +311 -77
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +44 -15
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +231 -60
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.js +9 -2
- package/dist/transport/AssistantChatTransport.js.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +541 -3
- package/src/converters/convertMessage.ts +123 -15
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.ts +0 -4
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +105 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
- package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +533 -60
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
- package/src/runtime/useChatRuntime.test.ts +11 -7
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.test.ts +166 -2
- package/src/runtime/useChatThread.transport.test.tsx +152 -0
- package/src/runtime/useChatThread.ts +89 -22
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +378 -15
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/transport/AssistantChatTransport.test.ts +164 -0
- package/src/transport/AssistantChatTransport.ts +22 -2
- package/src/usage.ts +1 -1
|
@@ -36,7 +36,9 @@ const createChatHelpers = (initialMessages: UIMessage[] = []) => {
|
|
|
36
36
|
return chatHelpers;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const createVoiceAdapter = (
|
|
39
|
+
const createVoiceAdapter = ({
|
|
40
|
+
sendText,
|
|
41
|
+
}: { sendText?: RealtimeVoiceAdapter.Session["sendText"] } = {}) => {
|
|
40
42
|
let transcriptCallback:
|
|
41
43
|
| ((transcript: RealtimeVoiceAdapter.TranscriptItem) => void)
|
|
42
44
|
| undefined;
|
|
@@ -46,6 +48,7 @@ const createVoiceAdapter = () => {
|
|
|
46
48
|
disconnect: vi.fn(),
|
|
47
49
|
mute: vi.fn(),
|
|
48
50
|
unmute: vi.fn(),
|
|
51
|
+
...(sendText && { sendText }),
|
|
49
52
|
onStatusChange: () => () => {},
|
|
50
53
|
onTranscript: (callback) => {
|
|
51
54
|
transcriptCallback = callback;
|
|
@@ -267,4 +270,50 @@ describe("useAISDKRuntime voice transcripts", () => {
|
|
|
267
270
|
message: transcript,
|
|
268
271
|
});
|
|
269
272
|
});
|
|
273
|
+
|
|
274
|
+
it("persists a message typed into the session as a typed turn", async () => {
|
|
275
|
+
const chat = createChatHelpers();
|
|
276
|
+
const sendText = vi.fn(async (_text: string) => {});
|
|
277
|
+
const voice = createVoiceAdapter({ sendText });
|
|
278
|
+
const history = createHistoryAdapter();
|
|
279
|
+
const { result, rerender } = await renderVoiceRuntime(chat, {
|
|
280
|
+
voice: voice.adapter,
|
|
281
|
+
history: history.adapter,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
act(() => {
|
|
285
|
+
result.current.thread.connectVoice();
|
|
286
|
+
});
|
|
287
|
+
await act(async () => {
|
|
288
|
+
await result.current.thread.append({
|
|
289
|
+
role: "user",
|
|
290
|
+
content: [{ type: "text", text: "Typed" }],
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(sendText).toHaveBeenCalledExactlyOnceWith("Typed");
|
|
295
|
+
expect(chat.sendMessage).not.toHaveBeenCalled();
|
|
296
|
+
const typed = chat.messages[0];
|
|
297
|
+
expect(typed).toEqual({
|
|
298
|
+
id: expect.any(String),
|
|
299
|
+
role: "user",
|
|
300
|
+
parts: [{ type: "text", text: "Typed" }],
|
|
301
|
+
metadata: {},
|
|
302
|
+
});
|
|
303
|
+
rerender();
|
|
304
|
+
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
const messages = result.current.thread
|
|
307
|
+
.getState()
|
|
308
|
+
.messages.filter((message) => message.id === typed.id);
|
|
309
|
+
expect(messages).toHaveLength(1);
|
|
310
|
+
expect(messages[0]?.metadata.modality).toBeUndefined();
|
|
311
|
+
expect(textOf(messages[0]!)).toBe("Typed");
|
|
312
|
+
});
|
|
313
|
+
await waitFor(() => expect(history.append).toHaveBeenCalledTimes(1));
|
|
314
|
+
expect(history.append).toHaveBeenCalledWith({
|
|
315
|
+
parentId: null,
|
|
316
|
+
message: typed,
|
|
317
|
+
});
|
|
318
|
+
});
|
|
270
319
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, type ReactNode } from "react";
|
|
4
|
+
import { afterAll, afterEach, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
type Family = { current: unknown };
|
|
7
|
+
type RendererInternals = {
|
|
8
|
+
setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
|
|
9
|
+
scheduleRefresh: (
|
|
10
|
+
root: unknown,
|
|
11
|
+
update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
|
|
12
|
+
) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Fast Refresh drives React through the renderer internals handed to the DevTools hook, so the hook has to exist before react-dom loads.
|
|
16
|
+
let renderer: RendererInternals | undefined;
|
|
17
|
+
const fiberRoots = new Set<unknown>();
|
|
18
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
19
|
+
vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
|
20
|
+
supportsFiber: true,
|
|
21
|
+
inject: (internals: RendererInternals) => {
|
|
22
|
+
renderer = internals;
|
|
23
|
+
return 1;
|
|
24
|
+
},
|
|
25
|
+
onScheduleFiberRoot: () => {},
|
|
26
|
+
onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
|
|
27
|
+
onCommitFiberUnmount: () => {},
|
|
28
|
+
});
|
|
29
|
+
const { cleanup, render, waitFor } = await import("@testing-library/react");
|
|
30
|
+
const { AssistantRuntimeProvider } = await import("@assistant-ui/core/react");
|
|
31
|
+
const { useChatRuntime } = await import("./useChatRuntime");
|
|
32
|
+
const { createCancellableTransport, createStreamHarness } =
|
|
33
|
+
await import("./__tests__/controlled-transport");
|
|
34
|
+
|
|
35
|
+
afterEach(cleanup);
|
|
36
|
+
afterAll(() => vi.unstubAllGlobals());
|
|
37
|
+
|
|
38
|
+
it("keeps a running thread across a Fast Refresh of its host and aborts it on unmount", async () => {
|
|
39
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
40
|
+
const { Probe, send, isRunning, client } = createStreamHarness();
|
|
41
|
+
const runtimes: unknown[] = [];
|
|
42
|
+
let rendered: string | undefined;
|
|
43
|
+
const createHost =
|
|
44
|
+
(name: string) =>
|
|
45
|
+
({ children }: { children: ReactNode }) => {
|
|
46
|
+
rendered = name;
|
|
47
|
+
const runtime = useChatRuntime({ transport });
|
|
48
|
+
runtimes.push(runtime);
|
|
49
|
+
return (
|
|
50
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
51
|
+
{children}
|
|
52
|
+
</AssistantRuntimeProvider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
const Before = createHost("before");
|
|
56
|
+
const After = createHost("after");
|
|
57
|
+
|
|
58
|
+
const view = render(
|
|
59
|
+
<Before>
|
|
60
|
+
<Probe />
|
|
61
|
+
</Before>,
|
|
62
|
+
);
|
|
63
|
+
await act(async () => send());
|
|
64
|
+
await waitFor(() => expect(isRunning()).toBe(true));
|
|
65
|
+
const runtime = runtimes.at(-1);
|
|
66
|
+
const threadId = client().threads.getState().mainThreadId;
|
|
67
|
+
const messages = client().thread.getState().messages;
|
|
68
|
+
const error = vi.spyOn(console, "error");
|
|
69
|
+
|
|
70
|
+
const family: Family = { current: After };
|
|
71
|
+
renderer!.setRefreshHandler((type) =>
|
|
72
|
+
type === Before || type === After ? family : undefined,
|
|
73
|
+
);
|
|
74
|
+
await act(async () => {
|
|
75
|
+
for (const fiberRoot of fiberRoots) {
|
|
76
|
+
renderer!.scheduleRefresh(fiberRoot, {
|
|
77
|
+
staleFamilies: new Set(),
|
|
78
|
+
updatedFamilies: new Set([family]),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
await act(async () => {});
|
|
83
|
+
|
|
84
|
+
expect(rendered).toBe("after");
|
|
85
|
+
expect(runtimes.at(-1)).toBe(runtime);
|
|
86
|
+
expect(isRunning()).toBe(true);
|
|
87
|
+
expect(getCancelCount()).toBe(0);
|
|
88
|
+
expect(client().threads.getState().mainThreadId).toBe(threadId);
|
|
89
|
+
expect(client().thread.getState().messages).toEqual(messages);
|
|
90
|
+
expect(error).not.toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
view.unmount();
|
|
93
|
+
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
94
|
+
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
+
import { getEventListeners } from "node:events";
|
|
3
4
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
4
5
|
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
5
6
|
import { AuiConfig, AuiProvider, useAuiState } from "@assistant-ui/store";
|
|
7
|
+
import { useAssistantClientDestroySignal } from "@assistant-ui/store/internal";
|
|
6
8
|
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
7
9
|
import { AISDKChat } from "./AISDKChat";
|
|
8
10
|
import type { ChatTransport, UIMessage } from "ai";
|
|
9
|
-
import { Activity, StrictMode, useState, type ReactNode } from "react";
|
|
11
|
+
import { Activity, StrictMode, useState, version, type ReactNode } from "react";
|
|
10
12
|
import { describe, expect, it } from "vitest";
|
|
11
13
|
import { AssistantChatTransport } from "../transport/AssistantChatTransport";
|
|
12
14
|
import {
|
|
@@ -17,6 +19,8 @@ import {
|
|
|
17
19
|
import { useChatRuntime } from "./useChatRuntime";
|
|
18
20
|
import { useThreadTokenUsage } from "../usage";
|
|
19
21
|
|
|
22
|
+
const onReact18 = version.startsWith("18.");
|
|
23
|
+
|
|
20
24
|
const messages: UIMessage[] = [
|
|
21
25
|
{
|
|
22
26
|
id: "initial-user-message",
|
|
@@ -86,7 +90,12 @@ describe("useChatRuntime integration", () => {
|
|
|
86
90
|
await act(async () => send());
|
|
87
91
|
await waitFor(() => expect(isRunning()).toBe(true));
|
|
88
92
|
|
|
89
|
-
|
|
93
|
+
// React 18's act holds the render that settles the delete until its callback returns, so the delete is awaited outside it.
|
|
94
|
+
let deletion: unknown;
|
|
95
|
+
await act(async () => {
|
|
96
|
+
deletion = client().threadListItem.delete();
|
|
97
|
+
});
|
|
98
|
+
await deletion;
|
|
90
99
|
|
|
91
100
|
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
92
101
|
view.unmount();
|
|
@@ -111,78 +120,147 @@ describe("useChatRuntime integration", () => {
|
|
|
111
120
|
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
112
121
|
});
|
|
113
122
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
setMode
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// Activity is React 19 only.
|
|
124
|
+
it.skipIf(onReact18)(
|
|
125
|
+
"keeps streaming while hidden and aborts when the hidden host unmounts",
|
|
126
|
+
async () => {
|
|
127
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
128
|
+
const { Probe, send, isRunning } = createStreamHarness();
|
|
129
|
+
|
|
130
|
+
let setMode: ((mode: "visible" | "hidden") => void) | undefined;
|
|
131
|
+
const Shell = () => {
|
|
132
|
+
const [mode, set] = useState<"visible" | "hidden">("visible");
|
|
133
|
+
setMode = set;
|
|
134
|
+
return (
|
|
135
|
+
<Activity mode={mode}>
|
|
136
|
+
<StreamingApp transport={transport} probe={<Probe />} />
|
|
137
|
+
</Activity>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const view = render(
|
|
142
|
+
<StrictMode>
|
|
143
|
+
<Shell />
|
|
144
|
+
</StrictMode>,
|
|
126
145
|
);
|
|
127
|
-
};
|
|
128
146
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<Shell />
|
|
132
|
-
</StrictMode>,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
await act(async () => send());
|
|
136
|
-
await waitFor(() => expect(isRunning()).toBe(true));
|
|
147
|
+
await act(async () => send());
|
|
148
|
+
await waitFor(() => expect(isRunning()).toBe(true));
|
|
137
149
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await act(async () => setMode?.("visible"));
|
|
144
|
-
await act(nextTask);
|
|
145
|
-
expect(getCancelCount()).toBe(0);
|
|
146
|
-
expect(isRunning()).toBe(true);
|
|
150
|
+
await act(async () => setMode?.("hidden"));
|
|
151
|
+
await act(nextTask);
|
|
152
|
+
expect(getCancelCount()).toBe(0);
|
|
153
|
+
expect(isRunning()).toBe(true);
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
await act(async () => setMode?.("visible"));
|
|
156
|
+
await act(nextTask);
|
|
157
|
+
expect(getCancelCount()).toBe(0);
|
|
158
|
+
expect(isRunning()).toBe(true);
|
|
152
159
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
await act(async () => setMode?.("hidden"));
|
|
161
|
+
view.unmount();
|
|
162
|
+
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
163
|
+
},
|
|
164
|
+
);
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// Fails on React 18: TypeError: ReactRuntime.use is not a function. Shipped React 18 incompatibility.
|
|
167
|
+
it.skipIf(onReact18)(
|
|
168
|
+
"aborts a nested runtime's stream when the provider above it unmounts",
|
|
169
|
+
async () => {
|
|
170
|
+
const outer = createCancellableTransport();
|
|
171
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
172
|
+
let nested: AssistantRuntime | undefined;
|
|
173
|
+
|
|
174
|
+
// allowNesting: the inner useChatRuntime runs its thread hook directly, as
|
|
175
|
+
// a plain React hook under the provider rather than inside a tap resource.
|
|
176
|
+
const NestedChat = () => {
|
|
177
|
+
nested = useChatRuntime({ transport });
|
|
178
|
+
return null;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const view = render(
|
|
182
|
+
<StrictMode>
|
|
183
|
+
<AuiProvider
|
|
184
|
+
config={AuiConfig({
|
|
185
|
+
threads: AISDKChat({ transport: outer.transport }),
|
|
186
|
+
})}
|
|
187
|
+
>
|
|
188
|
+
<NestedChat />
|
|
189
|
+
</AuiProvider>
|
|
190
|
+
</StrictMode>,
|
|
191
|
+
);
|
|
164
192
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<NestedChat />
|
|
173
|
-
</AuiProvider>
|
|
174
|
-
</StrictMode>,
|
|
175
|
-
);
|
|
193
|
+
await waitFor(() => expect(nested).toBeDefined());
|
|
194
|
+
await act(async () => {
|
|
195
|
+
await nested!.thread.append("keep streaming");
|
|
196
|
+
});
|
|
197
|
+
await waitFor(() =>
|
|
198
|
+
expect(nested!.thread.getState().isRunning).toBe(true),
|
|
199
|
+
);
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await waitFor(() => expect(nested!.thread.getState().isRunning).toBe(true));
|
|
201
|
+
view.unmount();
|
|
202
|
+
await waitFor(() => expect(getCancelCount()).toBe(1));
|
|
203
|
+
},
|
|
204
|
+
);
|
|
182
205
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
206
|
+
// Fails on React 18: TypeError: ReactRuntime.use is not a function. Shipped React 18 incompatibility.
|
|
207
|
+
it.skipIf(onReact18)(
|
|
208
|
+
"aborts a nested runtime when only its own component unmounts",
|
|
209
|
+
async () => {
|
|
210
|
+
const outer = createCancellableTransport();
|
|
211
|
+
const { transport, getCancelCount } = createCancellableTransport();
|
|
212
|
+
let nested: AssistantRuntime | undefined;
|
|
213
|
+
let providerSignal: AbortSignal | undefined;
|
|
214
|
+
let setVisible: ((visible: boolean) => void) | undefined;
|
|
215
|
+
|
|
216
|
+
const NestedChat = () => {
|
|
217
|
+
providerSignal = useAssistantClientDestroySignal();
|
|
218
|
+
nested = useChatRuntime({ transport });
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
const Shell = () => {
|
|
222
|
+
const [visible, set] = useState(true);
|
|
223
|
+
setVisible = set;
|
|
224
|
+
return (
|
|
225
|
+
<AuiProvider
|
|
226
|
+
config={AuiConfig({
|
|
227
|
+
threads: AISDKChat({ transport: outer.transport }),
|
|
228
|
+
})}
|
|
229
|
+
>
|
|
230
|
+
{visible && <NestedChat />}
|
|
231
|
+
</AuiProvider>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
const listeners = () =>
|
|
235
|
+
getEventListeners(providerSignal!, "abort").length;
|
|
236
|
+
|
|
237
|
+
const view = render(<Shell />);
|
|
238
|
+
await waitFor(() => expect(nested).toBeDefined());
|
|
239
|
+
const mounted = listeners();
|
|
240
|
+
|
|
241
|
+
for (const cycle of [1, 2]) {
|
|
242
|
+
await act(async () => {
|
|
243
|
+
await nested!.thread.append(`stream ${cycle}`);
|
|
244
|
+
});
|
|
245
|
+
await waitFor(() =>
|
|
246
|
+
expect(nested!.thread.getState().isRunning).toBe(true),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await act(async () => setVisible?.(false));
|
|
250
|
+
await waitFor(() => expect(getCancelCount()).toBe(cycle));
|
|
251
|
+
expect(listeners()).toBe(mounted - 1);
|
|
252
|
+
|
|
253
|
+
nested = undefined;
|
|
254
|
+
await act(async () => setVisible?.(true));
|
|
255
|
+
await waitFor(() => expect(nested).toBeDefined());
|
|
256
|
+
expect(listeners()).toBe(mounted);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
view.unmount();
|
|
260
|
+
await act(nextTask);
|
|
261
|
+
expect(getCancelCount()).toBe(2);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
186
264
|
});
|
|
187
265
|
|
|
188
266
|
const StreamingApp = ({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import { version } from "react";
|
|
4
5
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
6
|
|
|
6
7
|
const mocks = vi.hoisted(() => {
|
|
@@ -51,12 +52,12 @@ const mocks = vi.hoisted(() => {
|
|
|
51
52
|
};
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
vi.mock("@ai-sdk/react", () => ({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
vi.mock("@ai-sdk/react", async (importOriginal) => ({
|
|
56
|
+
...(await importOriginal<typeof import("@ai-sdk/react")>()),
|
|
57
|
+
useChat: (...args: unknown[]) => {
|
|
58
|
+
const chat = mocks.useChat(...args);
|
|
59
|
+
if (chat) chat.stop ??= vi.fn(async () => {});
|
|
60
|
+
return chat;
|
|
60
61
|
},
|
|
61
62
|
}));
|
|
62
63
|
|
|
@@ -83,6 +84,8 @@ import {
|
|
|
83
84
|
} from "../transport/resumable";
|
|
84
85
|
import { useChatRuntime } from "./useChatRuntime";
|
|
85
86
|
|
|
87
|
+
const onReact18 = version.startsWith("18.");
|
|
88
|
+
|
|
86
89
|
const sendMessagesOptions = {
|
|
87
90
|
trigger: "submit-message" as const,
|
|
88
91
|
chatId: "thread-id",
|
|
@@ -91,7 +94,8 @@ const sendMessagesOptions = {
|
|
|
91
94
|
abortSignal: undefined,
|
|
92
95
|
};
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
// Fails on React 18: TypeError: ReactRuntime.use is not a function. Shipped React 18 incompatibility.
|
|
98
|
+
describe.skipIf(onReact18)("useChatRuntime", () => {
|
|
95
99
|
beforeEach(() => {
|
|
96
100
|
mocks.state.isLoadingHistory = false;
|
|
97
101
|
mocks.state.threadId = "thread-id";
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useRemoteThreadListRuntime,
|
|
9
9
|
} from "@assistant-ui/core/react";
|
|
10
10
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
11
|
+
import { useHostDestroySignal } from "@assistant-ui/store/internal";
|
|
11
12
|
import { useChatThread, type ChatThreadOptions } from "./useChatThread";
|
|
12
13
|
import { AI_SDK_SDK } from "./sdkIdentity";
|
|
13
14
|
|
|
@@ -18,7 +19,8 @@ export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
21
|
-
options
|
|
22
|
+
options: ChatThreadOptions<UI_MESSAGE> | undefined,
|
|
23
|
+
hostDestroySignal: AbortSignal,
|
|
22
24
|
): AssistantRuntime => {
|
|
23
25
|
const id = useAuiState((s) => s.threadListItem.id);
|
|
24
26
|
const isMainThread = useAuiState(
|
|
@@ -31,6 +33,7 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
31
33
|
getThreadListItem: () =>
|
|
32
34
|
aui.threadListItem.source ? aui.threadListItem : undefined,
|
|
33
35
|
stopOnClientDestroy: true,
|
|
36
|
+
hostDestroySignal,
|
|
34
37
|
});
|
|
35
38
|
};
|
|
36
39
|
|
|
@@ -39,10 +42,11 @@ export const useChatRuntime = <UI_MESSAGE extends UIMessage = UIMessage>({
|
|
|
39
42
|
onThreadIdChange,
|
|
40
43
|
...options
|
|
41
44
|
}: UseChatRuntimeOptions<UI_MESSAGE> = {}): AssistantRuntime => {
|
|
45
|
+
const hostDestroySignal = useHostDestroySignal();
|
|
42
46
|
const cloudAdapter = useCloudThreadListAdapter({ cloud, sdk: AI_SDK_SDK });
|
|
43
47
|
return useRemoteThreadListRuntime({
|
|
44
48
|
runtimeHook: function RuntimeHook() {
|
|
45
|
-
return useChatThreadRuntime(options);
|
|
49
|
+
return useChatThreadRuntime(options, hostDestroySignal);
|
|
46
50
|
},
|
|
47
51
|
adapter: cloudAdapter,
|
|
48
52
|
allowNesting: true,
|