@assistant-ui/react 0.14.28 → 0.15.0
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/client/ExternalThread.d.ts +11 -1
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +762 -516
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.js +3 -3
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/context/index.d.ts +1 -8
- package/dist/context/index.js +1 -8
- package/dist/index.d.ts +1 -8
- package/dist/index.js +1 -8
- package/dist/legacy-runtime/cloud/auiV0.d.ts +12 -1
- package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
- package/dist/legacy-runtime/cloud/auiV0.js +2 -1
- package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +7 -6
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +30 -15
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +11 -7
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +1 -1
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +26 -25
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +3 -3
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js +3 -3
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +7 -6
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +10 -11
- package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
- package/dist/primitives/attachment/AttachmentRemove.js +4 -3
- package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +4 -3
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.js +11 -5
- package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +2 -4
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +18 -16
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQuote.js +1 -1
- package/dist/primitives/composer/ComposerQuote.js.map +1 -1
- package/dist/primitives/composer/ComposerSend.js +1 -0
- package/dist/primitives/composer/ComposerSend.js.map +1 -1
- package/dist/primitives/composer/ComposerStopDictation.js +4 -3
- package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -3
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +67 -89
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/message/MessageRoot.js +3 -3
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.js +4 -3
- package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.js +4 -3
- package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js +17 -15
- package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.js +1 -0
- package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
- package/dist/unstable/useComposerInput.js +2 -2
- package/dist/unstable/useComposerInput.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.js +6 -6
- package/dist/unstable/useComposerInputHistory.js.map +1 -1
- package/dist/unstable/useMentionAdapter.js +1 -1
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +1 -0
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/hooks/useSizeHandle.js +1 -0
- package/dist/utils/hooks/useSizeHandle.js.map +1 -1
- package/dist/utils/smooth/SmoothContext.js +3 -3
- package/dist/utils/smooth/SmoothContext.js.map +1 -1
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +3 -3
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +9 -9
- package/src/client/ExternalThread.ts +345 -56
- package/src/client/InMemoryThreadList.ts +3 -3
- package/src/context/index.ts +0 -44
- package/src/legacy-runtime/cloud/auiV0.ts +14 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +4 -1
- package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +37 -10
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +79 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +97 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +113 -2
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +10 -2
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +185 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +21 -5
- package/src/mcp-apps/McpAppRenderer.tsx +1 -1
- package/src/model-context/makeAssistantVisible.test.tsx +1 -1
- package/src/model-context/makeAssistantVisible.tsx +1 -1
- package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +1 -1
- package/src/primitives/actionBar/useActionBarFloatStatus.ts +7 -5
- package/src/primitives/attachment/AttachmentRemove.ts +1 -1
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.ts +1 -1
- package/src/primitives/composer/ComposerAddAttachment.test.tsx +108 -0
- package/src/primitives/composer/ComposerAddAttachment.ts +11 -5
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +5 -4
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +4 -3
- package/src/primitives/composer/ComposerInput.test.tsx +104 -7
- package/src/primitives/composer/ComposerInput.tsx +22 -20
- package/src/primitives/composer/ComposerQuote.tsx +1 -1
- package/src/primitives/composer/ComposerStopDictation.ts +1 -1
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +7 -9
- package/src/primitives/message/MessagePartsGrouped.tsx +6 -8
- package/src/primitives/message/MessageRoot.tsx +1 -1
- package/src/primitives/queueItem/QueueItemRemove.ts +1 -1
- package/src/primitives/queueItem/QueueItemSteer.ts +1 -1
- package/src/primitives/selectionToolbar/SelectionToolbarQuote.tsx +1 -1
- package/src/primitives/suggestion/SuggestionTrigger.ts +9 -9
- package/src/tests/DataRenderers.test.tsx +212 -0
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +1 -11
- package/src/tests/RemoteThreadListRuntime.deferredProvider.test.tsx +3 -3
- package/src/tests/external-thread-attachments.test.tsx +300 -11
- package/src/tests/external-thread-branches.test.tsx +10 -10
- package/src/tests/external-thread-parity.test.tsx +230 -0
- package/src/tests/generative-ui.test.tsx +1 -46
- package/src/tests/local-runtime-queue.test.tsx +29 -34
- package/src/tests/messagePartSwitchRace.test.tsx +46 -15
- package/src/tests/messagePartTypeMismatch.test.tsx +2 -2
- package/src/tests/threadMessageById.test.tsx +0 -7
- package/src/unstable/useComposerInput.test.tsx +2 -2
- package/src/unstable/useComposerInput.ts +2 -2
- package/src/unstable/useComposerInputHistory.test.tsx +3 -3
- package/src/unstable/useComposerInputHistory.ts +6 -6
- package/src/unstable/useMentionAdapter.ts +1 -1
- package/src/utils/smooth/SmoothContext.tsx +1 -1
- package/src/utils/smooth/useSmooth.test.tsx +1 -1
- package/src/utils/smooth/useSmooth.ts +8 -5
- package/dist/context/react/utils/createStateHookForRuntime.d.ts +0 -34
- package/dist/context/react/utils/createStateHookForRuntime.d.ts.map +0 -1
- package/dist/context/react/utils/createStateHookForRuntime.js +0 -21
- package/dist/context/react/utils/createStateHookForRuntime.js.map +0 -1
- package/dist/legacy-runtime/hooks/AssistantContext.d.ts +0 -74
- package/dist/legacy-runtime/hooks/AssistantContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/AssistantContext.js +0 -29
- package/dist/legacy-runtime/hooks/AssistantContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/AttachmentContext.d.ts +0 -1007
- package/dist/legacy-runtime/hooks/AttachmentContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/AttachmentContext.js +0 -47
- package/dist/legacy-runtime/hooks/AttachmentContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/ComposerContext.d.ts +0 -141
- package/dist/legacy-runtime/hooks/ComposerContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/ComposerContext.js +0 -65
- package/dist/legacy-runtime/hooks/ComposerContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/MessageContext.d.ts +0 -155
- package/dist/legacy-runtime/hooks/MessageContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/MessageContext.js +0 -68
- package/dist/legacy-runtime/hooks/MessageContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/MessagePartContext.d.ts +0 -44
- package/dist/legacy-runtime/hooks/MessagePartContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/MessagePartContext.js +0 -26
- package/dist/legacy-runtime/hooks/MessagePartContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/ThreadContext.d.ts +0 -139
- package/dist/legacy-runtime/hooks/ThreadContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/ThreadContext.js +0 -84
- package/dist/legacy-runtime/hooks/ThreadContext.js.map +0 -1
- package/dist/legacy-runtime/hooks/ThreadListItemContext.d.ts +0 -44
- package/dist/legacy-runtime/hooks/ThreadListItemContext.d.ts.map +0 -1
- package/dist/legacy-runtime/hooks/ThreadListItemContext.js +0 -26
- package/dist/legacy-runtime/hooks/ThreadListItemContext.js.map +0 -1
- package/src/context/react/utils/createStateHookForRuntime.ts +0 -82
- package/src/legacy-runtime/hooks/AssistantContext.ts +0 -67
- package/src/legacy-runtime/hooks/AttachmentContext.ts +0 -99
- package/src/legacy-runtime/hooks/ComposerContext.ts +0 -131
- package/src/legacy-runtime/hooks/MessageContext.ts +0 -127
- package/src/legacy-runtime/hooks/MessagePartContext.ts +0 -32
- package/src/legacy-runtime/hooks/ThreadContext.ts +0 -120
- package/src/legacy-runtime/hooks/ThreadListItemContext.ts +0 -36
- package/src/tests/BaseComposerRuntimeCore.test.ts +0 -621
- package/src/tests/BaseSubscribable.test.ts +0 -54
- package/src/tests/DataRenderers.test.ts +0 -355
- package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +0 -685
- package/src/tests/LocalThreadRuntimeCore.test.ts +0 -324
- package/src/tests/auiV0Encode.test.ts +0 -188
- package/src/tests/external-message-converter.test.ts +0 -346
- package/src/tests/shouldContinue.test.ts +0 -122
|
@@ -2,10 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
import { act, render, waitFor } from "@testing-library/react";
|
|
4
4
|
import type { FC } from "react";
|
|
5
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { AuiProvider, useAui } from "@assistant-ui/store";
|
|
7
|
+
import type {
|
|
8
|
+
ExternalThreadMessage,
|
|
9
|
+
ExternalThreadProps,
|
|
10
|
+
} from "../client/ExternalThread";
|
|
7
11
|
import { ExternalThread } from "../client/ExternalThread";
|
|
8
12
|
|
|
13
|
+
const renderThreadWithProps = (props: Partial<ExternalThreadProps>) => {
|
|
14
|
+
const captured: { aui?: ReturnType<typeof useAui> } = {};
|
|
15
|
+
const Capture: FC = () => {
|
|
16
|
+
captured.aui = useAui();
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const App: FC = () => {
|
|
20
|
+
const aui = useAui({
|
|
21
|
+
thread: ExternalThread({ messages: [], isRunning: false, ...props }),
|
|
22
|
+
});
|
|
23
|
+
return (
|
|
24
|
+
<AuiProvider value={aui}>
|
|
25
|
+
<Capture />
|
|
26
|
+
</AuiProvider>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
render(<App />);
|
|
31
|
+
return () => captured.aui!;
|
|
32
|
+
};
|
|
33
|
+
|
|
9
34
|
const renderThread = () => {
|
|
10
35
|
const captured: { aui?: ReturnType<typeof useAui> } = {};
|
|
11
36
|
const Capture: FC = () => {
|
|
@@ -49,14 +74,12 @@ describe("ExternalThread attachments", () => {
|
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
await waitFor(() => {
|
|
52
|
-
expect(aui().thread
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
},
|
|
59
|
-
);
|
|
77
|
+
expect(aui().thread.composer().getState().attachments[0]).toMatchObject({
|
|
78
|
+
type: "document",
|
|
79
|
+
name: "notes.txt",
|
|
80
|
+
contentType: "text/plain",
|
|
81
|
+
content: [{ type: "text", text: "hello" }],
|
|
82
|
+
});
|
|
60
83
|
});
|
|
61
84
|
});
|
|
62
85
|
|
|
@@ -69,13 +92,279 @@ describe("ExternalThread attachments", () => {
|
|
|
69
92
|
content: [{ type: "text", text: "implementation detail" }],
|
|
70
93
|
} as File;
|
|
71
94
|
|
|
72
|
-
await act(() => aui().thread
|
|
95
|
+
await act(() => aui().thread.composer().addAttachment(foreignFile));
|
|
73
96
|
|
|
74
|
-
expect(aui().thread
|
|
97
|
+
expect(aui().thread.composer().getState().attachments[0]).toMatchObject({
|
|
75
98
|
type: "file",
|
|
76
99
|
name: "photo.png",
|
|
77
100
|
contentType: "image/png",
|
|
78
101
|
file: foreignFile,
|
|
79
102
|
});
|
|
80
103
|
});
|
|
104
|
+
|
|
105
|
+
it("rejects files that do not match the adapter accept string", async () => {
|
|
106
|
+
const add = vi.fn(async () => ({}) as never);
|
|
107
|
+
const aui = renderThreadWithProps({
|
|
108
|
+
attachmentAdapter: {
|
|
109
|
+
accept: "image/*",
|
|
110
|
+
add,
|
|
111
|
+
send: async () => ({}) as never,
|
|
112
|
+
remove: async () => {},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
aui()
|
|
118
|
+
.thread()
|
|
119
|
+
.composer()
|
|
120
|
+
.addAttachment(
|
|
121
|
+
new File(["data"], "archive.zip", { type: "application/zip" }),
|
|
122
|
+
),
|
|
123
|
+
).rejects.toThrow(
|
|
124
|
+
"File type application/zip is not accepted. Accepted types: image/*",
|
|
125
|
+
);
|
|
126
|
+
expect(add).not.toHaveBeenCalled();
|
|
127
|
+
expect(aui().thread.composer().getState().attachments).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects prepared attachments that do not match the adapter accept string", async () => {
|
|
131
|
+
const aui = renderThreadWithProps({
|
|
132
|
+
attachmentAdapter: {
|
|
133
|
+
accept: ".pdf",
|
|
134
|
+
add: async () => ({}) as never,
|
|
135
|
+
send: async () => ({}) as never,
|
|
136
|
+
remove: async () => {},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await expect(
|
|
141
|
+
aui()
|
|
142
|
+
.thread()
|
|
143
|
+
.composer()
|
|
144
|
+
.addAttachment({
|
|
145
|
+
name: "notes.txt",
|
|
146
|
+
contentType: "text/plain",
|
|
147
|
+
content: [{ type: "text", text: "hello" }],
|
|
148
|
+
}),
|
|
149
|
+
).rejects.toThrow(
|
|
150
|
+
"File type text/plain is not accepted. Accepted types: .pdf",
|
|
151
|
+
);
|
|
152
|
+
expect(aui().thread.composer().getState().attachments).toHaveLength(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("restores the draft when an attachment upload fails on send", async () => {
|
|
156
|
+
const consoleError = vi
|
|
157
|
+
.spyOn(console, "error")
|
|
158
|
+
.mockImplementation(() => {});
|
|
159
|
+
const sent: unknown[] = [];
|
|
160
|
+
let rejectSend!: (reason: unknown) => void;
|
|
161
|
+
const adapter = {
|
|
162
|
+
accept: "*",
|
|
163
|
+
add: async ({ file }: { file: File }) => ({
|
|
164
|
+
id: "att-1",
|
|
165
|
+
type: "file" as const,
|
|
166
|
+
name: file.name,
|
|
167
|
+
contentType: file.type,
|
|
168
|
+
file,
|
|
169
|
+
status: {
|
|
170
|
+
type: "requires-action" as const,
|
|
171
|
+
reason: "composer-send" as const,
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
send: () =>
|
|
175
|
+
new Promise((_resolve, reject) => {
|
|
176
|
+
rejectSend = reject;
|
|
177
|
+
}) as never,
|
|
178
|
+
remove: async () => {},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const aui = renderThreadWithProps({
|
|
182
|
+
attachmentAdapter: adapter,
|
|
183
|
+
onNew: (message) => sent.push(message),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await act(() =>
|
|
187
|
+
aui()
|
|
188
|
+
.thread()
|
|
189
|
+
.composer()
|
|
190
|
+
.addAttachment(new File(["data"], "notes.txt", { type: "text/plain" })),
|
|
191
|
+
);
|
|
192
|
+
await act(async () => {
|
|
193
|
+
aui().thread.composer().setText("hello");
|
|
194
|
+
});
|
|
195
|
+
await act(async () => {
|
|
196
|
+
aui().thread.composer().send();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// The draft is cleared optimistically while the upload runs.
|
|
200
|
+
expect(aui().thread.composer().getState().text).toBe("");
|
|
201
|
+
|
|
202
|
+
await act(async () => {
|
|
203
|
+
rejectSend(new Error("upload failed"));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
const state = aui().thread.composer().getState();
|
|
208
|
+
expect(state.text).toBe("hello");
|
|
209
|
+
expect(state.attachments).toHaveLength(1);
|
|
210
|
+
});
|
|
211
|
+
expect(sent).toHaveLength(0);
|
|
212
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
213
|
+
"Failed to send attachments",
|
|
214
|
+
expect.any(Error),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it.each([
|
|
219
|
+
[
|
|
220
|
+
"clearAttachments",
|
|
221
|
+
(c: { clearAttachments(): Promise<void> }) => c.clearAttachments(),
|
|
222
|
+
],
|
|
223
|
+
["reset", (c: { reset(): Promise<void> }) => c.reset()],
|
|
224
|
+
] as const)(
|
|
225
|
+
"calls adapter.remove for pending attachments on %s",
|
|
226
|
+
async (_name, invoke) => {
|
|
227
|
+
const remove = vi.fn(async () => {});
|
|
228
|
+
const adapter = {
|
|
229
|
+
accept: "*",
|
|
230
|
+
add: async ({ file }: { file: File }) => ({
|
|
231
|
+
id: "att-1",
|
|
232
|
+
type: "file" as const,
|
|
233
|
+
name: file.name,
|
|
234
|
+
contentType: file.type,
|
|
235
|
+
file,
|
|
236
|
+
status: {
|
|
237
|
+
type: "requires-action" as const,
|
|
238
|
+
reason: "composer-send" as const,
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
send: async () => ({}) as never,
|
|
242
|
+
remove,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const aui = renderThreadWithProps({ attachmentAdapter: adapter });
|
|
246
|
+
|
|
247
|
+
await act(() =>
|
|
248
|
+
aui()
|
|
249
|
+
.thread()
|
|
250
|
+
.composer()
|
|
251
|
+
.addAttachment(
|
|
252
|
+
new File(["data"], "notes.txt", { type: "text/plain" }),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
await act(() => invoke(aui().thread.composer()));
|
|
257
|
+
|
|
258
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
259
|
+
expect(remove).toHaveBeenCalledWith(
|
|
260
|
+
expect.objectContaining({ id: "att-1" }),
|
|
261
|
+
);
|
|
262
|
+
expect(aui().thread.composer().getState().attachments).toHaveLength(0);
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
it("routes edit-composer attachments through the adapter", async () => {
|
|
267
|
+
const add = vi.fn(async ({ file }: { file: File }) => ({
|
|
268
|
+
id: "att-edit",
|
|
269
|
+
type: "file" as const,
|
|
270
|
+
name: file.name,
|
|
271
|
+
contentType: file.type,
|
|
272
|
+
file,
|
|
273
|
+
status: {
|
|
274
|
+
type: "requires-action" as const,
|
|
275
|
+
reason: "composer-send" as const,
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
const aui = renderThreadWithProps({
|
|
279
|
+
messages: [
|
|
280
|
+
{
|
|
281
|
+
id: "u1",
|
|
282
|
+
role: "user",
|
|
283
|
+
content: [{ type: "text", text: "hi" }],
|
|
284
|
+
createdAt: new Date(0),
|
|
285
|
+
attachments: [],
|
|
286
|
+
metadata: { custom: {} },
|
|
287
|
+
} as unknown as ExternalThreadMessage,
|
|
288
|
+
],
|
|
289
|
+
onEdit: () => {},
|
|
290
|
+
attachmentAdapter: {
|
|
291
|
+
accept: "*",
|
|
292
|
+
add,
|
|
293
|
+
send: async () => ({}) as never,
|
|
294
|
+
remove: async () => {},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const composer = () => aui().thread.message({ id: "u1" }).composer();
|
|
299
|
+
await act(async () => {
|
|
300
|
+
composer().beginEdit();
|
|
301
|
+
});
|
|
302
|
+
await act(() =>
|
|
303
|
+
composer().addAttachment(
|
|
304
|
+
new File(["data"], "notes.txt", { type: "text/plain" }),
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(add).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(composer().getState().attachments[0]).toMatchObject({
|
|
310
|
+
id: "att-edit",
|
|
311
|
+
name: "notes.txt",
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("merges the failed send into a draft the user already modified", async () => {
|
|
316
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
317
|
+
let rejectSend!: (reason: unknown) => void;
|
|
318
|
+
const adapter = {
|
|
319
|
+
accept: "*",
|
|
320
|
+
add: async ({ file }: { file: File }) => ({
|
|
321
|
+
id: "att-1",
|
|
322
|
+
type: "file" as const,
|
|
323
|
+
name: file.name,
|
|
324
|
+
contentType: file.type,
|
|
325
|
+
file,
|
|
326
|
+
status: {
|
|
327
|
+
type: "requires-action" as const,
|
|
328
|
+
reason: "composer-send" as const,
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
send: () =>
|
|
332
|
+
new Promise((_resolve, reject) => {
|
|
333
|
+
rejectSend = reject;
|
|
334
|
+
}) as never,
|
|
335
|
+
remove: async () => {},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const aui = renderThreadWithProps({
|
|
339
|
+
attachmentAdapter: adapter,
|
|
340
|
+
onNew: () => {},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await act(() =>
|
|
344
|
+
aui()
|
|
345
|
+
.thread()
|
|
346
|
+
.composer()
|
|
347
|
+
.addAttachment(new File(["data"], "notes.txt", { type: "text/plain" })),
|
|
348
|
+
);
|
|
349
|
+
await act(async () => {
|
|
350
|
+
aui().thread.composer().setText("hello");
|
|
351
|
+
aui().thread.composer().send();
|
|
352
|
+
});
|
|
353
|
+
await act(async () => {
|
|
354
|
+
aui().thread.composer().setText("meanwhile");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await act(async () => {
|
|
358
|
+
rejectSend(new Error("upload failed"));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const state = aui().thread.composer().getState();
|
|
362
|
+
expect(state.text).toBe("hello\nmeanwhile");
|
|
363
|
+
expect(state.attachments).toHaveLength(1);
|
|
364
|
+
expect(state.attachments[0]).toMatchObject({ id: "att-1" });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(() => {
|
|
368
|
+
vi.restoreAllMocks();
|
|
369
|
+
});
|
|
81
370
|
});
|
|
@@ -66,18 +66,18 @@ const adapterFor = (
|
|
|
66
66
|
describe("ExternalThread branches", () => {
|
|
67
67
|
it("defaults to single-branch state without an adapter", () => {
|
|
68
68
|
const { aui } = renderThread(baseProps());
|
|
69
|
-
const state = aui().thread
|
|
69
|
+
const state = aui().thread.getState();
|
|
70
70
|
expect(state.messages[1]!.branchNumber).toBe(1);
|
|
71
71
|
expect(state.messages[1]!.branchCount).toBe(1);
|
|
72
72
|
expect(state.capabilities.switchToBranch).toBe(false);
|
|
73
73
|
expect(() =>
|
|
74
|
-
aui().thread
|
|
74
|
+
aui().thread.message({ index: 1 }).switchToBranch({ position: "next" }),
|
|
75
75
|
).not.toThrow();
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it("derives branchNumber and branchCount from the adapter", () => {
|
|
79
79
|
const { aui } = renderThread(baseProps(adapterFor(["a1", "a2", "a3"])));
|
|
80
|
-
const state = aui().thread
|
|
80
|
+
const state = aui().thread.getState();
|
|
81
81
|
expect(state.messages[1]!.branchNumber).toBe(2);
|
|
82
82
|
expect(state.messages[1]!.branchCount).toBe(3);
|
|
83
83
|
expect(state.capabilities.switchToBranch).toBe(true);
|
|
@@ -88,7 +88,7 @@ describe("ExternalThread branches", () => {
|
|
|
88
88
|
const { aui } = renderThread(
|
|
89
89
|
baseProps(adapterFor(["a1", "a2", "a3"], switchToBranch)),
|
|
90
90
|
);
|
|
91
|
-
const msg = () => aui().thread
|
|
91
|
+
const msg = () => aui().thread.message({ index: 1 });
|
|
92
92
|
|
|
93
93
|
msg().switchToBranch({ position: "previous" });
|
|
94
94
|
expect(switchToBranch).toHaveBeenLastCalledWith("a1");
|
|
@@ -101,7 +101,7 @@ describe("ExternalThread branches", () => {
|
|
|
101
101
|
isRunning: false,
|
|
102
102
|
branches: adapterFor(["a1", "a2"], switchToBranch),
|
|
103
103
|
});
|
|
104
|
-
edge.aui().thread
|
|
104
|
+
edge.aui().thread.message({ index: 1 }).switchToBranch({
|
|
105
105
|
position: "previous",
|
|
106
106
|
});
|
|
107
107
|
expect(switchToBranch).not.toHaveBeenCalled();
|
|
@@ -112,7 +112,7 @@ describe("ExternalThread branches", () => {
|
|
|
112
112
|
const { aui } = renderThread(
|
|
113
113
|
baseProps(adapterFor(["a1", "a2"], switchToBranch)),
|
|
114
114
|
);
|
|
115
|
-
const msg = () => aui().thread
|
|
115
|
+
const msg = () => aui().thread.message({ index: 1 });
|
|
116
116
|
|
|
117
117
|
msg().switchToBranch({ branchId: "not-in-the-list" });
|
|
118
118
|
expect(switchToBranch).toHaveBeenLastCalledWith("not-in-the-list");
|
|
@@ -130,11 +130,11 @@ describe("ExternalThread branches", () => {
|
|
|
130
130
|
switchToBranch,
|
|
131
131
|
}),
|
|
132
132
|
);
|
|
133
|
-
const state = aui().thread
|
|
133
|
+
const state = aui().thread.getState();
|
|
134
134
|
expect(state.messages[1]!.branchNumber).toBe(1);
|
|
135
135
|
expect(state.messages[1]!.branchCount).toBe(1);
|
|
136
136
|
|
|
137
|
-
aui().thread
|
|
137
|
+
aui().thread.message({ index: 1 }).switchToBranch({ position: "next" });
|
|
138
138
|
expect(switchToBranch).not.toHaveBeenCalled();
|
|
139
139
|
});
|
|
140
140
|
|
|
@@ -145,14 +145,14 @@ describe("ExternalThread branches", () => {
|
|
|
145
145
|
isRunning: false,
|
|
146
146
|
branches: adapterFor(["a1", "a2", "a3"]),
|
|
147
147
|
});
|
|
148
|
-
const before = aui().thread
|
|
148
|
+
const before = aui().thread.getState().messages[1];
|
|
149
149
|
|
|
150
150
|
rerender({
|
|
151
151
|
messages,
|
|
152
152
|
isRunning: false,
|
|
153
153
|
branches: adapterFor(["a1", "a2", "a3"]),
|
|
154
154
|
});
|
|
155
|
-
const after = aui().thread
|
|
155
|
+
const after = aui().thread.getState().messages[1];
|
|
156
156
|
|
|
157
157
|
expect(after!.branchNumber).toBe(2);
|
|
158
158
|
expect(before!.parts[0]).toBe(after!.parts[0]);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
// Legacy-runtime parity: part statuses derive from the message status, and
|
|
4
|
+
// imperative composer call sequences observe writes before React re-renders.
|
|
5
|
+
|
|
6
|
+
import { render, waitFor } from "@testing-library/react";
|
|
7
|
+
import type { FC } from "react";
|
|
8
|
+
import { describe, it, expect, vi } from "vitest";
|
|
9
|
+
import { useAui, AuiProvider } from "@assistant-ui/store";
|
|
10
|
+
import type { ThreadMessage } from "@assistant-ui/core";
|
|
11
|
+
import {
|
|
12
|
+
ExternalThread,
|
|
13
|
+
type ExternalThreadProps,
|
|
14
|
+
type ExternalThreadMessage,
|
|
15
|
+
} from "../client/ExternalThread";
|
|
16
|
+
|
|
17
|
+
const renderThread = (props: ExternalThreadProps) => {
|
|
18
|
+
const captured: { aui?: ReturnType<typeof useAui> } = {};
|
|
19
|
+
const Capture: FC = () => {
|
|
20
|
+
captured.aui = useAui();
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
|
|
24
|
+
const aui = useAui({ thread: ExternalThread(threadProps) });
|
|
25
|
+
return (
|
|
26
|
+
<AuiProvider value={aui}>
|
|
27
|
+
<Capture />
|
|
28
|
+
</AuiProvider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
const utils = render(<App threadProps={props} />);
|
|
32
|
+
return {
|
|
33
|
+
aui: () => captured.aui!,
|
|
34
|
+
rerender: (next: ExternalThreadProps) =>
|
|
35
|
+
utils.rerender(<App threadProps={next} />),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assistantMessage = (
|
|
40
|
+
status: ThreadMessage["status"],
|
|
41
|
+
result?: string,
|
|
42
|
+
): ExternalThreadMessage =>
|
|
43
|
+
({
|
|
44
|
+
id: "a1",
|
|
45
|
+
role: "assistant",
|
|
46
|
+
content: [
|
|
47
|
+
{ type: "text", text: "let me check" },
|
|
48
|
+
{
|
|
49
|
+
type: "tool-call",
|
|
50
|
+
toolCallId: "tc1",
|
|
51
|
+
toolName: "probe_tool",
|
|
52
|
+
args: {},
|
|
53
|
+
argsText: "{}",
|
|
54
|
+
...(result !== undefined && { result }),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
createdAt: new Date(0),
|
|
58
|
+
status,
|
|
59
|
+
metadata: { custom: {} },
|
|
60
|
+
}) as unknown as ExternalThreadMessage;
|
|
61
|
+
|
|
62
|
+
describe("ExternalThread part status", () => {
|
|
63
|
+
it("gives an unresolved tool call its message's status", () => {
|
|
64
|
+
const { aui } = renderThread({
|
|
65
|
+
messages: [assistantMessage({ type: "running" })],
|
|
66
|
+
isRunning: true,
|
|
67
|
+
});
|
|
68
|
+
const part = (toolCallId: string) =>
|
|
69
|
+
aui().thread.message({ id: "a1" }).part({ toolCallId }).getState();
|
|
70
|
+
expect(part("tc1").status).toEqual({ type: "running" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("marks a resolved tool call complete and streams only the last part", () => {
|
|
74
|
+
const { aui } = renderThread({
|
|
75
|
+
messages: [assistantMessage({ type: "running" }, "ok")],
|
|
76
|
+
isRunning: true,
|
|
77
|
+
});
|
|
78
|
+
const state = aui().thread.message({ id: "a1" }).getState();
|
|
79
|
+
expect(state.parts[0]!.status).toEqual({ type: "complete" });
|
|
80
|
+
expect(state.parts[1]!.status).toEqual({ type: "complete" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("keeps parts complete on requires-action and user messages", () => {
|
|
84
|
+
const { aui } = renderThread({
|
|
85
|
+
messages: [
|
|
86
|
+
{
|
|
87
|
+
id: "u1",
|
|
88
|
+
role: "user",
|
|
89
|
+
content: [{ type: "text", text: "hi" }],
|
|
90
|
+
createdAt: new Date(0),
|
|
91
|
+
attachments: [],
|
|
92
|
+
metadata: { custom: {} },
|
|
93
|
+
} as unknown as ExternalThreadMessage,
|
|
94
|
+
assistantMessage({ type: "requires-action", reason: "tool-calls" }),
|
|
95
|
+
],
|
|
96
|
+
isRunning: false,
|
|
97
|
+
});
|
|
98
|
+
expect(
|
|
99
|
+
aui().thread.message({ id: "u1" }).part({ index: 0 }).getState().status,
|
|
100
|
+
).toEqual({ type: "complete" });
|
|
101
|
+
expect(
|
|
102
|
+
aui()
|
|
103
|
+
.thread()
|
|
104
|
+
.message({ id: "a1" })
|
|
105
|
+
.part({ toolCallId: "tc1" })
|
|
106
|
+
.getState().status,
|
|
107
|
+
).toEqual({ type: "requires-action", reason: "tool-calls" });
|
|
108
|
+
expect(
|
|
109
|
+
aui().thread.message({ id: "a1" }).part({ index: 0 }).getState().status,
|
|
110
|
+
).toEqual({ type: "complete" });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("ExternalThread unset optional callbacks", () => {
|
|
115
|
+
it("throws a capability error when the callback prop is not set", () => {
|
|
116
|
+
const { aui } = renderThread({
|
|
117
|
+
messages: [
|
|
118
|
+
assistantMessage({ type: "requires-action", reason: "tool-calls" }),
|
|
119
|
+
],
|
|
120
|
+
isRunning: false,
|
|
121
|
+
});
|
|
122
|
+
const part = () =>
|
|
123
|
+
aui().thread.message({ id: "a1" }).part({ toolCallId: "tc1" });
|
|
124
|
+
|
|
125
|
+
expect(() => part().addToolResult("ok")).toThrow(
|
|
126
|
+
"Runtime does not support tool results (onAddToolResult is not set).",
|
|
127
|
+
);
|
|
128
|
+
expect(() => part().resumeToolCall(undefined)).toThrow(
|
|
129
|
+
"Runtime does not support resuming tool calls (onResumeToolCall is not set).",
|
|
130
|
+
);
|
|
131
|
+
expect(() => aui().thread.resumeRun()).toThrow(
|
|
132
|
+
"Runtime does not support resuming runs (onResume is not set).",
|
|
133
|
+
);
|
|
134
|
+
expect(() => aui().thread.importExternalState({})).toThrow(
|
|
135
|
+
"Runtime does not support importing external states (onLoadExternalState is not set).",
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("ExternalThread composer", () => {
|
|
141
|
+
it("dispatches a synchronous setText + send sequence", async () => {
|
|
142
|
+
const onNew = vi.fn();
|
|
143
|
+
const { aui } = renderThread({ messages: [], isRunning: false, onNew });
|
|
144
|
+
|
|
145
|
+
aui().thread.composer().setText("hello");
|
|
146
|
+
aui().thread.composer().send();
|
|
147
|
+
|
|
148
|
+
await waitFor(() => expect(onNew).toHaveBeenCalledTimes(1));
|
|
149
|
+
expect(onNew.mock.calls[0]![0].content).toEqual([
|
|
150
|
+
{ type: "text", text: "hello" },
|
|
151
|
+
]);
|
|
152
|
+
await waitFor(() => expect(aui().thread.getState().composer.text).toBe(""));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("stamps the thread head as parentId on queue-adapter sends", async () => {
|
|
156
|
+
const enqueue = vi.fn();
|
|
157
|
+
const { aui } = renderThread({
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
id: "u1",
|
|
161
|
+
role: "user",
|
|
162
|
+
content: [{ type: "text", text: "hi" }],
|
|
163
|
+
createdAt: new Date(0),
|
|
164
|
+
attachments: [],
|
|
165
|
+
metadata: { custom: {} },
|
|
166
|
+
} as unknown as ExternalThreadMessage,
|
|
167
|
+
],
|
|
168
|
+
isRunning: true,
|
|
169
|
+
queue: {
|
|
170
|
+
items: [],
|
|
171
|
+
enqueue,
|
|
172
|
+
steer: vi.fn(),
|
|
173
|
+
remove: vi.fn(),
|
|
174
|
+
clear: vi.fn(),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
aui().thread.composer().setText("queued");
|
|
179
|
+
aui().thread.composer().send();
|
|
180
|
+
|
|
181
|
+
await waitFor(() => expect(enqueue).toHaveBeenCalledTimes(1));
|
|
182
|
+
expect(enqueue.mock.calls[0]![0].parentId).toBe("u1");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("still refuses to send an empty composer synchronously after a send", async () => {
|
|
186
|
+
const onNew = vi.fn();
|
|
187
|
+
const { aui } = renderThread({ messages: [], isRunning: false, onNew });
|
|
188
|
+
|
|
189
|
+
aui().thread.composer().send();
|
|
190
|
+
aui().thread.composer().setText("first");
|
|
191
|
+
aui().thread.composer().send();
|
|
192
|
+
aui().thread.composer().send();
|
|
193
|
+
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
195
|
+
expect(onNew).toHaveBeenCalledTimes(1);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("ExternalThread duplicate message ids", () => {
|
|
200
|
+
const userMessage = (id: string, text: string): ExternalThreadMessage =>
|
|
201
|
+
({
|
|
202
|
+
id,
|
|
203
|
+
role: "user",
|
|
204
|
+
content: [{ type: "text", text }],
|
|
205
|
+
createdAt: new Date(0),
|
|
206
|
+
metadata: { custom: {} },
|
|
207
|
+
}) as unknown as ExternalThreadMessage;
|
|
208
|
+
|
|
209
|
+
it("warns and keeps the last occurrence instead of throwing on a duplicate id", () => {
|
|
210
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
211
|
+
try {
|
|
212
|
+
const { aui } = renderThread({
|
|
213
|
+
messages: [
|
|
214
|
+
userMessage("u1", "hi"),
|
|
215
|
+
userMessage("dup", "stale"),
|
|
216
|
+
userMessage("dup", "fresh"),
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const state = aui().thread.getState();
|
|
221
|
+
expect(state.messages.map((m) => m.id)).toEqual(["u1", "dup"]);
|
|
222
|
+
|
|
223
|
+
const dup = aui().thread.message({ id: "dup" }).getState();
|
|
224
|
+
expect(dup.parts[0]).toMatchObject({ type: "text", text: "fresh" });
|
|
225
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('"dup"'));
|
|
226
|
+
} finally {
|
|
227
|
+
warn.mockRestore();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|