@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
package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { FC } from "react";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { useAui } from "@assistant-ui/store";
|
|
7
|
+
import { AssistantRuntimeProvider } from "../../../context";
|
|
8
|
+
import {
|
|
9
|
+
useAssistantTransportRuntime,
|
|
10
|
+
useAssistantTransportSendCommand,
|
|
11
|
+
} from "./useAssistantTransportRuntime";
|
|
12
|
+
import type {
|
|
13
|
+
AssistantTransportCommand,
|
|
14
|
+
AssistantTransportOptions,
|
|
15
|
+
AssistantTransportStateConverter,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
const converter: AssistantTransportStateConverter<unknown> = (
|
|
19
|
+
_state,
|
|
20
|
+
meta,
|
|
21
|
+
) => ({
|
|
22
|
+
messages: [],
|
|
23
|
+
isRunning: meta.isSending,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const createMessageCommand = (text: string): AssistantTransportCommand => ({
|
|
27
|
+
type: "add-message",
|
|
28
|
+
message: {
|
|
29
|
+
role: "user",
|
|
30
|
+
parts: [{ type: "text", text }],
|
|
31
|
+
},
|
|
32
|
+
parentId: null,
|
|
33
|
+
sourceId: null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const createStreamResponse = () => {
|
|
37
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
38
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
39
|
+
start(c) {
|
|
40
|
+
controller = c;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
response: new Response(stream, { status: 200 }),
|
|
45
|
+
close: () => controller.close(),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type StreamResponse = ReturnType<typeof createStreamResponse>;
|
|
50
|
+
|
|
51
|
+
type RecordedRequest = {
|
|
52
|
+
url: string;
|
|
53
|
+
init: RequestInit;
|
|
54
|
+
body: Record<string, any>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const installFetch = () => {
|
|
58
|
+
const requests: RecordedRequest[] = [];
|
|
59
|
+
const servers: StreamResponse[] = [];
|
|
60
|
+
|
|
61
|
+
vi.stubGlobal(
|
|
62
|
+
"fetch",
|
|
63
|
+
async (url: RequestInfo | URL, init: RequestInit = {}) => {
|
|
64
|
+
requests.push({
|
|
65
|
+
url: String(url),
|
|
66
|
+
init,
|
|
67
|
+
body: JSON.parse(init.body as string),
|
|
68
|
+
});
|
|
69
|
+
const server = createStreamResponse();
|
|
70
|
+
servers.push(server);
|
|
71
|
+
return server.response;
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { requests, servers };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const mountRuntime = (
|
|
79
|
+
options?: Partial<AssistantTransportOptions<unknown>>,
|
|
80
|
+
) => {
|
|
81
|
+
const captured: {
|
|
82
|
+
aui?: ReturnType<typeof useAui>;
|
|
83
|
+
sendCommand?: (command: AssistantTransportCommand) => void;
|
|
84
|
+
} = {};
|
|
85
|
+
const Capture: FC = () => {
|
|
86
|
+
captured.aui = useAui();
|
|
87
|
+
captured.sendCommand = useAssistantTransportSendCommand();
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
const App: FC = () => {
|
|
91
|
+
const runtime = useAssistantTransportRuntime({
|
|
92
|
+
initialState: {},
|
|
93
|
+
api: "https://example.com/api",
|
|
94
|
+
headers: {},
|
|
95
|
+
converter,
|
|
96
|
+
...options,
|
|
97
|
+
});
|
|
98
|
+
return (
|
|
99
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
100
|
+
<Capture />
|
|
101
|
+
</AssistantRuntimeProvider>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
const utils = render(<App />);
|
|
105
|
+
return {
|
|
106
|
+
aui: () => captured.aui!,
|
|
107
|
+
sendCommand: (command: AssistantTransportCommand) =>
|
|
108
|
+
captured.sendCommand!(command),
|
|
109
|
+
...utils,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
vi.unstubAllGlobals();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("useAssistantTransportRuntime", () => {
|
|
118
|
+
it("no-ops a follow-up run that finds an empty queue", async () => {
|
|
119
|
+
const fetchMock = installFetch();
|
|
120
|
+
const onError = vi.fn();
|
|
121
|
+
const { aui, sendCommand } = mountRuntime({ onError });
|
|
122
|
+
await waitFor(() =>
|
|
123
|
+
expect(
|
|
124
|
+
(aui().thread.getState().extras as { sendCommand?: unknown })
|
|
125
|
+
?.sendCommand,
|
|
126
|
+
).toBeTypeOf("function"),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Two synchronous sends coalesce into one request plus one follow-up run.
|
|
130
|
+
act(() => {
|
|
131
|
+
sendCommand(createMessageCommand("a"));
|
|
132
|
+
sendCommand(createMessageCommand("b"));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
|
|
136
|
+
expect(
|
|
137
|
+
fetchMock.requests[0]!.body["commands"].map((c: any) => c.type),
|
|
138
|
+
).toEqual(["add-message", "add-message"]);
|
|
139
|
+
|
|
140
|
+
act(() => fetchMock.servers[0]!.close());
|
|
141
|
+
|
|
142
|
+
await waitFor(() => expect(aui().thread.getState().isRunning).toBe(false));
|
|
143
|
+
await act(async () => {});
|
|
144
|
+
expect(onError).not.toHaveBeenCalled();
|
|
145
|
+
expect(fetchMock.requests).toHaveLength(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("flushes commands enqueued during a resume run in a follow-up run", async () => {
|
|
149
|
+
const fetchMock = installFetch();
|
|
150
|
+
const { aui, sendCommand } = mountRuntime({
|
|
151
|
+
resumeApi: "https://example.com/resume",
|
|
152
|
+
});
|
|
153
|
+
await waitFor(() =>
|
|
154
|
+
expect(
|
|
155
|
+
(aui().thread.getState().extras as { sendCommand?: unknown })
|
|
156
|
+
?.sendCommand,
|
|
157
|
+
).toBeTypeOf("function"),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
act(() => sendCommand(createMessageCommand("a")));
|
|
161
|
+
await waitFor(() => expect(fetchMock.requests).toHaveLength(1));
|
|
162
|
+
|
|
163
|
+
// Both land while the first run is active and coalesce into one follow-up.
|
|
164
|
+
act(() => {
|
|
165
|
+
sendCommand(createMessageCommand("b"));
|
|
166
|
+
aui().thread.resumeRun({ parentId: null });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
act(() => fetchMock.servers[0]!.close());
|
|
170
|
+
await waitFor(() => expect(fetchMock.requests).toHaveLength(2));
|
|
171
|
+
expect(fetchMock.requests[1]!.url).toBe("https://example.com/resume");
|
|
172
|
+
expect(fetchMock.requests[1]!.body["commands"]).toEqual([]);
|
|
173
|
+
|
|
174
|
+
// "b" coalesced into the resume run and must not starve in the queue.
|
|
175
|
+
act(() => fetchMock.servers[1]!.close());
|
|
176
|
+
await waitFor(() => expect(fetchMock.requests).toHaveLength(3));
|
|
177
|
+
expect(fetchMock.requests[2]!.url).toBe("https://example.com/api");
|
|
178
|
+
expect(fetchMock.requests[2]!.body["commands"]).toEqual([
|
|
179
|
+
createMessageCommand("b"),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
act(() => fetchMock.servers[2]!.close());
|
|
183
|
+
await waitFor(() => expect(aui().thread.getState().isRunning).toBe(false));
|
|
184
|
+
});
|
|
185
|
+
});
|
package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts
CHANGED
|
@@ -96,7 +96,7 @@ export const useAssistantTransportSendCommand = () => {
|
|
|
96
96
|
const aui = useAui();
|
|
97
97
|
|
|
98
98
|
return (command: AssistantTransportCommand) => {
|
|
99
|
-
const extras = aui.thread
|
|
99
|
+
const extras = aui.thread.getState().extras;
|
|
100
100
|
const transportExtras = asAssistantTransportExtras(extras);
|
|
101
101
|
transportExtras.sendCommand(command);
|
|
102
102
|
};
|
|
@@ -135,8 +135,13 @@ const useAssistantTransportThreadRuntime = <T>(
|
|
|
135
135
|
resumeFlagRef.current = false;
|
|
136
136
|
setIsReplaying(false);
|
|
137
137
|
const commands: QueuedCommand[] = isResume ? [] : commandQueue.flush();
|
|
138
|
-
if (commands.length === 0 && !isResume)
|
|
139
|
-
|
|
138
|
+
if (commands.length === 0 && !isResume) return;
|
|
139
|
+
|
|
140
|
+
// The flushed batch consumes the parentId; read it alongside the flush
|
|
141
|
+
// (before any awaits) so a mid-run append keeps its own value. Resume
|
|
142
|
+
// runs send no commands, so they neither send nor consume it.
|
|
143
|
+
const parentId = isResume ? undefined : parentIdRef.current;
|
|
144
|
+
if (!isResume) parentIdRef.current = undefined;
|
|
140
145
|
|
|
141
146
|
const headers = await createRequestHeaders(options.headers);
|
|
142
147
|
const bodyValue =
|
|
@@ -151,8 +156,8 @@ const useAssistantTransportThreadRuntime = <T>(
|
|
|
151
156
|
system: context.system,
|
|
152
157
|
tools: context.tools ? toToolsJSONSchema(context.tools) : undefined,
|
|
153
158
|
threadId,
|
|
154
|
-
...(
|
|
155
|
-
parentId
|
|
159
|
+
...(parentId !== undefined && {
|
|
160
|
+
parentId,
|
|
156
161
|
}),
|
|
157
162
|
// nested (new format, aligned with AssistantChatTransport)
|
|
158
163
|
callSettings: context.callSettings,
|
|
@@ -232,6 +237,17 @@ const useAssistantTransportThreadRuntime = <T>(
|
|
|
232
237
|
if (err) {
|
|
233
238
|
throw new Error(err);
|
|
234
239
|
}
|
|
240
|
+
|
|
241
|
+
// A successful run confirms delivery even when no state-changing
|
|
242
|
+
// chunk was observed.
|
|
243
|
+
if (!markedDelivered) {
|
|
244
|
+
commandQueue.markDelivered();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// commands that coalesced into this resume run must not starve
|
|
248
|
+
if (isResume && commandQueue.state.queued.length > 0) {
|
|
249
|
+
runManager.schedule();
|
|
250
|
+
}
|
|
235
251
|
},
|
|
236
252
|
onFinish: options.onFinish,
|
|
237
253
|
onCancel: () => {
|
|
@@ -162,7 +162,7 @@ function InlineRenderer({
|
|
|
162
162
|
sendMessage: (params) => {
|
|
163
163
|
const text = extractSendMessageText(params);
|
|
164
164
|
if (!text) return { ok: false, reason: "unrecognised params shape" };
|
|
165
|
-
aui.thread
|
|
165
|
+
aui.thread.append({ content: [{ type: "text", text }] });
|
|
166
166
|
return { ok: true };
|
|
167
167
|
},
|
|
168
168
|
callTool: (params) =>
|
|
@@ -104,7 +104,7 @@ export const makeAssistantVisible = <T extends ComponentType<any>>(
|
|
|
104
104
|
|
|
105
105
|
const { clickable, editable } = config ?? {};
|
|
106
106
|
useEffect(() => {
|
|
107
|
-
return aui.modelContext
|
|
107
|
+
return aui.modelContext.register({
|
|
108
108
|
getModelContext: () => {
|
|
109
109
|
return {
|
|
110
110
|
tools: {
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useAuiState } from "@assistant-ui/store";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
Hidden
|
|
7
|
-
Floating
|
|
8
|
-
Normal
|
|
9
|
-
}
|
|
5
|
+
export const HideAndFloatStatus = {
|
|
6
|
+
Hidden: "hidden",
|
|
7
|
+
Floating: "floating",
|
|
8
|
+
Normal: "normal",
|
|
9
|
+
} as const;
|
|
10
|
+
export type HideAndFloatStatus =
|
|
11
|
+
(typeof HideAndFloatStatus)[keyof typeof HideAndFloatStatus];
|
|
10
12
|
|
|
11
13
|
export type UseActionBarFloatStatusProps = {
|
|
12
14
|
hideWhenRunning?: boolean | undefined;
|
|
@@ -13,7 +13,7 @@ const useChainOfThoughtAccordionTrigger = () => {
|
|
|
13
13
|
const collapsed = useAuiState((s) => s.chainOfThought.collapsed);
|
|
14
14
|
|
|
15
15
|
const callback = useCallback(() => {
|
|
16
|
-
aui.chainOfThought
|
|
16
|
+
aui.chainOfThought.setCollapsed(!collapsed);
|
|
17
17
|
}, [aui, collapsed]);
|
|
18
18
|
|
|
19
19
|
return callback;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { act } from "react";
|
|
5
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
6
|
+
import type * as CoreReact from "@assistant-ui/core/react";
|
|
7
|
+
import type * as AssistantStore from "@assistant-ui/store";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { ComposerPrimitiveAddAttachment } from "./ComposerAddAttachment";
|
|
10
|
+
|
|
11
|
+
const { addAttachment } = vi.hoisted(() => ({
|
|
12
|
+
addAttachment: vi.fn<(file: File) => Promise<void>>(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
|
+
|
|
17
|
+
vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof CoreReact>();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
useComposerAddAttachment: () => ({
|
|
22
|
+
disabled: false,
|
|
23
|
+
addAttachment,
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
29
|
+
const actual = await importOriginal<typeof AssistantStore>();
|
|
30
|
+
return {
|
|
31
|
+
...actual,
|
|
32
|
+
useAui: () => ({
|
|
33
|
+
composer: {
|
|
34
|
+
getState: () => ({ attachmentAccept: "*" }),
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("ComposerPrimitiveAddAttachment", () => {
|
|
41
|
+
let container: HTMLDivElement;
|
|
42
|
+
let root: Root;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
addAttachment.mockReset();
|
|
46
|
+
vi.spyOn(HTMLInputElement.prototype, "click").mockImplementation(() => {});
|
|
47
|
+
|
|
48
|
+
container = document.createElement("div");
|
|
49
|
+
document.body.appendChild(container);
|
|
50
|
+
root = createRoot(container);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
await act(async () => {
|
|
55
|
+
root.unmount();
|
|
56
|
+
});
|
|
57
|
+
document.querySelectorAll('input[type="file"]').forEach((input) => {
|
|
58
|
+
input.remove();
|
|
59
|
+
});
|
|
60
|
+
container.remove();
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles rejected file picker attachments and continues processing", async () => {
|
|
65
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
66
|
+
|
|
67
|
+
addAttachment
|
|
68
|
+
.mockRejectedValueOnce(new Error("unsupported file"))
|
|
69
|
+
.mockResolvedValueOnce(undefined);
|
|
70
|
+
|
|
71
|
+
await act(async () => {
|
|
72
|
+
root.render(
|
|
73
|
+
<ComposerPrimitiveAddAttachment>
|
|
74
|
+
Add attachment
|
|
75
|
+
</ComposerPrimitiveAddAttachment>,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const button = container.querySelector("button");
|
|
80
|
+
expect(button).not.toBeNull();
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
button!.click();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const input =
|
|
87
|
+
document.querySelector<HTMLInputElement>('input[type="file"]');
|
|
88
|
+
expect(input).not.toBeNull();
|
|
89
|
+
|
|
90
|
+
const files = [
|
|
91
|
+
new File(["a"], "unsupported.bin"),
|
|
92
|
+
new File(["b"], "supported.txt", { type: "text/plain" }),
|
|
93
|
+
];
|
|
94
|
+
Object.defineProperty(input, "files", {
|
|
95
|
+
configurable: true,
|
|
96
|
+
value: files,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = input!.onchange?.call(input, {
|
|
100
|
+
target: input,
|
|
101
|
+
} as unknown as Event);
|
|
102
|
+
|
|
103
|
+
await expect(Promise.resolve(result)).resolves.toBeUndefined();
|
|
104
|
+
expect(addAttachment).toHaveBeenCalledTimes(2);
|
|
105
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
106
|
+
expect(input!.isConnected).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -24,21 +24,27 @@ const useComposerAddAttachment = ({
|
|
|
24
24
|
input.multiple = multiple;
|
|
25
25
|
input.hidden = true;
|
|
26
26
|
|
|
27
|
-
const attachmentAccept = aui.composer
|
|
27
|
+
const attachmentAccept = aui.composer.getState().attachmentAccept;
|
|
28
28
|
if (attachmentAccept !== "*") {
|
|
29
29
|
input.accept = attachmentAccept;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
document.body.appendChild(input);
|
|
33
33
|
|
|
34
|
-
input.onchange = (e) => {
|
|
34
|
+
input.onchange = async (e) => {
|
|
35
35
|
const fileList = (e.target as HTMLInputElement).files;
|
|
36
36
|
if (!fileList) return;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
|
|
38
|
+
const attachmentPromises = Array.from(fileList, async (file) => {
|
|
39
|
+
try {
|
|
40
|
+
await addAttachment(file);
|
|
41
|
+
} catch {
|
|
42
|
+
// The composer runtime emits composer.attachmentAddError before rejecting.
|
|
43
|
+
}
|
|
44
|
+
});
|
|
40
45
|
|
|
41
46
|
document.body.removeChild(input);
|
|
47
|
+
await Promise.all(attachmentPromises);
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
input.oncancel = () => {
|
|
@@ -15,9 +15,9 @@ vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
|
15
15
|
return {
|
|
16
16
|
...actual,
|
|
17
17
|
useAui: () => ({
|
|
18
|
-
composer:
|
|
18
|
+
composer: {
|
|
19
19
|
addAttachment,
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
21
|
}),
|
|
22
22
|
};
|
|
23
23
|
});
|
|
@@ -95,7 +95,7 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it("continues processing other files when one attachment fails", async () => {
|
|
98
|
+
it("continues processing other files when one attachment fails without console.error", async () => {
|
|
99
99
|
const error = new Error("upload failed");
|
|
100
100
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
101
101
|
|
|
@@ -120,6 +120,7 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
expect(addAttachment).toHaveBeenCalledTimes(3);
|
|
123
|
-
expect(errorSpy).
|
|
123
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
124
|
+
errorSpy.mockRestore();
|
|
124
125
|
});
|
|
125
126
|
});
|
|
@@ -70,9 +70,10 @@ export const ComposerPrimitiveAttachmentDropzone = forwardRef<
|
|
|
70
70
|
await Promise.all(
|
|
71
71
|
files.map(async (file) => {
|
|
72
72
|
try {
|
|
73
|
-
await aui.composer
|
|
74
|
-
} catch
|
|
75
|
-
|
|
73
|
+
await aui.composer.addAttachment(file);
|
|
74
|
+
} catch {
|
|
75
|
+
// The composer runtime emits composer.attachmentAddError before rejecting;
|
|
76
|
+
// the readonly and empty-thread stubs only throw.
|
|
76
77
|
}
|
|
77
78
|
}),
|
|
78
79
|
);
|
|
@@ -9,6 +9,7 @@ import { ComposerPrimitiveInput } from "./ComposerInput";
|
|
|
9
9
|
const setText = vi.fn<(text: string) => void>();
|
|
10
10
|
const setCursorPosition = vi.fn<(pos: number) => void>();
|
|
11
11
|
const sendSpy = vi.fn<(options?: { steer?: boolean }) => void>();
|
|
12
|
+
const addAttachment = vi.fn<(file: File) => Promise<void>>();
|
|
12
13
|
|
|
13
14
|
const composerState = {
|
|
14
15
|
isEditing: true,
|
|
@@ -37,16 +38,16 @@ let pluginRegistry: { getPlugins: () => (typeof plugin)[] } | null = null;
|
|
|
37
38
|
|
|
38
39
|
vi.mock("@assistant-ui/store", () => {
|
|
39
40
|
const aui = {
|
|
40
|
-
composer:
|
|
41
|
-
setText,
|
|
41
|
+
composer: {
|
|
42
|
+
setText: (text: string) => setText(text),
|
|
42
43
|
getState: () => composerState,
|
|
43
44
|
cancel: () => {},
|
|
44
|
-
send: sendSpy,
|
|
45
|
-
addAttachment:
|
|
46
|
-
}
|
|
47
|
-
thread:
|
|
45
|
+
send: (options?: { steer?: boolean }) => sendSpy(options),
|
|
46
|
+
addAttachment: (file: File) => addAttachment(file),
|
|
47
|
+
},
|
|
48
|
+
thread: {
|
|
48
49
|
getState: () => threadState,
|
|
49
|
-
}
|
|
50
|
+
},
|
|
50
51
|
on: () => () => {},
|
|
51
52
|
};
|
|
52
53
|
type Selector<T> = (s: {
|
|
@@ -140,6 +141,16 @@ const fireKeyDown = (
|
|
|
140
141
|
return event;
|
|
141
142
|
};
|
|
142
143
|
|
|
144
|
+
const firePaste = (textarea: HTMLTextAreaElement, files: File[]) => {
|
|
145
|
+
const event = new Event("paste", { bubbles: true, cancelable: true });
|
|
146
|
+
Object.defineProperty(event, "clipboardData", {
|
|
147
|
+
value: { files },
|
|
148
|
+
configurable: true,
|
|
149
|
+
});
|
|
150
|
+
textarea.dispatchEvent(event);
|
|
151
|
+
return event;
|
|
152
|
+
};
|
|
153
|
+
|
|
143
154
|
const setMatchMedia = (matches: boolean) => {
|
|
144
155
|
Object.defineProperty(window, "matchMedia", {
|
|
145
156
|
writable: true,
|
|
@@ -169,6 +180,7 @@ describe("ComposerPrimitiveInput", () => {
|
|
|
169
180
|
setText.mockReset();
|
|
170
181
|
setCursorPosition.mockReset();
|
|
171
182
|
sendSpy.mockReset();
|
|
183
|
+
addAttachment.mockReset();
|
|
172
184
|
composerState.isEditing = true;
|
|
173
185
|
composerState.text = "";
|
|
174
186
|
composerState.isEmpty = true;
|
|
@@ -501,4 +513,89 @@ describe("ComposerPrimitiveInput", () => {
|
|
|
501
513
|
expect(event.defaultPrevented).toBe(true);
|
|
502
514
|
});
|
|
503
515
|
});
|
|
516
|
+
|
|
517
|
+
describe("paste attachments", () => {
|
|
518
|
+
it("attempts every pasted file and prevents default when attachments are supported", async () => {
|
|
519
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
520
|
+
|
|
521
|
+
const textarea = await mount();
|
|
522
|
+
|
|
523
|
+
let event!: Event;
|
|
524
|
+
await act(async () => {
|
|
525
|
+
event = firePaste(textarea, [
|
|
526
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
527
|
+
new File(["b"], "second.txt", { type: "text/plain" }),
|
|
528
|
+
new File(["c"], "third.txt", { type: "text/plain" }),
|
|
529
|
+
]);
|
|
530
|
+
await Promise.resolve();
|
|
531
|
+
await Promise.resolve();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(addAttachment).toHaveBeenCalledTimes(3);
|
|
535
|
+
expect(addAttachment.mock.calls.map(([file]) => file.name)).toEqual([
|
|
536
|
+
"first.txt",
|
|
537
|
+
"second.txt",
|
|
538
|
+
"third.txt",
|
|
539
|
+
]);
|
|
540
|
+
expect(event.defaultPrevented).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("continues processing other files when one attachment fails without console.error", async () => {
|
|
544
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
545
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
546
|
+
|
|
547
|
+
addAttachment
|
|
548
|
+
.mockRejectedValueOnce(new Error("upload failed"))
|
|
549
|
+
.mockResolvedValueOnce(undefined)
|
|
550
|
+
.mockResolvedValueOnce(undefined);
|
|
551
|
+
|
|
552
|
+
const textarea = await mount();
|
|
553
|
+
|
|
554
|
+
await act(async () => {
|
|
555
|
+
firePaste(textarea, [
|
|
556
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
557
|
+
new File(["b"], "second.txt", { type: "text/plain" }),
|
|
558
|
+
new File(["c"], "third.txt", { type: "text/plain" }),
|
|
559
|
+
]);
|
|
560
|
+
await Promise.resolve();
|
|
561
|
+
await Promise.resolve();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(addAttachment).toHaveBeenCalledTimes(3);
|
|
565
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
566
|
+
errorSpy.mockRestore();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("does not add attachments on paste when attachments are unsupported", async () => {
|
|
570
|
+
threadState.capabilities = { queue: false, attachments: false };
|
|
571
|
+
|
|
572
|
+
const textarea = await mount();
|
|
573
|
+
|
|
574
|
+
let event!: Event;
|
|
575
|
+
await act(async () => {
|
|
576
|
+
event = firePaste(textarea, [
|
|
577
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
578
|
+
]);
|
|
579
|
+
await Promise.resolve();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(addAttachment).not.toHaveBeenCalled();
|
|
583
|
+
expect(event.defaultPrevented).toBe(false);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("skips attachment handling when addAttachmentOnPaste is false", async () => {
|
|
587
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
588
|
+
|
|
589
|
+
const textarea = await mount({ addAttachmentOnPaste: false });
|
|
590
|
+
|
|
591
|
+
await act(async () => {
|
|
592
|
+
firePaste(textarea, [
|
|
593
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
594
|
+
]);
|
|
595
|
+
await Promise.resolve();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
expect(addAttachment).not.toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
504
601
|
});
|