@assistant-ui/core 0.3.17 → 0.3.18
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/attachment.js +5 -4
- package/dist/adapters/attachment.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.js +3 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +10 -4
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +2 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +30 -7
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +16 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/registry.js +3 -5
- package/dist/model-context/registry.js.map +1 -1
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +5 -4
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/react/client/InMemoryThreadList.js +62 -56
- package/dist/react/client/InMemoryThreadList.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +24 -27
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -0
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +151 -141
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +18 -16
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/interactables-legacy/Interactables.js +242 -121
- package/dist/react/interactables-legacy/Interactables.js.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
- package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
- package/dist/react/model-context/makeAssistantTool.js +1 -1
- package/dist/react/model-context/makeAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantTool.d.ts +2 -2
- package/dist/react/model-context/useAssistantTool.js +1 -1
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +2 -1
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +13 -11
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
- package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +2 -2
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +18 -14
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +4 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +1 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +16 -12
- 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 +6 -5
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +1 -1
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +1 -0
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +15 -6
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +1 -1
- package/dist/runtime/utils/thread-message-like.js.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-list-runtime-core.js +21 -27
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +2 -2
- package/dist/runtimes/internal.js +2 -2
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
- package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
- package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
- package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
- package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/title.js +9 -1
- package/dist/runtimes/remote-thread-list/title.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +19 -3
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/model-context-client.d.ts.map +1 -1
- package/dist/store/clients/model-context-client.js +2 -1
- package/dist/store/clients/model-context-client.js.map +1 -1
- package/dist/store/clients/single-thread-list.d.ts.map +1 -1
- package/dist/store/clients/single-thread-list.js +1 -0
- package/dist/store/clients/single-thread-list.js.map +1 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +5 -4
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +221 -144
- package/dist/store/runtime-clients/message-runtime-client.js.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-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +193 -121
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +10 -2
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/message.d.ts +30 -1
- package/dist/store/scopes/message.d.ts.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +2 -2
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +8 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.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 +2 -1
- package/dist/types/attachment.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts +19 -0
- package/dist/utils/abortable-iterable.d.ts.map +1 -0
- package/dist/utils/abortable-iterable.js +72 -0
- package/dist/utils/abortable-iterable.js.map +1 -0
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/composite-context-provider.js +20 -2
- package/dist/utils/composite-context-provider.js.map +1 -1
- package/dist/utils/record.d.ts +11 -0
- package/dist/utils/record.d.ts.map +1 -0
- package/dist/utils/record.js +12 -0
- package/dist/utils/record.js.map +1 -0
- package/package.json +10 -10
- package/src/adapters/attachment.test.ts +50 -1
- package/src/adapters/attachment.ts +7 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +4 -0
- package/src/model-context/frame/host.test.ts +24 -0
- package/src/model-context/frame/host.ts +10 -4
- package/src/model-context/frame/provider.test.ts +71 -0
- package/src/model-context/frame/provider.ts +37 -7
- package/src/model-context/interactable-composer-metadata.test.ts +28 -0
- package/src/model-context/interactable-composer-metadata.ts +9 -3
- package/src/model-context/registry.test.ts +12 -0
- package/src/model-context/registry.ts +3 -2
- package/src/model-context/types.test.ts +25 -0
- package/src/model-context/types.ts +9 -4
- package/src/react/RuntimeAdapter.test.ts +34 -0
- package/src/react/client/InMemoryThreadList.ts +1 -0
- package/src/react/client/Interactables.test.ts +28 -0
- package/src/react/client/Interactables.ts +15 -13
- package/src/react/client/RemoteThreadList.background.test.ts +148 -6
- package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
- package/src/react/client/RemoteThreadList.test.ts +345 -3
- package/src/react/client/RemoteThreadList.ts +166 -110
- package/src/react/client/Tools.ts +22 -11
- package/src/react/interactables-legacy/Interactables.ts +16 -16
- package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
- package/src/react/model-context/makeAssistantTool.ts +2 -2
- package/src/react/model-context/useAssistantTool.ts +2 -2
- package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
- package/src/react/model-context/useToolArgsStatus.ts +2 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
- package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
- package/src/react/runtimes/RemoteThreadResource.ts +19 -12
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
- package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
- package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
- package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
- package/src/runtime/api/message-runtime.test.ts +75 -0
- package/src/runtime/api/thread-list-item-runtime.ts +10 -3
- package/src/runtime/api/thread-list-runtime.ts +21 -11
- package/src/runtime/api/thread-runtime.test.ts +78 -0
- package/src/runtime/api/thread-runtime.ts +8 -1
- package/src/runtime/base/base-composer-runtime-core.ts +36 -8
- package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
- package/src/runtime/base/base-thread-runtime-core.ts +6 -4
- package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
- package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
- package/src/runtime/utils/external-message-conversion.test.ts +68 -0
- package/src/runtime/utils/external-message-conversion.ts +1 -1
- package/src/runtime/utils/message-repository.ts +29 -17
- package/src/runtime/utils/thread-message-like.ts +1 -1
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
- package/src/runtimes/internal.ts +1 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
- package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
- package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
- package/src/runtimes/remote-thread-list/title.test.ts +62 -0
- package/src/runtimes/remote-thread-list/title.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +10 -0
- package/src/store/clients/external-thread.ts +48 -3
- package/src/store/clients/model-context-client.ts +5 -1
- package/src/store/clients/single-thread-list.ts +1 -0
- package/src/store/clients/suggestions.test.ts +40 -0
- package/src/store/clients/suggestions.ts +13 -10
- package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
- package/src/store/runtime-clients/message-runtime-client.ts +54 -6
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
- package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
- package/src/store/scopes/composer.ts +16 -4
- package/src/store/scopes/message.ts +14 -0
- package/src/store/scopes/thread-list-item.ts +5 -2
- package/src/store/scopes/thread.ts +4 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/MessageRepository.test.ts +116 -0
- package/src/tests/OptimisticState-list-race.test.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
- package/src/tests/attachment-adapters.test.ts +14 -0
- package/src/tests/base-composer-runtime-core.test.ts +39 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
- package/src/tests/external-thread-attachments.test.tsx +139 -0
- package/src/tests/external-thread-parity.test.tsx +23 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
- package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
- package/src/tests/thread-switch-events.test.tsx +78 -0
- package/src/tests/tools-scope-migration.test.tsx +28 -0
- package/src/types/attachment.ts +4 -0
- package/src/utils/abortable-iterable.test.ts +213 -0
- package/src/utils/abortable-iterable.ts +93 -0
- package/src/utils/composite-context-provider.test.ts +25 -0
- package/src/utils/composite-context-provider.ts +23 -2
- package/src/utils/record.test.ts +48 -0
- package/src/utils/record.ts +11 -0
|
@@ -113,6 +113,52 @@ describe("RemoteThreadListThreadListRuntimeCore load race", () => {
|
|
|
113
113
|
expect(core.threadIds).toEqual([]);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it("keeps the initialized runtime when its listed duplicate is selected", async () => {
|
|
117
|
+
const listDeferred = deferred<ListResult>();
|
|
118
|
+
const initializeDeferred = deferred<{
|
|
119
|
+
remoteId: string;
|
|
120
|
+
externalId: undefined;
|
|
121
|
+
}>();
|
|
122
|
+
const adapter = makeAdapter({
|
|
123
|
+
list: vi.fn(() => listDeferred.promise),
|
|
124
|
+
initialize: vi.fn(() => initializeDeferred.promise),
|
|
125
|
+
});
|
|
126
|
+
const core = createCore(adapter);
|
|
127
|
+
const hookManager = (
|
|
128
|
+
core as unknown as {
|
|
129
|
+
_hookManager: { stopThreadRuntime: (threadId: string) => void };
|
|
130
|
+
}
|
|
131
|
+
)._hookManager;
|
|
132
|
+
const stopThreadRuntime = vi.spyOn(hookManager, "stopThreadRuntime");
|
|
133
|
+
|
|
134
|
+
const loadPromise = core.getLoadThreadsPromise();
|
|
135
|
+
await core.switchToNewThread();
|
|
136
|
+
const localId = core.newThreadId!;
|
|
137
|
+
const initializePromise = core.initialize(localId);
|
|
138
|
+
|
|
139
|
+
listDeferred.resolve({
|
|
140
|
+
threads: [
|
|
141
|
+
{ status: "regular", remoteId: "remote-1", externalId: "remote-1" },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
await loadPromise;
|
|
145
|
+
await core.switchToThread("remote-1");
|
|
146
|
+
|
|
147
|
+
initializeDeferred.resolve({
|
|
148
|
+
remoteId: "remote-1",
|
|
149
|
+
externalId: undefined,
|
|
150
|
+
});
|
|
151
|
+
await initializePromise;
|
|
152
|
+
|
|
153
|
+
expect(core.mainThreadId).toBe(localId);
|
|
154
|
+
expect(core.threadIds).toEqual([localId]);
|
|
155
|
+
expect(Object.keys(core.threadItems)).toEqual([localId]);
|
|
156
|
+
expect(core.getItemById("remote-1")?.id).toBe(localId);
|
|
157
|
+
expect(core.getItemById("remote-1")?.externalId).toBe("remote-1");
|
|
158
|
+
expect(stopThreadRuntime).toHaveBeenCalledWith("remote-1");
|
|
159
|
+
expect(stopThreadRuntime).not.toHaveBeenCalledWith(localId);
|
|
160
|
+
});
|
|
161
|
+
|
|
116
162
|
it("does not leave the collapsed slot in both lists when the race reported it archived", async () => {
|
|
117
163
|
const listDeferred = deferred<ListResult>();
|
|
118
164
|
const initializeDeferred = deferred<{
|
|
@@ -160,6 +206,7 @@ describe("preserveMidLoadTransitions", () => {
|
|
|
160
206
|
lists: { threadIds?: string[]; archivedThreadIds?: string[] } = {},
|
|
161
207
|
): RemoteThreadState => ({
|
|
162
208
|
isLoading: false,
|
|
209
|
+
loadError: undefined,
|
|
163
210
|
isLoadingMore: false,
|
|
164
211
|
cursor: undefined,
|
|
165
212
|
newThreadId: undefined,
|
|
@@ -6,6 +6,95 @@ import {
|
|
|
6
6
|
} from "./remote-thread-list-test-helpers";
|
|
7
7
|
|
|
8
8
|
describe("RemoteThreadListThreadListRuntimeCore title generation", () => {
|
|
9
|
+
it("preserves an existing title when generation returns no title", async () => {
|
|
10
|
+
const adapter = makeAdapter({
|
|
11
|
+
list: vi.fn(async () => ({
|
|
12
|
+
threads: [
|
|
13
|
+
{
|
|
14
|
+
status: "regular" as const,
|
|
15
|
+
remoteId: "thread-1",
|
|
16
|
+
externalId: "thread-1",
|
|
17
|
+
title: "Existing title",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
})),
|
|
21
|
+
});
|
|
22
|
+
const core = createCore(adapter);
|
|
23
|
+
await core.getLoadThreadsPromise();
|
|
24
|
+
|
|
25
|
+
const internals = core as unknown as {
|
|
26
|
+
_hookManager: { getThreadRuntimeCore: () => { messages: never[] } };
|
|
27
|
+
};
|
|
28
|
+
internals._hookManager.getThreadRuntimeCore = () => ({ messages: [] });
|
|
29
|
+
|
|
30
|
+
await core.generateTitle("thread-1");
|
|
31
|
+
|
|
32
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
33
|
+
expect(core.getItemById("thread-1")?.title).toBe("Existing title");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps a manual rename made during automatic title generation", async () => {
|
|
37
|
+
const generatedTitle = deferred<ReadableStream>();
|
|
38
|
+
const adapter = makeAdapter({
|
|
39
|
+
list: vi.fn(async () => ({
|
|
40
|
+
threads: [
|
|
41
|
+
{
|
|
42
|
+
status: "regular" as const,
|
|
43
|
+
remoteId: "thread-1",
|
|
44
|
+
externalId: "thread-1",
|
|
45
|
+
title: "New chat",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
})),
|
|
49
|
+
generateTitle: vi.fn(async () => generatedTitle.promise as never),
|
|
50
|
+
});
|
|
51
|
+
const core = createCore(adapter);
|
|
52
|
+
await core.getLoadThreadsPromise();
|
|
53
|
+
|
|
54
|
+
const internals = core as unknown as {
|
|
55
|
+
_hookManager: { getThreadRuntimeCore: () => { messages: never[] } };
|
|
56
|
+
};
|
|
57
|
+
internals._hookManager.getThreadRuntimeCore = () => ({ messages: [] });
|
|
58
|
+
|
|
59
|
+
const generation = core.generateTitle("thread-1", { automatic: true });
|
|
60
|
+
await vi.waitFor(() => {
|
|
61
|
+
expect(adapter.generateTitle).toHaveBeenCalledOnce();
|
|
62
|
+
});
|
|
63
|
+
await core.rename("thread-1", "Manual title");
|
|
64
|
+
|
|
65
|
+
generatedTitle.resolve(
|
|
66
|
+
new ReadableStream({
|
|
67
|
+
start(controller) {
|
|
68
|
+
controller.enqueue({
|
|
69
|
+
type: "part-start",
|
|
70
|
+
path: [0],
|
|
71
|
+
part: { type: "text" },
|
|
72
|
+
});
|
|
73
|
+
controller.enqueue({
|
|
74
|
+
type: "text-delta",
|
|
75
|
+
path: [0],
|
|
76
|
+
textDelta: "Generated title",
|
|
77
|
+
});
|
|
78
|
+
controller.enqueue({ type: "part-finish", path: [0] });
|
|
79
|
+
controller.close();
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
await generation;
|
|
84
|
+
|
|
85
|
+
expect(core.getItemById("thread-1")?.title).toBe("Manual title");
|
|
86
|
+
expect(adapter.rename).toHaveBeenNthCalledWith(
|
|
87
|
+
1,
|
|
88
|
+
"thread-1",
|
|
89
|
+
"Manual title",
|
|
90
|
+
);
|
|
91
|
+
expect(adapter.rename).toHaveBeenNthCalledWith(
|
|
92
|
+
2,
|
|
93
|
+
"thread-1",
|
|
94
|
+
"Manual title",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
9
98
|
it("keeps a generated title when an earlier rename completes during another update", async () => {
|
|
10
99
|
const reloadRequest = deferred<{
|
|
11
100
|
threads: {
|
|
@@ -8,6 +8,20 @@ import {
|
|
|
8
8
|
} from "../adapters/attachment";
|
|
9
9
|
|
|
10
10
|
describe("fileMatchesAccept", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
["BACKUP.TAR.GZ", ".tar.gz"],
|
|
13
|
+
[".env", ".env"],
|
|
14
|
+
])("accepts %s for %s", (name, accept) => {
|
|
15
|
+
expect(fileMatchesAccept({ name, type: "" }, accept)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it.each([
|
|
19
|
+
["png", ".png"],
|
|
20
|
+
["foo.mjs", ".js"],
|
|
21
|
+
])("rejects %s for %s", (name, accept) => {
|
|
22
|
+
expect(fileMatchesAccept({ name, type: "" }, accept)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
11
25
|
it("matches MIME types with parameters", () => {
|
|
12
26
|
expect(
|
|
13
27
|
fileMatchesAccept(
|
|
@@ -103,6 +103,45 @@ describe("BaseComposerRuntimeCore", () => {
|
|
|
103
103
|
expect(listener).not.toHaveBeenCalled();
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
it("reset keeps discarded text out of later dictation results", async () => {
|
|
107
|
+
let emitSpeech!: (result: DictationAdapter.Result) => void;
|
|
108
|
+
composer.setDictationAdapter({
|
|
109
|
+
listen: () => ({
|
|
110
|
+
status: { type: "running" },
|
|
111
|
+
stop: async () => {},
|
|
112
|
+
cancel: () => {},
|
|
113
|
+
onSpeechStart: () => () => {},
|
|
114
|
+
onSpeechEnd: () => () => {},
|
|
115
|
+
onSpeech: (callback) => {
|
|
116
|
+
emitSpeech = callback;
|
|
117
|
+
return () => {};
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
composer.setText("old");
|
|
123
|
+
composer.startDictation();
|
|
124
|
+
try {
|
|
125
|
+
await composer.reset();
|
|
126
|
+
expect(composer.text).toBe("");
|
|
127
|
+
emitSpeech({ transcript: "new", isFinal: true });
|
|
128
|
+
expect(composer.text).toBe("new");
|
|
129
|
+
|
|
130
|
+
emitSpeech({ transcript: "pending", isFinal: false });
|
|
131
|
+
expect(composer.text).toBe("new pending");
|
|
132
|
+
await composer.reset();
|
|
133
|
+
expect(composer.text).toBe("");
|
|
134
|
+
expect(composer.dictation?.transcript).toBeUndefined();
|
|
135
|
+
|
|
136
|
+
emitSpeech({ transcript: "next", isFinal: false });
|
|
137
|
+
expect(composer.text).toBe("next");
|
|
138
|
+
emitSpeech({ transcript: "next final", isFinal: true });
|
|
139
|
+
expect(composer.text).toBe("next final");
|
|
140
|
+
} finally {
|
|
141
|
+
composer.stopDictation();
|
|
142
|
+
await Promise.resolve();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
106
145
|
it("send includes quote in metadata and clears it", async () => {
|
|
107
146
|
const quote = { text: "quoted", messageId: "m1" };
|
|
108
147
|
composer.setText("reply");
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { ExternalStoreThreadListRuntimeCore } from "../runtimes/external-store/external-store-thread-list-runtime-core";
|
|
3
3
|
import type { ExternalStoreThreadRuntimeCore } from "../runtimes/external-store/external-store-thread-runtime-core";
|
|
4
4
|
import { ExternalStoreThreadRuntimeCore as ExternalStoreThreadRuntimeCoreImpl } from "../runtimes/external-store/external-store-thread-runtime-core";
|
|
5
|
+
import { ExternalStoreRuntimeCore } from "../runtimes/external-store/external-store-runtime-core";
|
|
5
6
|
import type { ExternalStoreThreadListAdapter } from "../runtimes/external-store/external-store-adapter";
|
|
6
7
|
import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
|
|
7
8
|
import type { ModelContextProvider } from "../model-context/types";
|
|
@@ -109,10 +110,56 @@ describe("ExternalStoreThreadListRuntimeCore - construction", () => {
|
|
|
109
110
|
// Two empty-adapter constructions should share the frozen DEFAULT_THREAD_DATA
|
|
110
111
|
// singleton rather than each getting a fresh `{ ... }` clone.
|
|
111
112
|
expect(a.threadItems).toBe(b.threadItems);
|
|
113
|
+
expect(Object.getPrototypeOf(a.threadItems)).toBeNull();
|
|
112
114
|
});
|
|
115
|
+
|
|
116
|
+
it.each(["__proto__", "constructor", "toString"])(
|
|
117
|
+
"returns undefined for an absent prototype-named thread id %s",
|
|
118
|
+
(threadId) => {
|
|
119
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
120
|
+
makeAdapter(),
|
|
121
|
+
makeFactory(),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(core.getItemById(threadId)).toBeUndefined();
|
|
125
|
+
expect(Object.hasOwn(core.threadItems, threadId)).toBe(false);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
113
128
|
});
|
|
114
129
|
|
|
115
130
|
describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
|
|
131
|
+
it("updates subscribed loading state when thread ids and arrays stay unchanged", () => {
|
|
132
|
+
const threadList = makeAdapter({
|
|
133
|
+
threadId: "thread-alpha",
|
|
134
|
+
threads: [{ id: "thread-alpha", status: "regular" }],
|
|
135
|
+
archivedThreads: [],
|
|
136
|
+
isLoading: true,
|
|
137
|
+
});
|
|
138
|
+
const adapter: ExternalStoreAdapter = {
|
|
139
|
+
messages: [],
|
|
140
|
+
onNew: async () => {},
|
|
141
|
+
adapters: { threadList },
|
|
142
|
+
};
|
|
143
|
+
const core = new ExternalStoreRuntimeCore(adapter);
|
|
144
|
+
const runtime = new ThreadListRuntimeImpl(core.threads);
|
|
145
|
+
const seen: boolean[] = [];
|
|
146
|
+
const unsubscribe = runtime.subscribe(() => {
|
|
147
|
+
seen.push(runtime.getState().isLoading);
|
|
148
|
+
});
|
|
149
|
+
expect(runtime.getState().isLoading).toBe(true);
|
|
150
|
+
|
|
151
|
+
for (const isLoading of [false, true, undefined, false]) {
|
|
152
|
+
core.setAdapter({
|
|
153
|
+
...adapter,
|
|
154
|
+
adapters: { threadList: { ...threadList, isLoading } },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(seen).toEqual([false, true, false]);
|
|
159
|
+
expect(runtime.getState().isLoading).toBe(false);
|
|
160
|
+
unsubscribe();
|
|
161
|
+
});
|
|
162
|
+
|
|
116
163
|
it("updates mainThreadId when adapter.threadId changes", () => {
|
|
117
164
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
118
165
|
makeAdapter({ threadId: "thread-alpha" }),
|
|
@@ -198,6 +245,42 @@ describe("ExternalStoreThreadListRuntimeCore - __internal_setAdapter", () => {
|
|
|
198
245
|
expect(item?.id).toBe("thread-beta");
|
|
199
246
|
});
|
|
200
247
|
|
|
248
|
+
it.each(["__proto__", "constructor", "toString"])(
|
|
249
|
+
"synthesizes a main entry for a prototype-named thread id %s",
|
|
250
|
+
(threadId) => {
|
|
251
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
252
|
+
makeAdapter({ threadId }),
|
|
253
|
+
makeFactory(),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(core.getItemById(threadId)).toEqual(
|
|
257
|
+
expect.objectContaining({ id: threadId, status: "regular" }),
|
|
258
|
+
);
|
|
259
|
+
expect(Object.hasOwn(core.threadItems, threadId)).toBe(true);
|
|
260
|
+
expect(Object.getPrototypeOf(core.threadItems)).toBeNull();
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
it.each(["regular", "archived"] as const)(
|
|
265
|
+
"preserves a prototype-named %s adapter entry",
|
|
266
|
+
(status) => {
|
|
267
|
+
const adapter: ExternalStoreThreadListAdapter =
|
|
268
|
+
status === "regular"
|
|
269
|
+
? { threads: [{ id: "__proto__", status }] }
|
|
270
|
+
: { archivedThreads: [{ id: "__proto__", status }] };
|
|
271
|
+
const core = new ExternalStoreThreadListRuntimeCore(
|
|
272
|
+
makeAdapter(adapter),
|
|
273
|
+
makeFactory(),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(core.getItemById("__proto__")).toEqual(
|
|
277
|
+
expect.objectContaining({ id: "__proto__", status }),
|
|
278
|
+
);
|
|
279
|
+
expect(Object.hasOwn(core.threadItems, "__proto__")).toBe(true);
|
|
280
|
+
expect(Object.getPrototypeOf(core.threadItems)).toBeNull();
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
|
|
201
284
|
it("does not retain stale synthesized entries across mainThreadId switches", () => {
|
|
202
285
|
const core = new ExternalStoreThreadListRuntimeCore(
|
|
203
286
|
makeAdapter({ threadId: "thread-alpha" }),
|
|
@@ -160,26 +160,35 @@ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
|
|
|
160
160
|
expect(onNew).not.toHaveBeenCalled();
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
-
it("
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
it.each(["u1", null])(
|
|
164
|
+
"rejects parent %s without onEdit",
|
|
165
|
+
async (parentId) => {
|
|
166
|
+
const messages = [
|
|
167
|
+
createUserMessage("u1"),
|
|
168
|
+
createAssistantMessage("a1"),
|
|
169
|
+
];
|
|
170
|
+
const adapter = createBaseAdapter({ messages });
|
|
171
|
+
const core = new ExternalStoreThreadRuntimeCore(
|
|
172
|
+
contextProvider,
|
|
173
|
+
adapter,
|
|
174
|
+
);
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const appendMessage: AppendMessage = {
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [{ type: "text", text: "Edit" }],
|
|
179
|
+
attachments: [],
|
|
180
|
+
createdAt: new Date(),
|
|
181
|
+
parentId,
|
|
182
|
+
sourceId: null,
|
|
183
|
+
runConfig: undefined,
|
|
184
|
+
metadata: { custom: {} },
|
|
185
|
+
} as AppendMessage;
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
await expect(core.append(appendMessage)).rejects.toThrow(
|
|
188
|
+
"Runtime does not support editing messages.",
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
183
192
|
});
|
|
184
193
|
|
|
185
194
|
describe("startRun", () => {
|
|
@@ -4,6 +4,7 @@ import { act, render, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import type { FC } from "react";
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { AuiProvider, useAui } from "@assistant-ui/store";
|
|
7
|
+
import { CompositeAttachmentAdapter } from "../adapters/attachment";
|
|
7
8
|
import type {
|
|
8
9
|
ExternalThreadMessage,
|
|
9
10
|
ExternalThreadProps,
|
|
@@ -404,6 +405,53 @@ describe("ExternalThread attachments", () => {
|
|
|
404
405
|
},
|
|
405
406
|
);
|
|
406
407
|
|
|
408
|
+
it("preserves pending attachments when adapter removal fails", async () => {
|
|
409
|
+
const error = new Error("remove failed");
|
|
410
|
+
const remove = vi.fn(async () => {
|
|
411
|
+
throw error;
|
|
412
|
+
});
|
|
413
|
+
const file = new File(["data"], "notes.txt", { type: "text/plain" });
|
|
414
|
+
const adapter = {
|
|
415
|
+
accept: "*",
|
|
416
|
+
add: async () => ({
|
|
417
|
+
id: "att-1",
|
|
418
|
+
type: "file" as const,
|
|
419
|
+
name: file.name,
|
|
420
|
+
contentType: file.type,
|
|
421
|
+
file,
|
|
422
|
+
status: {
|
|
423
|
+
type: "requires-action" as const,
|
|
424
|
+
reason: "composer-send" as const,
|
|
425
|
+
},
|
|
426
|
+
}),
|
|
427
|
+
send: async () => ({}) as never,
|
|
428
|
+
remove,
|
|
429
|
+
};
|
|
430
|
+
const aui = renderThreadWithProps({ attachmentAdapter: adapter });
|
|
431
|
+
const composer = () => aui().thread.composer();
|
|
432
|
+
|
|
433
|
+
await act(() => composer().addAttachment(file));
|
|
434
|
+
await waitFor(() =>
|
|
435
|
+
expect(composer().getState().attachments).toHaveLength(1),
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
await expect(
|
|
439
|
+
act(() => composer().attachment({ id: "att-1" }).remove()),
|
|
440
|
+
).rejects.toBe(error);
|
|
441
|
+
|
|
442
|
+
expect(remove).toHaveBeenCalledWith(
|
|
443
|
+
expect.objectContaining({ id: "att-1" }),
|
|
444
|
+
);
|
|
445
|
+
expect(composer().getState().attachments).toHaveLength(1);
|
|
446
|
+
await waitFor(() =>
|
|
447
|
+
expect(composer().getState().attachments[0]?.status).toEqual({
|
|
448
|
+
type: "incomplete",
|
|
449
|
+
reason: "error",
|
|
450
|
+
message: "remove failed",
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
407
455
|
it.each([
|
|
408
456
|
[
|
|
409
457
|
"clearAttachments",
|
|
@@ -610,6 +658,97 @@ describe("ExternalThread attachments", () => {
|
|
|
610
658
|
});
|
|
611
659
|
});
|
|
612
660
|
|
|
661
|
+
it("removes complete edit attachments without adapter cleanup", async () => {
|
|
662
|
+
const remove = vi.fn(async () => {});
|
|
663
|
+
const completeAttachment = {
|
|
664
|
+
id: "image-1",
|
|
665
|
+
type: "image",
|
|
666
|
+
name: "image",
|
|
667
|
+
content: [{ type: "image", image: "data:image/png;base64,data" }],
|
|
668
|
+
status: { type: "complete" },
|
|
669
|
+
} as unknown as CompleteAttachment;
|
|
670
|
+
const aui = renderThreadWithProps({
|
|
671
|
+
messages: [
|
|
672
|
+
{
|
|
673
|
+
id: "u1",
|
|
674
|
+
role: "user",
|
|
675
|
+
content: [{ type: "text", text: "hi" }],
|
|
676
|
+
createdAt: new Date(0),
|
|
677
|
+
attachments: [completeAttachment],
|
|
678
|
+
metadata: { custom: {} },
|
|
679
|
+
} as unknown as ExternalThreadMessage,
|
|
680
|
+
],
|
|
681
|
+
onEdit: () => {},
|
|
682
|
+
attachmentAdapter: new CompositeAttachmentAdapter([
|
|
683
|
+
{
|
|
684
|
+
accept: "image/*",
|
|
685
|
+
add: async () => {
|
|
686
|
+
throw new Error("not used");
|
|
687
|
+
},
|
|
688
|
+
send: async () => {
|
|
689
|
+
throw new Error("not used");
|
|
690
|
+
},
|
|
691
|
+
remove,
|
|
692
|
+
},
|
|
693
|
+
]),
|
|
694
|
+
});
|
|
695
|
+
const composer = () => aui().thread.message({ id: "u1" }).composer();
|
|
696
|
+
|
|
697
|
+
await act(async () => composer().beginEdit());
|
|
698
|
+
await waitFor(() =>
|
|
699
|
+
expect(composer().getState().attachments).toHaveLength(1),
|
|
700
|
+
);
|
|
701
|
+
await act(() => composer().attachment({ id: "image-1" }).remove());
|
|
702
|
+
|
|
703
|
+
expect(remove).not.toHaveBeenCalled();
|
|
704
|
+
expect(composer().getState().attachments).toEqual([]);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it("calls adapter.remove for pending edit attachments", async () => {
|
|
708
|
+
const remove = vi.fn(async () => {});
|
|
709
|
+
const file = new File(["data"], "notes.txt", { type: "text/plain" });
|
|
710
|
+
const aui = renderThreadWithProps({
|
|
711
|
+
messages: [
|
|
712
|
+
{
|
|
713
|
+
id: "u1",
|
|
714
|
+
role: "user",
|
|
715
|
+
content: [{ type: "text", text: "hi" }],
|
|
716
|
+
createdAt: new Date(0),
|
|
717
|
+
attachments: [],
|
|
718
|
+
metadata: { custom: {} },
|
|
719
|
+
} as unknown as ExternalThreadMessage,
|
|
720
|
+
],
|
|
721
|
+
onEdit: () => {},
|
|
722
|
+
attachmentAdapter: {
|
|
723
|
+
accept: "*",
|
|
724
|
+
add: async () => ({
|
|
725
|
+
id: "pending-1",
|
|
726
|
+
type: "document" as const,
|
|
727
|
+
name: file.name,
|
|
728
|
+
contentType: file.type,
|
|
729
|
+
file,
|
|
730
|
+
status: {
|
|
731
|
+
type: "requires-action" as const,
|
|
732
|
+
reason: "composer-send" as const,
|
|
733
|
+
},
|
|
734
|
+
}),
|
|
735
|
+
send: async () => ({}) as never,
|
|
736
|
+
remove,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const composer = () => aui().thread.message({ id: "u1" }).composer();
|
|
740
|
+
|
|
741
|
+
await act(async () => composer().beginEdit());
|
|
742
|
+
await act(() => composer().addAttachment(file));
|
|
743
|
+
await act(() => composer().attachment({ id: "pending-1" }).remove());
|
|
744
|
+
|
|
745
|
+
expect(remove).toHaveBeenCalledTimes(1);
|
|
746
|
+
expect(remove).toHaveBeenCalledWith(
|
|
747
|
+
expect.objectContaining({ id: "pending-1" }),
|
|
748
|
+
);
|
|
749
|
+
expect(composer().getState().attachments).toEqual([]);
|
|
750
|
+
});
|
|
751
|
+
|
|
613
752
|
it("merges the failed send into a draft the user already modified", async () => {
|
|
614
753
|
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
615
754
|
let rejectSend!: (reason: unknown) => void;
|
|
@@ -229,6 +229,29 @@ describe("ExternalThread unset optional callbacks", () => {
|
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
describe("ExternalThread append parent selection", () => {
|
|
233
|
+
it("passes an explicit null parent to the host", () => {
|
|
234
|
+
const onNew = vi.fn();
|
|
235
|
+
const { aui } = renderThread({
|
|
236
|
+
messages: [assistantMessage({ type: "complete", reason: "stop" })],
|
|
237
|
+
onNew,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
aui().thread.append({
|
|
241
|
+
parentId: null,
|
|
242
|
+
content: [{ type: "text", text: "new root" }],
|
|
243
|
+
startRun: false,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(onNew).toHaveBeenCalledExactlyOnceWith(
|
|
247
|
+
expect.objectContaining({
|
|
248
|
+
parentId: null,
|
|
249
|
+
content: [{ type: "text", text: "new root" }],
|
|
250
|
+
}),
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
232
255
|
describe("ExternalThread composer", () => {
|
|
233
256
|
it("dispatches a synchronous setText + send sequence", async () => {
|
|
234
257
|
const onNew = vi.fn();
|
|
@@ -352,6 +352,84 @@ const switchTo = async (runtime: AssistantRuntime, remoteId: string) => {
|
|
|
352
352
|
});
|
|
353
353
|
};
|
|
354
354
|
|
|
355
|
+
describe("ThreadListRuntime.getById", () => {
|
|
356
|
+
it("keeps background messages and metadata bound to the requested thread", async () => {
|
|
357
|
+
const harness = renderRemoteList(listAdapter(), async () => ({
|
|
358
|
+
content: [],
|
|
359
|
+
}));
|
|
360
|
+
const runtime = harness.getRuntime();
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(runtime.threads.mainItem.getState().remoteId).toBe("thread-a");
|
|
363
|
+
});
|
|
364
|
+
await act(async () => {
|
|
365
|
+
runtime.thread.reset([
|
|
366
|
+
{
|
|
367
|
+
id: "message-a",
|
|
368
|
+
role: "user",
|
|
369
|
+
content: [{ type: "text", text: "A" }],
|
|
370
|
+
},
|
|
371
|
+
]);
|
|
372
|
+
});
|
|
373
|
+
await switchTo(runtime, "thread-b");
|
|
374
|
+
await act(async () => {
|
|
375
|
+
runtime.thread.reset([
|
|
376
|
+
{
|
|
377
|
+
id: "message-b",
|
|
378
|
+
role: "user",
|
|
379
|
+
content: [{ type: "text", text: "B" }],
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
const threadBId = runtime.threads.mainItem.getState().id;
|
|
384
|
+
await switchTo(runtime, "thread-a");
|
|
385
|
+
|
|
386
|
+
const threadB = runtime.threads.getById(threadBId);
|
|
387
|
+
expect(threadB.getState()).toMatchObject({
|
|
388
|
+
threadId: threadBId,
|
|
389
|
+
messages: [{ id: "message-b" }],
|
|
390
|
+
metadata: {
|
|
391
|
+
id: threadBId,
|
|
392
|
+
remoteId: "thread-b",
|
|
393
|
+
title: "B",
|
|
394
|
+
isMain: false,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
expect(runtime.thread.getState()).toMatchObject({
|
|
398
|
+
messages: [{ id: "message-a" }],
|
|
399
|
+
metadata: { remoteId: "thread-a", isMain: true },
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
let observed = threadB.getState();
|
|
403
|
+
const unsubscribe = threadB.subscribe(() => {
|
|
404
|
+
observed = threadB.getState();
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
await act(async () => {
|
|
408
|
+
await runtime.threads.getItemById(threadBId).rename("Renamed B");
|
|
409
|
+
});
|
|
410
|
+
expect(observed).toMatchObject({
|
|
411
|
+
threadId: threadBId,
|
|
412
|
+
messages: [{ id: "message-b" }],
|
|
413
|
+
metadata: { title: "Renamed B", isMain: false },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await switchTo(runtime, "thread-b");
|
|
417
|
+
expect(observed.metadata).toMatchObject({
|
|
418
|
+
title: "Renamed B",
|
|
419
|
+
isMain: true,
|
|
420
|
+
});
|
|
421
|
+
await switchTo(runtime, "thread-a");
|
|
422
|
+
expect(observed).toMatchObject({
|
|
423
|
+
threadId: threadBId,
|
|
424
|
+
messages: [{ id: "message-b" }],
|
|
425
|
+
metadata: { title: "Renamed B", isMain: false },
|
|
426
|
+
});
|
|
427
|
+
} finally {
|
|
428
|
+
unsubscribe();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
355
433
|
describe("background thread run events", () => {
|
|
356
434
|
it("delivers runEnd globally after the running thread is switched away", async () => {
|
|
357
435
|
const runA = deferred<{ content: [] }>();
|