@assistant-ui/ai-sdk 0.0.8 → 0.0.10

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 (51) hide show
  1. package/LICENSE +1 -1
  2. package/dist/converters/convertMessage.d.ts +2 -0
  3. package/dist/converters/convertMessage.d.ts.map +1 -1
  4. package/dist/converters/convertMessage.js +15 -4
  5. package/dist/converters/convertMessage.js.map +1 -1
  6. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  7. package/dist/converters/toCreateMessage.js +2 -1
  8. package/dist/converters/toCreateMessage.js.map +1 -1
  9. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  10. package/dist/runtime/AISDKChat.js.map +1 -1
  11. package/dist/runtime/AISDKThreads.js.map +1 -1
  12. package/dist/runtime/sdkIdentity.js +1 -1
  13. package/dist/runtime/useAISDKRuntime.d.ts +8 -1
  14. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  15. package/dist/runtime/useAISDKRuntime.js +212 -34
  16. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  17. package/dist/runtime/useChatRuntime.js +1 -1
  18. package/dist/runtime/useChatRuntime.js.map +1 -1
  19. package/dist/runtime/useChatThread.d.ts.map +1 -1
  20. package/dist/runtime/useChatThread.js +40 -10
  21. package/dist/runtime/useChatThread.js.map +1 -1
  22. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  23. package/dist/runtime/useExternalHistory.js +2 -1
  24. package/dist/runtime/useExternalHistory.js.map +1 -1
  25. package/dist/runtime/useResourceCleanup.js.map +1 -1
  26. package/dist/runtime/useStreamingTiming.js.map +1 -1
  27. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  28. package/dist/transport/AssistantChatTransport.js +9 -2
  29. package/dist/transport/AssistantChatTransport.js.map +1 -1
  30. package/dist/transport/resumable.js.map +1 -1
  31. package/dist/usage.js.map +1 -1
  32. package/package.json +15 -12
  33. package/src/converters/convertMessage.test.ts +99 -0
  34. package/src/converters/convertMessage.ts +27 -2
  35. package/src/converters/toCreateMessage.test.ts +13 -0
  36. package/src/converters/toCreateMessage.ts +1 -0
  37. package/src/runtime/AISDKChat.ts +0 -4
  38. package/src/runtime/AISDKThreads.test.ts +26 -0
  39. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1588 -19
  40. package/src/runtime/useAISDKRuntime.approval.test.tsx +27 -0
  41. package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
  42. package/src/runtime/useAISDKRuntime.ts +397 -28
  43. package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
  44. package/src/runtime/useChatRuntime.integration.test.tsx +134 -112
  45. package/src/runtime/useChatRuntime.test.ts +7 -7
  46. package/src/runtime/useChatThread.test.ts +166 -2
  47. package/src/runtime/useChatThread.transport.test.tsx +5 -2
  48. package/src/runtime/useChatThread.ts +56 -17
  49. package/src/runtime/useExternalHistory.ts +12 -1
  50. package/src/transport/AssistantChatTransport.test.ts +164 -0
  51. package/src/transport/AssistantChatTransport.ts +22 -2
@@ -470,6 +470,33 @@ describe("useAISDKRuntime tool approvals", () => {
470
470
  expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
471
471
  });
472
472
 
473
+ it("answers through the host past an earlier approval-requested part without an approval", async () => {
474
+ const onRespondToToolApproval = vi.fn(async () => {});
475
+ const { respond, messages } = setupPendingApproval(onRespondToToolApproval);
476
+ messages.unshift({
477
+ id: "message-0",
478
+ role: "assistant",
479
+ parts: [
480
+ {
481
+ type: "tool-legacy",
482
+ toolCallId: "tool-0",
483
+ state: "approval-requested",
484
+ input: {},
485
+ } as (typeof messages)[number]["parts"][number],
486
+ ],
487
+ });
488
+
489
+ await act(async () => {
490
+ await respond({ approvalId: "approval-1", approved: true });
491
+ });
492
+
493
+ expect(onRespondToToolApproval).toHaveBeenCalledOnce();
494
+ expect(onRespondToToolApproval).toHaveBeenCalledWith(
495
+ expect.objectContaining({ approvalId: "approval-1" }),
496
+ expect.objectContaining({ toolCallId: "tool-1" }),
497
+ );
498
+ });
499
+
473
500
  it("rejects an approval that is not waiting for a response", async () => {
474
501
  const onRespondToToolApproval = vi.fn();
475
502
  const { respond } = setupPendingApproval(onRespondToToolApproval);
@@ -0,0 +1,182 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { Activity, act, version } from "react";
4
+ import type { UIMessage } from "ai";
5
+ import type { SuggestionAdapter } from "@assistant-ui/core";
6
+ import { afterAll, afterEach, expect, it, vi } from "vitest";
7
+
8
+ const onReact18 = version.startsWith("18.");
9
+
10
+ type Family = { current: unknown };
11
+ type RendererInternals = {
12
+ setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
13
+ scheduleRefresh: (
14
+ root: unknown,
15
+ update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
16
+ ) => void;
17
+ };
18
+
19
+ let renderer: RendererInternals | undefined;
20
+ const fiberRoots = new Set<unknown>();
21
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
22
+ vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
23
+ supportsFiber: true,
24
+ inject: (internals: RendererInternals) => {
25
+ renderer = internals;
26
+ return 1;
27
+ },
28
+ onScheduleFiberRoot: () => {},
29
+ onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
30
+ onCommitFiberUnmount: () => {},
31
+ });
32
+ const { cleanup, render, waitFor } = await import("@testing-library/react");
33
+ const { useAISDKRuntime } = await import("./useAISDKRuntime");
34
+
35
+ afterEach(() => {
36
+ cleanup();
37
+ renderer!.setRefreshHandler(() => undefined);
38
+ fiberRoots.clear();
39
+ });
40
+ afterAll(() => vi.unstubAllGlobals());
41
+
42
+ const refresh = async (Before: unknown, After: unknown) => {
43
+ const family: Family = { current: After };
44
+ renderer!.setRefreshHandler((type) =>
45
+ type === Before || type === After ? family : undefined,
46
+ );
47
+ await act(async () => {
48
+ for (const root of fiberRoots) {
49
+ renderer!.scheduleRefresh(root, {
50
+ staleFamilies: new Set(),
51
+ updatedFamilies: new Set([family]),
52
+ });
53
+ }
54
+ });
55
+ await act(async () => {});
56
+ };
57
+
58
+ const createChat = () => ({
59
+ id: "chat-1",
60
+ status: "submitted",
61
+ error: null,
62
+ messages: [
63
+ { id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
64
+ ] as UIMessage[],
65
+ setMessages: vi.fn(),
66
+ sendMessage: vi.fn(async () => {}),
67
+ regenerate: vi.fn(async () => {}),
68
+ addToolResult: vi.fn(),
69
+ addToolOutput: vi.fn(),
70
+ stop: vi.fn(),
71
+ });
72
+
73
+ const settleChat = (chat: ReturnType<typeof createChat>) => {
74
+ chat.status = "ready";
75
+ chat.messages = [
76
+ ...chat.messages,
77
+ { id: "a1", role: "assistant", parts: [{ type: "text", text: "hello" }] },
78
+ ];
79
+ };
80
+
81
+ const makeGeneration = () => {
82
+ let resolve!: (value: readonly { prompt: string }[]) => void;
83
+ const generate = vi.fn(
84
+ (_input: Parameters<SuggestionAdapter["generate"]>[0]) =>
85
+ new Promise<readonly { prompt: string }[]>((done) => {
86
+ resolve = done;
87
+ }),
88
+ );
89
+ return {
90
+ generate,
91
+ finish: (value: readonly { prompt: string }[]) => resolve(value),
92
+ };
93
+ };
94
+
95
+ it("completes AI SDK suggestion generation after Fast Refresh", async () => {
96
+ const chat = createChat();
97
+ const { generate, finish } = makeGeneration();
98
+ let runtime!: ReturnType<typeof useAISDKRuntime>;
99
+ let rendered = "";
100
+ const Before = () => {
101
+ rendered = "before";
102
+ runtime = useAISDKRuntime(chat as never, {
103
+ adapters: { suggestion: { generate } },
104
+ });
105
+ return null;
106
+ };
107
+ const After = () => {
108
+ rendered = "after";
109
+ runtime = useAISDKRuntime(chat as never, {
110
+ adapters: { suggestion: { generate } },
111
+ });
112
+ return null;
113
+ };
114
+ const view = render(<Before />);
115
+ settleChat(chat);
116
+ view.rerender(<Before />);
117
+ await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
118
+ const signal = generate.mock.calls[0]![0].signal!;
119
+
120
+ await refresh(Before, After);
121
+ expect(rendered).toBe("after");
122
+ expect(signal.aborted).toBe(false);
123
+ await act(async () => finish([{ prompt: "next" }]));
124
+ await waitFor(() =>
125
+ expect(runtime.thread.getState().suggestions).toEqual([{ prompt: "next" }]),
126
+ );
127
+ view.unmount();
128
+ });
129
+
130
+ it("aborts pending AI SDK suggestion generation on unmount", async () => {
131
+ const chat = createChat();
132
+ const { generate } = makeGeneration();
133
+ const Host = () => {
134
+ useAISDKRuntime(chat as never, { adapters: { suggestion: { generate } } });
135
+ return null;
136
+ };
137
+ const view = render(<Host />);
138
+ settleChat(chat);
139
+ view.rerender(<Host />);
140
+ await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
141
+ const signal = generate.mock.calls[0]![0].signal!;
142
+
143
+ view.unmount();
144
+ await act(async () => {});
145
+ expect(signal.aborted).toBe(true);
146
+ });
147
+
148
+ // Activity is React 19 only.
149
+ it.skipIf(onReact18)(
150
+ "aborts pending AI SDK suggestion generation when Activity hides",
151
+ async () => {
152
+ const chat = createChat();
153
+ const { generate } = makeGeneration();
154
+ const Host = () => {
155
+ useAISDKRuntime(chat as never, {
156
+ adapters: { suggestion: { generate } },
157
+ });
158
+ return null;
159
+ };
160
+ const view = render(
161
+ <Activity mode="visible">
162
+ <Host />
163
+ </Activity>,
164
+ );
165
+ settleChat(chat);
166
+ view.rerender(
167
+ <Activity mode="visible">
168
+ <Host />
169
+ </Activity>,
170
+ );
171
+ await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
172
+ const signal = generate.mock.calls[0]![0].signal!;
173
+
174
+ view.rerender(
175
+ <Activity mode="hidden">
176
+ <Host />
177
+ </Activity>,
178
+ );
179
+ await act(async () => {});
180
+ expect(signal.aborted).toBe(true);
181
+ },
182
+ );