@assistant-ui/core 0.2.21 → 0.2.22
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/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/speech.d.ts.map +1 -1
- package/dist/adapters/speech.js +9 -2
- package/dist/adapters/speech.js.map +1 -1
- package/dist/adapters/voice.d.ts.map +1 -1
- package/dist/adapters/voice.js +13 -5
- package/dist/adapters/voice.js.map +1 -1
- package/dist/internal.d.ts +3 -1
- package/dist/internal.js +3 -1
- package/dist/model-context/frame/host.d.ts +1 -0
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +5 -0
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +1 -0
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/react/AssistantProvider.js.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
- package/dist/react/adapters/LocalStorageThreadListAdapter.js +77 -50
- package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/model-context/useInteractable.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +13 -13
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.d.ts.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +2 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +46 -36
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +18 -30
- package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
- package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
- package/dist/react/providers/PartByIndexProvider.js +18 -30
- package/dist/react/providers/PartByIndexProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +9 -2
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +49 -8
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +2 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
- package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -1
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.d.ts +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js +9 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
- package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.d.ts.map +1 -1
- package/dist/runtime/api/message-runtime.js +4 -4
- package/dist/runtime/api/message-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +2 -1
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +41 -14
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +2 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +4 -2
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +2 -2
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +4 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +5 -2
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +4 -4
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.js +22 -9
- package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +1 -2
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
- package/dist/store/clients/chain-of-thought-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/attachment-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-part-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/subscribable/subscribable.d.ts +1 -1
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js +11 -3
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts +2 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.js +5 -2
- package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
- package/dist/types/attachment.d.ts +2 -1
- package/dist/types/attachment.d.ts.map +1 -1
- package/dist/types/attachment.js +6 -0
- package/dist/types/attachment.js.map +1 -0
- package/dist/types/message.d.ts +1 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +9 -0
- package/dist/utils/data-url.d.ts.map +1 -0
- package/dist/utils/data-url.js +14 -0
- package/dist/utils/data-url.js.map +1 -0
- package/dist/utils/notify-event-listeners.d.ts +6 -0
- package/dist/utils/notify-event-listeners.d.ts.map +1 -0
- package/dist/utils/notify-event-listeners.js +16 -0
- package/dist/utils/notify-event-listeners.js.map +1 -0
- package/package.json +10 -9
- package/src/adapters/speech.test.ts +88 -0
- package/src/adapters/speech.ts +17 -2
- package/src/adapters/voice.test.ts +130 -0
- package/src/adapters/voice.ts +26 -6
- package/src/internal.ts +5 -0
- package/src/model-context/frame/host.test.ts +100 -0
- package/src/model-context/frame/host.ts +10 -0
- package/src/model-context/frame/provider.test.ts +83 -0
- package/src/model-context/frame/provider.ts +1 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +82 -0
- package/src/react/adapters/LocalStorageThreadListAdapter.tsx +94 -54
- package/src/react/primitive-hooks/useActionBarCopy.test.ts +79 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +4 -3
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -3
- package/src/react/primitives/message/MessageParts.tsx +48 -35
- package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +19 -2
- package/src/react/providers/PartByIndexProvider.tsx +16 -2
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +18 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +48 -7
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +69 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +1 -1
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +16 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +2 -1
- package/src/react/runtimes/external-message-converter.test.tsx +21 -0
- package/src/react/runtimes/external-message-converter.ts +1 -1
- package/src/react/runtimes/useLocalRuntime.test.tsx +56 -0
- package/src/react/runtimes/useLocalRuntime.ts +13 -1
- package/src/runtime/api/message-runtime.test.ts +81 -0
- package/src/runtime/api/message-runtime.ts +4 -6
- package/src/runtime/api/thread-list-item-runtime.ts +6 -1
- package/src/runtime/base/base-composer-runtime-core.ts +77 -31
- package/src/runtime/base/base-thread-runtime-core.ts +2 -1
- package/src/runtime/base/default-edit-composer-runtime-core.ts +4 -2
- package/src/runtime/base/default-thread-composer-runtime-core.ts +2 -2
- package/src/runtime/utils/thread-message-like.ts +1 -0
- package/src/runtimes/local/local-thread-runtime-core.test.ts +150 -1
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -2
- package/src/runtimes/remote-thread-list/optimistic-state.test.ts +63 -0
- package/src/runtimes/remote-thread-list/optimistic-state.ts +49 -17
- package/src/runtimes/remote-thread-list/types.ts +1 -1
- package/src/runtimes/tool-invocations/EDGE_CASES.md +13 -19
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +127 -6
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +4 -2
- package/src/subscribable/subscribable.test.ts +58 -0
- package/src/subscribable/subscribable.ts +13 -3
- package/src/tests/OptimisticState-list-race.test.ts +33 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-order.test.ts +223 -0
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +44 -0
- package/src/tests/base-composer-runtime-core-send.test.ts +433 -5
- package/src/tests/default-edit-composer-runtime-core.test.ts +43 -5
- package/src/tests/event-subscription-listener-errors.test.ts +205 -0
- package/src/tests/remote-thread-list-test-helpers.ts +14 -7
- package/src/tests/thread-message-like.test.ts +57 -0
- package/src/types/attachment.ts +5 -0
- package/src/types/message.ts +1 -0
- package/src/utils/data-url.test.ts +50 -0
- package/src/utils/data-url.ts +9 -0
- package/src/utils/notify-event-listeners.ts +30 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Unsubscribe } from "../types/unsubscribe";
|
|
2
|
+
import { notifyEventListeners } from "../utils/notify-event-listeners";
|
|
2
3
|
|
|
3
4
|
export const SKIP_UPDATE = Symbol("skip-update");
|
|
4
5
|
export type SKIP_UPDATE = typeof SKIP_UPDATE;
|
|
@@ -39,7 +40,10 @@ function shallowEqual<T extends object>(
|
|
|
39
40
|
if (objA === undefined) return false;
|
|
40
41
|
if (objB === undefined) return false;
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
const keysA = Object.keys(objA);
|
|
44
|
+
if (keysA.length !== Object.keys(objB).length) return false;
|
|
45
|
+
|
|
46
|
+
for (const key of keysA) {
|
|
43
47
|
const valueA = objA[key as keyof T];
|
|
44
48
|
const valueB = objB[key as keyof T];
|
|
45
49
|
if (!Object.is(valueA, valueB)) return false;
|
|
@@ -99,7 +103,12 @@ export abstract class BaseSubject {
|
|
|
99
103
|
|
|
100
104
|
protected abstract _connect(): Unsubscribe;
|
|
101
105
|
|
|
102
|
-
protected notifySubscribers(payload?: unknown) {
|
|
106
|
+
protected notifySubscribers(payload?: unknown, errorContext?: string) {
|
|
107
|
+
if (errorContext) {
|
|
108
|
+
notifyEventListeners(this._subscriptions, payload, errorContext);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
for (const callback of this._subscriptions) callback(payload);
|
|
104
113
|
}
|
|
105
114
|
|
|
@@ -273,8 +282,9 @@ export class EventSubscriptionSubject<
|
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
protected _connect(): Unsubscribe {
|
|
285
|
+
const errorContext = `Runtime event "${this.config.event}"`;
|
|
276
286
|
const callback = (payload?: unknown) => {
|
|
277
|
-
this.notifySubscribers(payload);
|
|
287
|
+
this.notifySubscribers(payload, errorContext);
|
|
278
288
|
};
|
|
279
289
|
|
|
280
290
|
let lastState = this.config.binding.getState();
|
|
@@ -79,6 +79,39 @@ const applyListResult = (
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
describe("list + delete race condition", () => {
|
|
82
|
+
it("stale list() does not re-add a thread deleted before list started", async () => {
|
|
83
|
+
const state = new OptimisticState<RemoteThreadState>(
|
|
84
|
+
applyListResult(EMPTY_STATE, {
|
|
85
|
+
threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
const deleteDeferred = deferred<void>();
|
|
89
|
+
const listDeferred = deferred<ListResult>();
|
|
90
|
+
|
|
91
|
+
const deletePromise = state.optimisticUpdate({
|
|
92
|
+
execute: () => deleteDeferred.promise,
|
|
93
|
+
optimistic: (s) => updateStatusReducer(s, "thread-A", "deleted"),
|
|
94
|
+
});
|
|
95
|
+
const listPromise = state.optimisticUpdate({
|
|
96
|
+
execute: () => listDeferred.promise,
|
|
97
|
+
loading: (s) => ({ ...s, isLoading: true }),
|
|
98
|
+
then: applyListResult,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
deleteDeferred.resolve();
|
|
102
|
+
await deletePromise;
|
|
103
|
+
|
|
104
|
+
listDeferred.resolve({
|
|
105
|
+
threads: [{ remoteId: "thread-A", status: "regular", title: "A" }],
|
|
106
|
+
});
|
|
107
|
+
await listPromise;
|
|
108
|
+
|
|
109
|
+
expect(state.value.threadIds).not.toContain("thread-A");
|
|
110
|
+
expect(
|
|
111
|
+
state.value.threadData[createThreadMappingId("thread-A")],
|
|
112
|
+
).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
82
115
|
it("stale list() does not re-add a thread deleted while list was in flight", async () => {
|
|
83
116
|
const state = new OptimisticState<RemoteThreadState>(EMPTY_STATE);
|
|
84
117
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { RemoteThreadMetadata } from "../runtimes/remote-thread-list/types";
|
|
3
|
+
import {
|
|
4
|
+
createCore,
|
|
5
|
+
deferred,
|
|
6
|
+
makeAdapter,
|
|
7
|
+
setStartThreadRuntime,
|
|
8
|
+
} from "./remote-thread-list-test-helpers";
|
|
9
|
+
|
|
10
|
+
const thread = (id: string): RemoteThreadMetadata => ({
|
|
11
|
+
status: "regular",
|
|
12
|
+
remoteId: id,
|
|
13
|
+
externalId: id,
|
|
14
|
+
title: id,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("RemoteThreadListThreadListRuntimeCore.switchToThread order", () => {
|
|
18
|
+
it("preserves the latest switch when an earlier fetch resolves last", async () => {
|
|
19
|
+
const fetchB = deferred<RemoteThreadMetadata>();
|
|
20
|
+
const fetchC = deferred<RemoteThreadMetadata>();
|
|
21
|
+
const adapter = makeAdapter({
|
|
22
|
+
fetch: vi.fn((id: string) =>
|
|
23
|
+
id === "thread-b" ? fetchB.promise : fetchC.promise,
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
const core = createCore(adapter);
|
|
27
|
+
|
|
28
|
+
const switchToB = core.switchToThread("thread-b");
|
|
29
|
+
const switchToC = core.switchToThread("thread-c");
|
|
30
|
+
|
|
31
|
+
fetchC.resolve(thread("thread-c"));
|
|
32
|
+
await switchToC;
|
|
33
|
+
fetchB.resolve(thread("thread-b"));
|
|
34
|
+
await switchToB;
|
|
35
|
+
|
|
36
|
+
expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("preserves the latest switch when an earlier runtime starts last", async () => {
|
|
40
|
+
const runtimeB = deferred<unknown>();
|
|
41
|
+
const runtimeC = deferred<unknown>();
|
|
42
|
+
const adapter = makeAdapter({
|
|
43
|
+
list: vi.fn(async () => ({
|
|
44
|
+
threads: [thread("thread-b"), thread("thread-c")],
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
const core = createCore(adapter);
|
|
48
|
+
await core.getLoadThreadsPromise();
|
|
49
|
+
|
|
50
|
+
setStartThreadRuntime(core, (id) =>
|
|
51
|
+
id === core.getItemById("thread-b")?.id
|
|
52
|
+
? runtimeB.promise
|
|
53
|
+
: runtimeC.promise,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const switchToB = core.switchToThread("thread-b");
|
|
57
|
+
const switchToC = core.switchToThread("thread-c");
|
|
58
|
+
|
|
59
|
+
runtimeC.resolve({});
|
|
60
|
+
await switchToC;
|
|
61
|
+
runtimeB.resolve({});
|
|
62
|
+
await switchToB;
|
|
63
|
+
|
|
64
|
+
expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("invalidates an earlier switch while a new thread request is pending", async () => {
|
|
68
|
+
const initializeThread = deferred<{
|
|
69
|
+
remoteId: string;
|
|
70
|
+
externalId: string;
|
|
71
|
+
}>();
|
|
72
|
+
const runtimeB = deferred<unknown>();
|
|
73
|
+
const adapter = makeAdapter({
|
|
74
|
+
initialize: vi.fn(() => initializeThread.promise),
|
|
75
|
+
list: vi.fn(async () => ({ threads: [thread("thread-b")] })),
|
|
76
|
+
});
|
|
77
|
+
const core = createCore(adapter);
|
|
78
|
+
await core.getLoadThreadsPromise();
|
|
79
|
+
|
|
80
|
+
const initialNewThreadId = core.newThreadId;
|
|
81
|
+
if (!initialNewThreadId) throw new Error("Expected an initial new thread");
|
|
82
|
+
|
|
83
|
+
const threadBId = core.getItemById("thread-b")?.id;
|
|
84
|
+
setStartThreadRuntime(core, (id) =>
|
|
85
|
+
id === threadBId ? runtimeB.promise : Promise.resolve({}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const initialize = core.initialize(initialNewThreadId);
|
|
89
|
+
const switchToB = core.switchToThread("thread-b");
|
|
90
|
+
const switchToNewThread = core.switchToNewThread();
|
|
91
|
+
|
|
92
|
+
runtimeB.resolve({});
|
|
93
|
+
await switchToB;
|
|
94
|
+
|
|
95
|
+
expect(core.mainThreadId).not.toBe(threadBId);
|
|
96
|
+
|
|
97
|
+
initializeThread.resolve({
|
|
98
|
+
remoteId: "initialized-thread",
|
|
99
|
+
externalId: "initialized-thread",
|
|
100
|
+
});
|
|
101
|
+
await initialize;
|
|
102
|
+
await switchToNewThread;
|
|
103
|
+
|
|
104
|
+
expect(core.mainThreadId).toBe(core.getItemById(core.newThreadId!)?.id);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not let a pending new thread request override a later switch", async () => {
|
|
108
|
+
const initializeThread = deferred<{
|
|
109
|
+
remoteId: string;
|
|
110
|
+
externalId: string;
|
|
111
|
+
}>();
|
|
112
|
+
const adapter = makeAdapter({
|
|
113
|
+
initialize: vi.fn(() => initializeThread.promise),
|
|
114
|
+
list: vi.fn(async () => ({ threads: [thread("thread-c")] })),
|
|
115
|
+
});
|
|
116
|
+
const core = createCore(adapter);
|
|
117
|
+
await core.getLoadThreadsPromise();
|
|
118
|
+
|
|
119
|
+
const initialNewThreadId = core.newThreadId;
|
|
120
|
+
if (!initialNewThreadId) throw new Error("Expected an initial new thread");
|
|
121
|
+
|
|
122
|
+
const initialize = core.initialize(initialNewThreadId);
|
|
123
|
+
const switchToNewThread = core.switchToNewThread();
|
|
124
|
+
await core.switchToThread("thread-c");
|
|
125
|
+
|
|
126
|
+
initializeThread.resolve({
|
|
127
|
+
remoteId: "initialized-thread",
|
|
128
|
+
externalId: "initialized-thread",
|
|
129
|
+
});
|
|
130
|
+
await initialize;
|
|
131
|
+
await switchToNewThread;
|
|
132
|
+
|
|
133
|
+
expect(core.mainThreadId).toBe(core.getItemById("thread-c")?.id);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("keeps mutations waiting when a newer switch fails", async () => {
|
|
137
|
+
const fetchB = deferred<RemoteThreadMetadata>();
|
|
138
|
+
const newThreadRuntime = deferred<unknown>();
|
|
139
|
+
const adapter = makeAdapter({
|
|
140
|
+
fetch: vi.fn(() => fetchB.promise),
|
|
141
|
+
list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
|
|
142
|
+
});
|
|
143
|
+
const core = createCore(adapter);
|
|
144
|
+
await core.getLoadThreadsPromise();
|
|
145
|
+
await core.switchToThread("thread-a");
|
|
146
|
+
|
|
147
|
+
const threadAId = core.getItemById("thread-a")?.id;
|
|
148
|
+
const newThreadId = core.newThreadId;
|
|
149
|
+
if (!newThreadId) throw new Error("Expected an initial new thread");
|
|
150
|
+
|
|
151
|
+
setStartThreadRuntime(core, (id) =>
|
|
152
|
+
id === core.getItemById(newThreadId)?.id
|
|
153
|
+
? newThreadRuntime.promise
|
|
154
|
+
: Promise.resolve({}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const deleteA = core.delete("thread-a");
|
|
158
|
+
const switchToB = core.switchToThread("thread-b");
|
|
159
|
+
const switchToBResult = expect(switchToB).rejects.toThrow("fetch failed");
|
|
160
|
+
|
|
161
|
+
newThreadRuntime.resolve({});
|
|
162
|
+
fetchB.reject(new Error("fetch failed"));
|
|
163
|
+
|
|
164
|
+
await switchToBResult;
|
|
165
|
+
await deleteA;
|
|
166
|
+
|
|
167
|
+
expect(core.mainThreadId).not.toBe(threadAId);
|
|
168
|
+
expect(core.mainThreadId).toBe(core.getItemById(core.newThreadId!)?.id);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("lets a newer successful switch move the mutation target away", async () => {
|
|
172
|
+
const fetchB = deferred<RemoteThreadMetadata>();
|
|
173
|
+
const newThreadRuntime = deferred<unknown>();
|
|
174
|
+
const adapter = makeAdapter({
|
|
175
|
+
fetch: vi.fn(() => fetchB.promise),
|
|
176
|
+
list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
|
|
177
|
+
});
|
|
178
|
+
const core = createCore(adapter);
|
|
179
|
+
await core.getLoadThreadsPromise();
|
|
180
|
+
await core.switchToThread("thread-a");
|
|
181
|
+
|
|
182
|
+
const newThreadId = core.newThreadId;
|
|
183
|
+
if (!newThreadId) throw new Error("Expected an initial new thread");
|
|
184
|
+
|
|
185
|
+
setStartThreadRuntime(core, (id) =>
|
|
186
|
+
id === core.getItemById(newThreadId)?.id
|
|
187
|
+
? newThreadRuntime.promise
|
|
188
|
+
: Promise.resolve({}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const deleteA = core.delete("thread-a");
|
|
192
|
+
const switchToB = core.switchToThread("thread-b");
|
|
193
|
+
|
|
194
|
+
newThreadRuntime.resolve({});
|
|
195
|
+
fetchB.resolve(thread("thread-b"));
|
|
196
|
+
|
|
197
|
+
await switchToB;
|
|
198
|
+
await deleteA;
|
|
199
|
+
|
|
200
|
+
expect(core.mainThreadId).toBe(core.getItemById("thread-b")?.id);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("does not let a mutation cancel an existing pending switch", async () => {
|
|
204
|
+
const fetchB = deferred<RemoteThreadMetadata>();
|
|
205
|
+
const adapter = makeAdapter({
|
|
206
|
+
fetch: vi.fn(() => fetchB.promise),
|
|
207
|
+
list: vi.fn(async () => ({ threads: [thread("thread-a")] })),
|
|
208
|
+
});
|
|
209
|
+
const core = createCore(adapter);
|
|
210
|
+
await core.getLoadThreadsPromise();
|
|
211
|
+
await core.switchToThread("thread-a");
|
|
212
|
+
|
|
213
|
+
const switchToB = core.switchToThread("thread-b");
|
|
214
|
+
const deleteA = core.delete("thread-a");
|
|
215
|
+
|
|
216
|
+
fetchB.resolve(thread("thread-b"));
|
|
217
|
+
|
|
218
|
+
await switchToB;
|
|
219
|
+
await deleteA;
|
|
220
|
+
|
|
221
|
+
expect(core.mainThreadId).toBe(core.getItemById("thread-b")?.id);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -128,6 +128,50 @@ describe("BaseComposerRuntimeCore.addAttachment error events", () => {
|
|
|
128
128
|
expect(onError).not.toHaveBeenCalled();
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
it("adds prepared attachments when File is unavailable", async () => {
|
|
132
|
+
const composer = makeComposer();
|
|
133
|
+
const fileConstructor = globalThis.File;
|
|
134
|
+
vi.stubGlobal("File", undefined);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
await composer.addAttachment({
|
|
138
|
+
name: "notes.txt",
|
|
139
|
+
contentType: "text/plain",
|
|
140
|
+
content: [{ type: "text", text: "hello" }],
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
vi.stubGlobal("File", fileConstructor);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
expect(composer.attachments[0]).toMatchObject({
|
|
147
|
+
type: "document",
|
|
148
|
+
name: "notes.txt",
|
|
149
|
+
contentType: "text/plain",
|
|
150
|
+
content: [{ type: "text", text: "hello" }],
|
|
151
|
+
status: { type: "complete" },
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("uses the adapter for foreign files that expose content", async () => {
|
|
156
|
+
const add = vi.fn(makeAdapter().add);
|
|
157
|
+
const composer = makeComposer(makeAdapter({ add }));
|
|
158
|
+
const foreignFile = {
|
|
159
|
+
name: "photo.png",
|
|
160
|
+
type: "image/png",
|
|
161
|
+
lastModified: 0,
|
|
162
|
+
content: [{ type: "text", text: "implementation detail" }],
|
|
163
|
+
} as File;
|
|
164
|
+
|
|
165
|
+
await composer.addAttachment(foreignFile);
|
|
166
|
+
|
|
167
|
+
expect(add).toHaveBeenCalledWith({ file: foreignFile });
|
|
168
|
+
expect(composer.attachments[0]).toMatchObject({
|
|
169
|
+
type: "image",
|
|
170
|
+
name: "photo.png",
|
|
171
|
+
contentType: "image/png",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
131
175
|
it("accepts JSON files with the application/json media type", async () => {
|
|
132
176
|
const composer = makeComposer(new SimpleTextAttachmentAdapter());
|
|
133
177
|
|