@assistant-ui/core 0.3.1 → 0.3.2
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/internal.d.ts +1 -2
- package/dist/internal.js +1 -2
- 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 +2 -2
- 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.js +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/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/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.js +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.js +2 -2
- 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/utils/data-url.js +2 -2
- package/dist/utils/data-url.js.map +1 -1
- package/package.json +4 -4
- package/src/internal.ts +0 -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/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/useRemoteThreadListRuntime.ts +9 -15
- package/src/runtime/utils/thread-message-like.ts +2 -2
- package/src/runtimes/remote-thread-list/types.ts +5 -4
- package/src/store/scopes/thread.ts +1 -1
- 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/utils/data-url.test.ts +26 -2
- package/src/utils/data-url.ts +2 -2
|
@@ -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
|
+
});
|
|
@@ -20,8 +20,32 @@ describe("parseDataUrl", () => {
|
|
|
20
20
|
expect(parseDataUrl("data:text/plain,hello")).toBeNull();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("
|
|
24
|
-
expect(parseDataUrl("data:image/png;base64,")).
|
|
23
|
+
it("parses a data URL with an empty payload as zero-byte data", () => {
|
|
24
|
+
expect(parseDataUrl("data:image/png;base64,")).toEqual({
|
|
25
|
+
mimeType: "image/png",
|
|
26
|
+
data: "",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("parses an uppercase scheme", () => {
|
|
31
|
+
expect(parseDataUrl("DATA:image/png;base64,aGVsbG8=")).toEqual({
|
|
32
|
+
mimeType: "image/png",
|
|
33
|
+
data: "aGVsbG8=",
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("parses a mixed-case scheme and base64 token", () => {
|
|
38
|
+
expect(parseDataUrl("Data:image/png;Base64,aGVsbG8=")).toEqual({
|
|
39
|
+
mimeType: "image/png",
|
|
40
|
+
data: "aGVsbG8=",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("lowercases the captured mime type", () => {
|
|
45
|
+
expect(parseDataUrl("data:IMAGE/PNG;base64,aGVsbG8=")).toEqual({
|
|
46
|
+
mimeType: "image/png",
|
|
47
|
+
data: "aGVsbG8=",
|
|
48
|
+
});
|
|
25
49
|
});
|
|
26
50
|
|
|
27
51
|
it("returns null for plain strings", () => {
|
package/src/utils/data-url.ts
CHANGED
|
@@ -3,7 +3,7 @@ export const httpUrlPattern = /^https?:\/\//i;
|
|
|
3
3
|
export function parseDataUrl(
|
|
4
4
|
value: string,
|
|
5
5
|
): { mimeType: string; data: string } | null {
|
|
6
|
-
const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(
|
|
6
|
+
const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.*)$/i);
|
|
7
7
|
if (!match) return null;
|
|
8
|
-
return { mimeType: match[1]
|
|
8
|
+
return { mimeType: match[1]!.toLowerCase(), data: match[2]! };
|
|
9
9
|
}
|