@assistant-ui/core 0.3.1 → 0.3.3
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/index.d.ts +4 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +6 -7
- package/dist/internal.js +5 -6
- package/dist/react/AssistantProvider.d.ts +1 -1
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.js +35 -9
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
- package/dist/react/AssistantRuntimeProvider.js +1 -1
- package/dist/react/RuntimeAdapter.js +2 -2
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +17 -33
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/DataRenderers.js +1 -1
- package/dist/react/client/Interactables.js +1 -1
- package/dist/react/client/Tools.js +2 -2
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +2 -1
- package/dist/react/interactables-legacy/Interactables.js +1 -1
- package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
- package/dist/react/interactables-legacy/useInteractableState.js +2 -2
- package/dist/react/model-context/useAssistantContext.js +1 -1
- package/dist/react/model-context/useAssistantDataUI.js +2 -2
- package/dist/react/model-context/useAssistantInstructions.js +2 -2
- package/dist/react/model-context/useAssistantTool.js +2 -2
- package/dist/react/model-context/useAssistantToolUI.js +2 -2
- package/dist/react/model-context/useAuiToolOverrides.js +1 -1
- package/dist/react/model-context/useInlineRender.js +1 -1
- package/dist/react/model-context/useInteractable.js +1 -1
- package/dist/react/model-context/useInteractableState.js +2 -2
- package/dist/react/model-context/useInteractableVersions.js +2 -2
- package/dist/react/model-context/useToolArgsStatus.js +2 -2
- package/dist/react/primitive-hooks/useActionBarCopy.js +52 -25
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarEdit.js +2 -2
- package/dist/react/primitive-hooks/useActionBarFeedback.js +2 -2
- package/dist/react/primitive-hooks/useActionBarReload.js +2 -2
- package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -2
- package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -2
- package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -2
- package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -2
- package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -2
- package/dist/react/primitive-hooks/useComposerCancel.js +2 -2
- package/dist/react/primitive-hooks/useComposerDictate.js +2 -2
- package/dist/react/primitive-hooks/useComposerSend.js +2 -2
- package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -2
- package/dist/react/primitive-hooks/useEditComposerSend.js +2 -2
- package/dist/react/primitive-hooks/useMessageBranching.js +2 -2
- package/dist/react/primitive-hooks/useMessageReload.js +2 -2
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -2
- package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -2
- package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -2
- package/dist/react/primitive-hooks/useThreadListNew.js +2 -2
- package/dist/react/primitive-hooks/useThreadMessageIds.js +1 -1
- package/dist/react/primitive-hooks/useVoice.js +2 -2
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +2 -2
- package/dist/react/primitives/composer/ComposerAttachments.js +2 -2
- package/dist/react/primitives/composer/ComposerIf.js +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
- package/dist/react/primitives/message/MessageAttachments.js +2 -2
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +3 -3
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -2
- package/dist/react/primitives/message/MessageQuote.js +2 -2
- package/dist/react/primitives/part/PartMessages.js +2 -2
- package/dist/react/primitives/thread/ThreadMessages.js +2 -2
- package/dist/react/primitives/thread/ThreadSuggestions.js +2 -2
- package/dist/react/primitives/threadList/ThreadListItems.js +2 -2
- package/dist/react/primitives/threadListItem/ThreadListItemTitle.js +1 -1
- package/dist/react/providers/AttachmentByIndexProvider.js +1 -1
- package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +1 -1
- package/dist/react/providers/MessageByIdProvider.js +1 -1
- package/dist/react/providers/MessageByIndexProvider.js +1 -1
- package/dist/react/providers/PartByIndexProvider.js +1 -1
- package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +2 -2
- package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
- package/dist/react/providers/TextMessagePartProvider.js +2 -2
- package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
- package/dist/react/providers/ThreadListItemRuntimeProvider.js +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +14 -13
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +22 -12
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.js +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +9 -23
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +24 -2
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +24 -5
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/useExternalStoreRuntime.js +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +7 -13
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/utils/useJSONEqual.js +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +5 -1
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/internal.d.ts +4 -4
- package/dist/runtime/internal.js +4 -4
- package/dist/runtime/utils/thread-message-like.js +2 -2
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +5 -4
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/chain-of-thought-client.js +30 -24
- package/dist/store/clients/chain-of-thought-client.js.map +1 -1
- package/dist/store/clients/model-context-client.js +1 -1
- package/dist/store/clients/no-op-composer-client.js +1 -1
- package/dist/store/clients/suggestions.js +2 -2
- package/dist/store/clients/thread-message-client.d.ts.map +1 -1
- package/dist/store/clients/thread-message-client.js +103 -91
- package/dist/store/clients/thread-message-client.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +2 -2
- package/dist/store/runtime-clients/message-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-list-runtime-client.js +2 -2
- package/dist/store/runtime-clients/thread-runtime-client.js +2 -2
- package/dist/store/scopes/thread.d.ts +1 -1
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/message.d.ts +11 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.js +2 -2
- package/dist/utils/data-url.js.map +1 -1
- package/dist/utils/getGroupStatus.d.ts +9 -0
- package/dist/utils/getGroupStatus.d.ts.map +1 -0
- package/dist/utils/getGroupStatus.js +15 -0
- package/dist/utils/getGroupStatus.js.map +1 -0
- package/dist/utils/normalizePartStatus.d.ts +8 -0
- package/dist/utils/normalizePartStatus.d.ts.map +1 -0
- package/dist/utils/normalizePartStatus.js +39 -0
- package/dist/utils/normalizePartStatus.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +1 -7
- package/src/react/AssistantProvider.tsx +28 -15
- package/src/react/AssistantRuntimeProvider.test.tsx +101 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +28 -7
- package/src/react/index.ts +4 -0
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +109 -14
- package/src/react/primitive-hooks/useActionBarCopy.ts +27 -2
- package/src/react/primitives/message/MessageGroupedParts.tsx +4 -4
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +16 -13
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +53 -10
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +7 -5
- package/src/react/runtimes/cloud/auiV0.ts +51 -12
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +9 -15
- package/src/runtime/api/message-runtime.test.ts +140 -0
- package/src/runtime/api/message-runtime.ts +10 -5
- package/src/runtime/utils/thread-message-like.ts +2 -2
- package/src/runtimes/remote-thread-list/types.ts +5 -4
- package/src/store/clients/chain-of-thought-client.ts +2 -7
- package/src/store/clients/thread-message-client.test.ts +84 -0
- package/src/store/clients/thread-message-client.ts +14 -4
- package/src/store/scopes/thread.ts +1 -1
- package/src/tests/auiV0Encode.test.ts +93 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +0 -92
- package/src/tests/thread-message-like.test.ts +15 -0
- package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +293 -0
- package/src/types/index.ts +1 -0
- package/src/types/message.ts +19 -0
- package/src/utils/data-url.test.ts +26 -2
- package/src/utils/data-url.ts +2 -2
- package/src/utils/getGroupStatus.test.ts +36 -0
- package/src/utils/getGroupStatus.ts +31 -0
- package/src/utils/normalizePartStatus.test.ts +59 -0
- package/src/utils/normalizePartStatus.ts +55 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { ThreadAssistantMessage } from "../../types/message";
|
|
4
|
+
import { ThreadMessageClient } from "./thread-message-client";
|
|
5
|
+
|
|
6
|
+
describe("ThreadMessageClient", () => {
|
|
7
|
+
const getPartStatus = (
|
|
8
|
+
part: ThreadAssistantMessage["content"][number],
|
|
9
|
+
status: ThreadAssistantMessage["status"],
|
|
10
|
+
) => {
|
|
11
|
+
const message: ThreadAssistantMessage = {
|
|
12
|
+
id: "message-1",
|
|
13
|
+
role: "assistant",
|
|
14
|
+
createdAt: new Date(0),
|
|
15
|
+
content: [part],
|
|
16
|
+
status,
|
|
17
|
+
metadata: {
|
|
18
|
+
unstable_state: null,
|
|
19
|
+
unstable_annotations: [],
|
|
20
|
+
unstable_data: [],
|
|
21
|
+
steps: [],
|
|
22
|
+
custom: {},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const root = createTapRoot(function ThreadMessageRoot() {
|
|
26
|
+
return useResource(ThreadMessageClient({ message, index: 0 }));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
return root.getValue().getState().parts[0]?.status;
|
|
31
|
+
} finally {
|
|
32
|
+
root.unmount();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
it("preserves a running part status on a running detached message", () => {
|
|
37
|
+
const part = {
|
|
38
|
+
type: "text",
|
|
39
|
+
text: "done",
|
|
40
|
+
status: { type: "running" },
|
|
41
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
42
|
+
|
|
43
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
44
|
+
type: "running",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("normalizes an unknown incomplete reason on a running detached message", () => {
|
|
49
|
+
const part = {
|
|
50
|
+
type: "text",
|
|
51
|
+
text: "done",
|
|
52
|
+
status: { type: "incomplete", reason: "unknown" },
|
|
53
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
54
|
+
|
|
55
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
56
|
+
type: "incomplete",
|
|
57
|
+
reason: "other",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("normalizes an upstream complete reason on a running detached message", () => {
|
|
62
|
+
const part = {
|
|
63
|
+
type: "text",
|
|
64
|
+
text: "done",
|
|
65
|
+
status: { type: "complete", reason: "unknown" },
|
|
66
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
67
|
+
|
|
68
|
+
expect(getPartStatus(part, { type: "running" })).toEqual({
|
|
69
|
+
type: "complete",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("marks parts complete on a non-running detached message", () => {
|
|
74
|
+
const part = {
|
|
75
|
+
type: "text",
|
|
76
|
+
text: "done",
|
|
77
|
+
status: { type: "running" },
|
|
78
|
+
} as unknown as ThreadAssistantMessage["content"][number];
|
|
79
|
+
|
|
80
|
+
expect(getPartStatus(part, { type: "complete", reason: "stop" })).toEqual({
|
|
81
|
+
type: "complete",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -11,19 +11,27 @@ import { useClientLookup } from "@assistant-ui/store";
|
|
|
11
11
|
import type { MessageState } from "../scopes/message";
|
|
12
12
|
import type { PartState } from "../scopes/part";
|
|
13
13
|
import { NoOpComposerClient } from "./no-op-composer-client";
|
|
14
|
+
import {
|
|
15
|
+
COMPLETE_STATUS,
|
|
16
|
+
normalizePartStatus,
|
|
17
|
+
} from "../../utils/normalizePartStatus";
|
|
14
18
|
import { getThreadMessageText } from "../../utils/text";
|
|
15
19
|
|
|
16
20
|
const useThreadMessagePartClient = ({
|
|
17
21
|
part,
|
|
22
|
+
isMessageRunning,
|
|
18
23
|
}: {
|
|
19
24
|
part: ThreadAssistantMessagePart | ThreadUserMessagePart;
|
|
25
|
+
isMessageRunning: boolean;
|
|
20
26
|
}): ClientOutput<"part"> => {
|
|
21
27
|
const state = useMemo<PartState>(() => {
|
|
22
28
|
return {
|
|
23
29
|
...part,
|
|
24
|
-
status:
|
|
30
|
+
status: isMessageRunning
|
|
31
|
+
? (normalizePartStatus(part) ?? COMPLETE_STATUS)
|
|
32
|
+
: COMPLETE_STATUS,
|
|
25
33
|
};
|
|
26
|
-
}, [part]);
|
|
34
|
+
}, [part, isMessageRunning]);
|
|
27
35
|
|
|
28
36
|
return {
|
|
29
37
|
getState: () => state,
|
|
@@ -74,6 +82,8 @@ const useThreadMessageClient = ({
|
|
|
74
82
|
}: ThreadMessageClientProps): ClientOutput<"message"> => {
|
|
75
83
|
const [isCopiedState, setIsCopied] = useState(false);
|
|
76
84
|
const [isHoveringState, setIsHovering] = useState(false);
|
|
85
|
+
const isMessageRunning =
|
|
86
|
+
message.role === "assistant" && message.status.type === "running";
|
|
77
87
|
|
|
78
88
|
const parts = useClientLookup(
|
|
79
89
|
message.content.map((part, idx) =>
|
|
@@ -81,8 +91,8 @@ const useThreadMessageClient = ({
|
|
|
81
91
|
"toolCallId" in part && part.toolCallId != null
|
|
82
92
|
? `toolCallId-${part.toolCallId}`
|
|
83
93
|
: `index-${idx}`,
|
|
84
|
-
ThreadMessagePartClient({ part }),
|
|
85
|
-
[part],
|
|
94
|
+
ThreadMessagePartClient({ part, isMessageRunning }),
|
|
95
|
+
[part, isMessageRunning],
|
|
86
96
|
),
|
|
87
97
|
),
|
|
88
98
|
);
|
|
@@ -108,7 +108,7 @@ export type ThreadMethods = {
|
|
|
108
108
|
* @param initialMessages - Optional array of initial messages to populate the thread
|
|
109
109
|
*/
|
|
110
110
|
reset(initialMessages?: readonly ThreadMessageLike[]): void;
|
|
111
|
-
importExternalState
|
|
111
|
+
importExternalState(state: unknown): void;
|
|
112
112
|
message(selector: { id: string } | { index: number }): MessageMethods;
|
|
113
113
|
/** @deprecated This API is still under active development and might change without notice. */
|
|
114
114
|
stopSpeaking(): void;
|
|
@@ -155,6 +155,99 @@ describe("auiV0Encode", () => {
|
|
|
155
155
|
},
|
|
156
156
|
]);
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
it("drops per-part status from message parts in the core cloud encoder", () => {
|
|
160
|
+
const encoded = auiV0Encode({
|
|
161
|
+
id: "m1",
|
|
162
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
163
|
+
role: "assistant",
|
|
164
|
+
status: { type: "complete", reason: "stop" },
|
|
165
|
+
metadata: {
|
|
166
|
+
unstable_state: null,
|
|
167
|
+
unstable_annotations: [],
|
|
168
|
+
unstable_data: [],
|
|
169
|
+
steps: [],
|
|
170
|
+
custom: {},
|
|
171
|
+
},
|
|
172
|
+
content: [
|
|
173
|
+
{ type: "reasoning", text: "thinking", status: { type: "complete" } },
|
|
174
|
+
{ type: "text", text: "answer", status: { type: "running" } },
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(encoded.content).toEqual([
|
|
179
|
+
{ type: "reasoning", text: "thinking" },
|
|
180
|
+
{ type: "text", text: "answer" },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("drops per-part status from attachment content in the core cloud encoder", () => {
|
|
185
|
+
const encoded = auiV0Encode({
|
|
186
|
+
id: "m1",
|
|
187
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
188
|
+
role: "user",
|
|
189
|
+
metadata: { custom: {} },
|
|
190
|
+
content: [{ type: "text", text: "please review this" }],
|
|
191
|
+
attachments: [
|
|
192
|
+
{
|
|
193
|
+
id: "att-1",
|
|
194
|
+
type: "document",
|
|
195
|
+
name: "notes.txt",
|
|
196
|
+
status: { type: "complete" },
|
|
197
|
+
content: [
|
|
198
|
+
{ type: "text", text: "notes", status: { type: "running" } },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(encoded.attachments?.[0]?.content).toEqual([
|
|
205
|
+
{ type: "text", text: "notes" },
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("preserves every attachment content field the wire shape carries in the core cloud encoder", () => {
|
|
210
|
+
const encoded = auiV0Encode({
|
|
211
|
+
id: "m1",
|
|
212
|
+
createdAt: new Date("2026-03-15T00:00:00.000Z"),
|
|
213
|
+
role: "user",
|
|
214
|
+
metadata: { custom: {} },
|
|
215
|
+
content: [{ type: "text", text: "please review these" }],
|
|
216
|
+
attachments: [
|
|
217
|
+
{
|
|
218
|
+
id: "att-1",
|
|
219
|
+
type: "file",
|
|
220
|
+
name: "bundle",
|
|
221
|
+
status: { type: "complete" },
|
|
222
|
+
content: [
|
|
223
|
+
{
|
|
224
|
+
type: "image",
|
|
225
|
+
image: "data:image/png;base64,iVBORw0KGgo=",
|
|
226
|
+
filename: "shot.png",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "audio",
|
|
230
|
+
audio: { data: "data:audio/mp3;base64,SUQzAw==", format: "mp3" },
|
|
231
|
+
},
|
|
232
|
+
{ type: "data", name: "telemetry", data: { runs: 3 } },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(encoded.attachments?.[0]?.content).toEqual([
|
|
239
|
+
{
|
|
240
|
+
type: "image",
|
|
241
|
+
image: "data:image/png;base64,iVBORw0KGgo=",
|
|
242
|
+
filename: "shot.png",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
type: "audio",
|
|
246
|
+
audio: { data: "data:audio/mp3;base64,SUQzAw==", format: "mp3" },
|
|
247
|
+
},
|
|
248
|
+
{ type: "data", name: "telemetry", data: { runs: 3 } },
|
|
249
|
+
]);
|
|
250
|
+
});
|
|
158
251
|
});
|
|
159
252
|
|
|
160
253
|
describe("auiV0Decode", () => {
|
|
@@ -2,98 +2,6 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
2
2
|
import type { RemoteThreadListAdapter } from "../runtimes/remote-thread-list/types";
|
|
3
3
|
import { createCore, makeAdapter } from "./remote-thread-list-test-helpers";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Tests for the reactive threadId useEffect logic in useRemoteThreadListRuntime.
|
|
7
|
-
*
|
|
8
|
-
* The useEffect compares options.threadId against a prevThreadIdRef and calls
|
|
9
|
-
* switchToThread or switchToNewThread when it changes. We test this logic
|
|
10
|
-
* by simulating the ref + comparison pattern.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// Mirrors the useEffect in useRemoteThreadListRuntimeImpl.
|
|
14
|
-
// Keep in sync if that implementation changes.
|
|
15
|
-
function simulateThreadIdEffect(
|
|
16
|
-
prevRef: { current: string | undefined },
|
|
17
|
-
threadId: string | undefined,
|
|
18
|
-
switchToThread: (id: string) => void,
|
|
19
|
-
switchToNewThread: () => void,
|
|
20
|
-
) {
|
|
21
|
-
if (threadId === prevRef.current) return;
|
|
22
|
-
prevRef.current = threadId;
|
|
23
|
-
if (threadId) {
|
|
24
|
-
switchToThread(threadId);
|
|
25
|
-
} else {
|
|
26
|
-
switchToNewThread();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("threadId reactive effect", () => {
|
|
31
|
-
it("does nothing when threadId stays the same", () => {
|
|
32
|
-
const ref = { current: "thread-1" };
|
|
33
|
-
const switchToThread = vi.fn();
|
|
34
|
-
const switchToNewThread = vi.fn();
|
|
35
|
-
|
|
36
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
37
|
-
|
|
38
|
-
expect(switchToThread).not.toHaveBeenCalled();
|
|
39
|
-
expect(switchToNewThread).not.toHaveBeenCalled();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("calls switchToThread when threadId changes", () => {
|
|
43
|
-
const ref = { current: "thread-1" };
|
|
44
|
-
const switchToThread = vi.fn();
|
|
45
|
-
const switchToNewThread = vi.fn();
|
|
46
|
-
|
|
47
|
-
simulateThreadIdEffect(ref, "thread-2", switchToThread, switchToNewThread);
|
|
48
|
-
|
|
49
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-2");
|
|
50
|
-
expect(ref.current).toBe("thread-2");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("calls switchToNewThread when threadId becomes undefined", () => {
|
|
54
|
-
const ref = { current: "thread-1" };
|
|
55
|
-
const switchToThread = vi.fn();
|
|
56
|
-
const switchToNewThread = vi.fn();
|
|
57
|
-
|
|
58
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
59
|
-
|
|
60
|
-
expect(switchToNewThread).toHaveBeenCalledOnce();
|
|
61
|
-
expect(ref.current).toBeUndefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("skips switchToNewThread on first render when threadId is already undefined", () => {
|
|
65
|
-
const ref = { current: undefined as string | undefined };
|
|
66
|
-
const switchToThread = vi.fn();
|
|
67
|
-
const switchToNewThread = vi.fn();
|
|
68
|
-
|
|
69
|
-
// First render: undefined === undefined → skipped
|
|
70
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
71
|
-
expect(switchToNewThread).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("handles full navigation cycle", () => {
|
|
75
|
-
const ref = { current: undefined as string | undefined };
|
|
76
|
-
const switchToThread = vi.fn();
|
|
77
|
-
const switchToNewThread = vi.fn();
|
|
78
|
-
|
|
79
|
-
// Mount with thread-1
|
|
80
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
81
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-1");
|
|
82
|
-
|
|
83
|
-
// Navigate to thread-2
|
|
84
|
-
simulateThreadIdEffect(ref, "thread-2", switchToThread, switchToNewThread);
|
|
85
|
-
expect(switchToThread).toHaveBeenCalledWith("thread-2");
|
|
86
|
-
|
|
87
|
-
// Navigate to new thread
|
|
88
|
-
simulateThreadIdEffect(ref, undefined, switchToThread, switchToNewThread);
|
|
89
|
-
expect(switchToNewThread).toHaveBeenCalledOnce();
|
|
90
|
-
|
|
91
|
-
// Navigate back to thread-1
|
|
92
|
-
simulateThreadIdEffect(ref, "thread-1", switchToThread, switchToNewThread);
|
|
93
|
-
expect(switchToThread).toHaveBeenCalledTimes(3);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
5
|
/**
|
|
98
6
|
* Tests for onThreadIdChange, driving the real
|
|
99
7
|
* RemoteThreadListThreadListRuntimeCore via a mock adapter (no React). These
|
|
@@ -159,6 +159,21 @@ describe("fromThreadMessageLike", () => {
|
|
|
159
159
|
|
|
160
160
|
expect(result.content).toEqual([]);
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it("keeps an image part with an uppercase-scheme data URL", () => {
|
|
164
|
+
const result = fromThreadMessageLike(
|
|
165
|
+
{
|
|
166
|
+
role: "assistant",
|
|
167
|
+
content: [{ type: "image", image: "DATA:IMAGE/PNG;base64,AAAA" }],
|
|
168
|
+
},
|
|
169
|
+
fallbackId,
|
|
170
|
+
fallbackStatus,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(result.content).toEqual([
|
|
174
|
+
{ type: "image", image: "DATA:IMAGE/PNG;base64,AAAA" },
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
162
177
|
});
|
|
163
178
|
|
|
164
179
|
describe("providerMetadata passthrough", () => {
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import { useEffect } from "react";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AssistantRuntimeProvider } from "../react/AssistantRuntimeProvider";
|
|
7
|
+
import { useExternalStoreRuntime } from "../react/runtimes/useExternalStoreRuntime";
|
|
8
|
+
import { useRemoteThreadListRuntime } from "../react/runtimes/useRemoteThreadListRuntime";
|
|
9
|
+
import type { AssistantRuntime } from "../runtime/api/assistant-runtime";
|
|
10
|
+
import type {
|
|
11
|
+
RemoteThreadListAdapter,
|
|
12
|
+
RemoteThreadMetadata,
|
|
13
|
+
} from "../runtimes/remote-thread-list/types";
|
|
14
|
+
import { deferred, makeAdapter } from "./remote-thread-list-test-helpers";
|
|
15
|
+
|
|
16
|
+
const EMPTY_MESSAGES: readonly never[] = [];
|
|
17
|
+
|
|
18
|
+
const makeThreadMetadata = (remoteId: string): RemoteThreadMetadata => ({
|
|
19
|
+
status: "regular",
|
|
20
|
+
remoteId,
|
|
21
|
+
externalId: remoteId,
|
|
22
|
+
title: "Test",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const useTestThreadRuntime = () =>
|
|
26
|
+
useExternalStoreRuntime({
|
|
27
|
+
messages: EMPTY_MESSAGES,
|
|
28
|
+
isRunning: false,
|
|
29
|
+
onNew: async () => {},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type RuntimeRef = {
|
|
33
|
+
current: AssistantRuntime | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ControlledRuntime = ({
|
|
37
|
+
adapter,
|
|
38
|
+
threadId,
|
|
39
|
+
onThreadIdChange,
|
|
40
|
+
runtimeRef,
|
|
41
|
+
}: {
|
|
42
|
+
adapter: RemoteThreadListAdapter;
|
|
43
|
+
threadId: string | undefined;
|
|
44
|
+
onThreadIdChange: (threadId: string | undefined) => void;
|
|
45
|
+
runtimeRef: RuntimeRef;
|
|
46
|
+
}) => {
|
|
47
|
+
const runtime = useRemoteThreadListRuntime({
|
|
48
|
+
adapter,
|
|
49
|
+
threadId,
|
|
50
|
+
onThreadIdChange,
|
|
51
|
+
runtimeHook: useTestThreadRuntime,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
runtimeRef.current = runtime;
|
|
56
|
+
}, [runtime, runtimeRef]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
60
|
+
{null}
|
|
61
|
+
</AssistantRuntimeProvider>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const waitForRemoteThread = async (
|
|
66
|
+
runtimeRef: RuntimeRef,
|
|
67
|
+
remoteId: string,
|
|
68
|
+
) => {
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(runtimeRef.current).not.toBeNull();
|
|
71
|
+
expect(runtimeRef.current!.threads.mainItem.getState().remoteId).toBe(
|
|
72
|
+
remoteId,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
describe("useRemoteThreadListRuntime controlled threadId", () => {
|
|
78
|
+
it("treats an initial empty string as a supplied thread ID", async () => {
|
|
79
|
+
const adapter = makeAdapter();
|
|
80
|
+
const onThreadIdChange = vi.fn();
|
|
81
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
82
|
+
|
|
83
|
+
render(
|
|
84
|
+
<ControlledRuntime
|
|
85
|
+
adapter={adapter}
|
|
86
|
+
threadId=""
|
|
87
|
+
onThreadIdChange={onThreadIdChange}
|
|
88
|
+
runtimeRef={runtimeRef}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await waitForRemoteThread(runtimeRef, "");
|
|
93
|
+
expect(adapter.fetch).toHaveBeenCalledWith("");
|
|
94
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("switches to an empty string supplied after mount", async () => {
|
|
98
|
+
const adapter = makeAdapter();
|
|
99
|
+
const onThreadIdChange = vi.fn();
|
|
100
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
101
|
+
|
|
102
|
+
const { rerender } = render(
|
|
103
|
+
<ControlledRuntime
|
|
104
|
+
adapter={adapter}
|
|
105
|
+
threadId="thread-a"
|
|
106
|
+
onThreadIdChange={onThreadIdChange}
|
|
107
|
+
runtimeRef={runtimeRef}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
await waitForRemoteThread(runtimeRef, "thread-a");
|
|
111
|
+
|
|
112
|
+
rerender(
|
|
113
|
+
<ControlledRuntime
|
|
114
|
+
adapter={adapter}
|
|
115
|
+
threadId=""
|
|
116
|
+
onThreadIdChange={onThreadIdChange}
|
|
117
|
+
runtimeRef={runtimeRef}
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
await waitForRemoteThread(runtimeRef, "");
|
|
121
|
+
expect(adapter.fetch).toHaveBeenLastCalledWith("");
|
|
122
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not echo prop-driven thread switches", async () => {
|
|
126
|
+
const adapter = makeAdapter();
|
|
127
|
+
const onThreadIdChange = vi.fn();
|
|
128
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
129
|
+
|
|
130
|
+
const { rerender } = render(
|
|
131
|
+
<ControlledRuntime
|
|
132
|
+
adapter={adapter}
|
|
133
|
+
threadId="thread-a"
|
|
134
|
+
onThreadIdChange={onThreadIdChange}
|
|
135
|
+
runtimeRef={runtimeRef}
|
|
136
|
+
/>,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await waitForRemoteThread(runtimeRef, "thread-a");
|
|
140
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
141
|
+
|
|
142
|
+
rerender(
|
|
143
|
+
<ControlledRuntime
|
|
144
|
+
adapter={adapter}
|
|
145
|
+
threadId="thread-b"
|
|
146
|
+
onThreadIdChange={onThreadIdChange}
|
|
147
|
+
runtimeRef={runtimeRef}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await waitForRemoteThread(runtimeRef, "thread-b");
|
|
152
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
153
|
+
|
|
154
|
+
const previousMainThreadId =
|
|
155
|
+
runtimeRef.current!.threads.getState().mainThreadId;
|
|
156
|
+
rerender(
|
|
157
|
+
<ControlledRuntime
|
|
158
|
+
adapter={adapter}
|
|
159
|
+
threadId={undefined}
|
|
160
|
+
onThreadIdChange={onThreadIdChange}
|
|
161
|
+
runtimeRef={runtimeRef}
|
|
162
|
+
/>,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
await waitFor(() => {
|
|
166
|
+
expect(runtimeRef.current!.threads.getState().mainThreadId).not.toBe(
|
|
167
|
+
previousMainThreadId,
|
|
168
|
+
);
|
|
169
|
+
expect(
|
|
170
|
+
runtimeRef.current!.threads.mainItem.getState().remoteId,
|
|
171
|
+
).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
expect(onThreadIdChange).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("still emits runtime-initiated thread switches", async () => {
|
|
177
|
+
const adapter = makeAdapter();
|
|
178
|
+
const onThreadIdChange = vi.fn();
|
|
179
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
180
|
+
|
|
181
|
+
render(
|
|
182
|
+
<ControlledRuntime
|
|
183
|
+
adapter={adapter}
|
|
184
|
+
threadId="thread-a"
|
|
185
|
+
onThreadIdChange={onThreadIdChange}
|
|
186
|
+
runtimeRef={runtimeRef}
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await waitForRemoteThread(runtimeRef, "thread-a");
|
|
191
|
+
onThreadIdChange.mockClear();
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await runtimeRef.current!.threads.switchToThread("thread-b");
|
|
195
|
+
});
|
|
196
|
+
expect(onThreadIdChange).toHaveBeenLastCalledWith("thread-b");
|
|
197
|
+
|
|
198
|
+
onThreadIdChange.mockClear();
|
|
199
|
+
await act(async () => {
|
|
200
|
+
await runtimeRef.current!.threads.switchToNewThread();
|
|
201
|
+
});
|
|
202
|
+
expect(onThreadIdChange).toHaveBeenLastCalledWith(undefined);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("does not retain suppression after an initial switch fails", async () => {
|
|
206
|
+
let threadAFetchCount = 0;
|
|
207
|
+
const adapter = makeAdapter({
|
|
208
|
+
fetch: vi.fn(async (threadId) => {
|
|
209
|
+
if (threadId === "thread-a" && threadAFetchCount++ === 0) {
|
|
210
|
+
throw new Error("initial fetch failed");
|
|
211
|
+
}
|
|
212
|
+
return makeThreadMetadata(threadId);
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
const onThreadIdChange = vi.fn();
|
|
216
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
217
|
+
|
|
218
|
+
render(
|
|
219
|
+
<ControlledRuntime
|
|
220
|
+
adapter={adapter}
|
|
221
|
+
threadId="thread-a"
|
|
222
|
+
onThreadIdChange={onThreadIdChange}
|
|
223
|
+
runtimeRef={runtimeRef}
|
|
224
|
+
/>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(adapter.fetch).toHaveBeenCalledWith("thread-a");
|
|
229
|
+
expect(runtimeRef.current).not.toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await act(async () => {
|
|
233
|
+
await runtimeRef.current!.threads.switchToThread("thread-b");
|
|
234
|
+
});
|
|
235
|
+
expect(onThreadIdChange).toHaveBeenLastCalledWith("thread-b");
|
|
236
|
+
|
|
237
|
+
onThreadIdChange.mockClear();
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await runtimeRef.current!.threads.switchToThread("thread-a");
|
|
240
|
+
});
|
|
241
|
+
expect(onThreadIdChange).toHaveBeenLastCalledWith("thread-a");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("emits a concurrent runtime switch to the controlled target", async () => {
|
|
245
|
+
const firstThreadBFetch = deferred<RemoteThreadMetadata>();
|
|
246
|
+
let threadBFetchCount = 0;
|
|
247
|
+
const adapter = makeAdapter({
|
|
248
|
+
fetch: vi.fn(async (threadId) => {
|
|
249
|
+
if (threadId === "thread-b" && threadBFetchCount++ === 0) {
|
|
250
|
+
return firstThreadBFetch.promise;
|
|
251
|
+
}
|
|
252
|
+
return makeThreadMetadata(threadId);
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
const onThreadIdChange = vi.fn();
|
|
256
|
+
const runtimeRef: RuntimeRef = { current: null };
|
|
257
|
+
|
|
258
|
+
const { rerender } = render(
|
|
259
|
+
<ControlledRuntime
|
|
260
|
+
adapter={adapter}
|
|
261
|
+
threadId="thread-a"
|
|
262
|
+
onThreadIdChange={onThreadIdChange}
|
|
263
|
+
runtimeRef={runtimeRef}
|
|
264
|
+
/>,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await waitForRemoteThread(runtimeRef, "thread-a");
|
|
268
|
+
onThreadIdChange.mockClear();
|
|
269
|
+
|
|
270
|
+
rerender(
|
|
271
|
+
<ControlledRuntime
|
|
272
|
+
adapter={adapter}
|
|
273
|
+
threadId="thread-b"
|
|
274
|
+
onThreadIdChange={onThreadIdChange}
|
|
275
|
+
runtimeRef={runtimeRef}
|
|
276
|
+
/>,
|
|
277
|
+
);
|
|
278
|
+
await waitFor(() => {
|
|
279
|
+
expect(adapter.fetch).toHaveBeenCalledWith("thread-b");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await act(async () => {
|
|
283
|
+
await runtimeRef.current!.threads.switchToThread("thread-b");
|
|
284
|
+
});
|
|
285
|
+
expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith("thread-b");
|
|
286
|
+
|
|
287
|
+
await act(async () => {
|
|
288
|
+
firstThreadBFetch.resolve(makeThreadMetadata("thread-b"));
|
|
289
|
+
await firstThreadBFetch.promise;
|
|
290
|
+
});
|
|
291
|
+
expect(onThreadIdChange).toHaveBeenCalledExactlyOnceWith("thread-b");
|
|
292
|
+
});
|
|
293
|
+
});
|