@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.
Files changed (115) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
  4. package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
  5. package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
  6. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  7. package/dist/adapters/vercelAttachmentAdapter.js +3 -3
  8. package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
  9. package/dist/aiSDKExtras.d.ts +4 -6
  10. package/dist/aiSDKExtras.d.ts.map +1 -1
  11. package/dist/converters/convertMessage.d.ts +34 -24
  12. package/dist/converters/convertMessage.d.ts.map +1 -1
  13. package/dist/converters/convertMessage.js +56 -12
  14. package/dist/converters/convertMessage.js.map +1 -1
  15. package/dist/converters/modelContentEnvelope.d.ts +6 -7
  16. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  17. package/dist/converters/toCreateMessage.d.ts +2 -4
  18. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  19. package/dist/converters/toolOutputConversion.d.ts +20 -22
  20. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  21. package/dist/hooks.d.ts +2 -4
  22. package/dist/hooks.d.ts.map +1 -1
  23. package/dist/index.d.ts +17 -14
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.native.d.ts +15 -12
  26. package/dist/index.native.d.ts.map +1 -0
  27. package/dist/model-context/injectInteractableContext.d.ts +2 -4
  28. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  29. package/dist/model-context/injectQuoteContext.d.ts +1 -3
  30. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  31. package/dist/runtime/AISDKChat.d.ts +2 -4
  32. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  33. package/dist/runtime/AISDKChat.js +1 -1
  34. package/dist/runtime/AISDKChat.js.map +1 -1
  35. package/dist/runtime/AISDKThreads.d.ts +29 -31
  36. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  37. package/dist/runtime/AISDKThreads.js +22 -23
  38. package/dist/runtime/AISDKThreads.js.map +1 -1
  39. package/dist/runtime/sdkIdentity.d.ts +1 -3
  40. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  41. package/dist/runtime/sdkIdentity.js +1 -1
  42. package/dist/runtime/useAISDKRuntime.d.ts +76 -70
  43. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  44. package/dist/runtime/useAISDKRuntime.js +311 -77
  45. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  46. package/dist/runtime/useChatRuntime.d.ts +6 -8
  47. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  48. package/dist/runtime/useChatRuntime.js +14 -10
  49. package/dist/runtime/useChatRuntime.js.map +1 -1
  50. package/dist/runtime/useChatThread.d.ts +94 -89
  51. package/dist/runtime/useChatThread.d.ts.map +1 -1
  52. package/dist/runtime/useChatThread.js +44 -15
  53. package/dist/runtime/useChatThread.js.map +1 -1
  54. package/dist/runtime/useExternalHistory.d.ts +7 -7
  55. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  56. package/dist/runtime/useExternalHistory.js +231 -60
  57. package/dist/runtime/useExternalHistory.js.map +1 -1
  58. package/dist/runtime/useResourceCleanup.d.ts +1 -3
  59. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  60. package/dist/runtime/useResourceCleanup.js +38 -14
  61. package/dist/runtime/useResourceCleanup.js.map +1 -1
  62. package/dist/runtime/useStreamingTiming.d.ts +3 -5
  63. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  64. package/dist/runtime/useStreamingTiming.js +1 -1
  65. package/dist/tools/frontendTools.d.ts +21 -23
  66. package/dist/tools/frontendTools.d.ts.map +1 -1
  67. package/dist/tools/generativeTools.d.ts +24 -26
  68. package/dist/tools/generativeTools.d.ts.map +1 -1
  69. package/dist/tools/mcp-stdio.node.d.ts +2 -2
  70. package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
  71. package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
  72. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  73. package/dist/transport/AssistantChatTransport.d.ts +14 -16
  74. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  75. package/dist/transport/AssistantChatTransport.js +9 -2
  76. package/dist/transport/AssistantChatTransport.js.map +1 -1
  77. package/dist/transport/resumable.d.ts +25 -27
  78. package/dist/transport/resumable.d.ts.map +1 -1
  79. package/dist/usage.d.ts +8 -10
  80. package/dist/usage.d.ts.map +1 -1
  81. package/dist/usage.js.map +1 -1
  82. package/dist/utils/getVercelAIMessages.d.ts +2 -4
  83. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  84. package/dist/utils/sliceMessagesUntil.d.ts +1 -3
  85. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  86. package/package.json +13 -9
  87. package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
  88. package/src/adapters/vercelAttachmentAdapter.ts +2 -2
  89. package/src/converters/convertMessage.test.ts +541 -3
  90. package/src/converters/convertMessage.ts +123 -15
  91. package/src/index.native.ts +1 -1
  92. package/src/index.ts +1 -1
  93. package/src/runtime/AISDKChat.ts +0 -4
  94. package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
  95. package/src/runtime/AISDKThreads.test.ts +105 -46
  96. package/src/runtime/AISDKThreads.ts +1 -1
  97. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
  98. package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
  99. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
  100. package/src/runtime/useAISDKRuntime.test.ts +275 -5
  101. package/src/runtime/useAISDKRuntime.ts +533 -60
  102. package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
  103. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
  104. package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
  105. package/src/runtime/useChatRuntime.test.ts +11 -7
  106. package/src/runtime/useChatRuntime.ts +6 -2
  107. package/src/runtime/useChatThread.test.ts +166 -2
  108. package/src/runtime/useChatThread.transport.test.tsx +152 -0
  109. package/src/runtime/useChatThread.ts +89 -22
  110. package/src/runtime/useExternalHistory.test.ts +943 -3
  111. package/src/runtime/useExternalHistory.ts +378 -15
  112. package/src/runtime/useResourceCleanup.ts +41 -15
  113. package/src/transport/AssistantChatTransport.test.ts +164 -0
  114. package/src/transport/AssistantChatTransport.ts +22 -2
  115. 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
- await act(async () => client().threadListItem.delete());
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
- it("keeps streaming while hidden and aborts when the hidden host unmounts", async () => {
115
- const { transport, getCancelCount } = createCancellableTransport();
116
- const { Probe, send, isRunning } = createStreamHarness();
117
-
118
- let setMode: ((mode: "visible" | "hidden") => void) | undefined;
119
- const Shell = () => {
120
- const [mode, set] = useState<"visible" | "hidden">("visible");
121
- setMode = set;
122
- return (
123
- <Activity mode={mode}>
124
- <StreamingApp transport={transport} probe={<Probe />} />
125
- </Activity>
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
- const view = render(
130
- <StrictMode>
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
- await act(async () => setMode?.("hidden"));
139
- await act(nextTask);
140
- expect(getCancelCount()).toBe(0);
141
- expect(isRunning()).toBe(true);
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
- await act(async () => setMode?.("hidden"));
149
- view.unmount();
150
- await waitFor(() => expect(getCancelCount()).toBe(1));
151
- });
155
+ await act(async () => setMode?.("visible"));
156
+ await act(nextTask);
157
+ expect(getCancelCount()).toBe(0);
158
+ expect(isRunning()).toBe(true);
152
159
 
153
- it("aborts a nested runtime's stream when the provider above it unmounts", async () => {
154
- const outer = createCancellableTransport();
155
- const { transport, getCancelCount } = createCancellableTransport();
156
- let nested: AssistantRuntime | undefined;
160
+ await act(async () => setMode?.("hidden"));
161
+ view.unmount();
162
+ await waitFor(() => expect(getCancelCount()).toBe(1));
163
+ },
164
+ );
157
165
 
158
- // allowNesting: the inner useChatRuntime runs its thread hook directly, as
159
- // a plain React hook under the provider rather than inside a tap resource.
160
- const NestedChat = () => {
161
- nested = useChatRuntime({ transport });
162
- return null;
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
- const view = render(
166
- <StrictMode>
167
- <AuiProvider
168
- config={AuiConfig({
169
- threads: AISDKChat({ transport: outer.transport }),
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
- await waitFor(() => expect(nested).toBeDefined());
178
- await act(async () => {
179
- await nested!.thread.append("keep streaming");
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
- view.unmount();
184
- await waitFor(() => expect(getCancelCount()).toBe(1));
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
- useChat: mocks.useChat,
56
- Chat: class MockChat {
57
- constructor(config: unknown) {
58
- Object.assign(this, config);
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
- describe("useChatRuntime", () => {
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?: ChatThreadOptions<UI_MESSAGE>,
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,