@assistant-ui/core 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +2 -1
- package/dist/internal.js +2 -1
- package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
- package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
- package/dist/model-context/interactable-composer-metadata.js +259 -0
- package/dist/model-context/interactable-composer-metadata.js.map +1 -0
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/registry.js +1 -0
- package/dist/model-context/registry.js.map +1 -1
- package/dist/model-context/types.d.ts +6 -0
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +4 -0
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/AssistantProvider.d.ts +1 -1
- package/dist/react/AssistantProvider.js +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
- package/dist/react/RuntimeAdapter.js +1 -1
- package/dist/react/client/Interactables.d.ts +8 -2
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +190 -94
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/Tools.js +1 -1
- package/dist/react/client/interactable-model-context.d.ts +9 -6
- package/dist/react/client/interactable-model-context.d.ts.map +1 -1
- package/dist/react/client/interactable-model-context.js +154 -32
- package/dist/react/client/interactable-model-context.js.map +1 -1
- package/dist/react/index.d.ts +15 -6
- package/dist/react/index.js +12 -5
- package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
- package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
- package/dist/react/interactables-legacy/Interactables.js +264 -0
- package/dist/react/interactables-legacy/Interactables.js.map +1 -0
- package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
- package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
- package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
- package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
- package/dist/react/interactables-legacy/scopes.d.ts +58 -0
- package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
- package/dist/react/interactables-legacy/scopes.js +0 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
- package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
- package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
- package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
- package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
- package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
- package/dist/react/interactables-legacy/useInteractableState.js +84 -0
- package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
- package/dist/react/model-context/interactableTool.d.ts +53 -0
- package/dist/react/model-context/interactableTool.d.ts.map +1 -0
- package/dist/react/model-context/interactableTool.js +188 -0
- package/dist/react/model-context/interactableTool.js.map +1 -0
- package/dist/react/model-context/useAssistantContext.js +1 -1
- package/dist/react/model-context/useAssistantDataUI.js +1 -1
- package/dist/react/model-context/useAssistantInstructions.js +1 -1
- package/dist/react/model-context/useAssistantTool.js +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +1 -1
- package/dist/react/model-context/useAuiToolOverrides.js +1 -1
- package/dist/react/model-context/useInteractable.d.ts +73 -0
- package/dist/react/model-context/useInteractable.d.ts.map +1 -0
- package/dist/react/model-context/useInteractable.js +100 -0
- package/dist/react/model-context/useInteractable.js.map +1 -0
- package/dist/react/model-context/useInteractableState.d.ts +8 -5
- package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
- package/dist/react/model-context/useInteractableState.js +48 -48
- package/dist/react/model-context/useInteractableState.js.map +1 -1
- package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
- package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
- package/dist/react/model-context/useInteractableVersions.js +51 -0
- package/dist/react/model-context/useInteractableVersions.js.map +1 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
- package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
- package/dist/react/primitive-hooks/useVoice.js +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
- package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
- package/dist/react/primitives/composer/ComposerQueue.js +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.js +1 -1
- package/dist/react/primitives/message/MessageAttachments.js +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
- package/dist/react/primitives/message/MessageParts.js +1 -1
- package/dist/react/primitives/message/MessageQuote.js +1 -1
- package/dist/react/primitives/part/PartMessages.d.ts +5 -2
- package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
- package/dist/react/primitives/part/PartMessages.js +8 -8
- package/dist/react/primitives/part/PartMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
- package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/react/primitives/thread/ThreadMessages.js +64 -2
- package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
- package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
- package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
- package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
- package/dist/react/providers/MessageByIdProvider.js +57 -0
- package/dist/react/providers/MessageByIdProvider.js.map +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +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 +16 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
- package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
- package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
- package/dist/react/runtimes/createRuntimeExtras.js +43 -0
- package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
- package/dist/react/runtimes/useLocalRuntime.js +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
- package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
- package/dist/react/runtimes/useStreamingTiming.js +43 -0
- package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
- package/dist/react/types/scopes/interactables.d.ts +40 -24
- package/dist/react/types/scopes/interactables.d.ts.map +1 -1
- package/dist/react/types/store-augmentation.d.ts +3 -1
- package/dist/react/types/store-augmentation.d.ts.map +1 -1
- package/dist/react/utils/useJSONEqual.d.ts +11 -0
- package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
- package/dist/react/utils/useJSONEqual.js +30 -0
- package/dist/react/utils/useJSONEqual.js.map +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +13 -0
- package/dist/runtime/base/base-composer-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 +6 -1
- 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 +4 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.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 +6 -3
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/streaming-timing.d.ts +59 -0
- package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
- package/dist/runtime/utils/streaming-timing.js +76 -0
- package/dist/runtime/utils/streaming-timing.js.map +1 -0
- package/dist/runtime/utils/thread-message-like.js +2 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
- 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 +19 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.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 +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +1 -1
- package/dist/store/clients/thread-message-client.js +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.js +3 -2
- package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +17 -0
- package/src/internal.ts +7 -0
- package/src/model-context/interactable-composer-metadata.test.ts +550 -0
- package/src/model-context/interactable-composer-metadata.ts +395 -0
- package/src/model-context/registry.ts +5 -0
- package/src/model-context/types.ts +12 -0
- package/src/react/client/Interactables.test.ts +358 -0
- package/src/react/client/Interactables.ts +339 -86
- package/src/react/client/interactable-model-context.test.ts +418 -0
- package/src/react/client/interactable-model-context.ts +252 -58
- package/src/react/index.ts +72 -3
- package/src/react/interactables-legacy/Interactables.ts +332 -0
- package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
- package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
- package/src/react/interactables-legacy/scopes.ts +66 -0
- package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
- package/src/react/interactables-legacy/useInteractableState.ts +67 -0
- package/src/react/model-context/interactableTool.tsx +175 -0
- package/src/react/model-context/useInteractable.ts +233 -0
- package/src/react/model-context/useInteractableState.ts +31 -23
- package/src/react/model-context/useInteractableVersions.ts +48 -0
- package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
- package/src/react/primitives/part/PartMessages.test.tsx +17 -0
- package/src/react/primitives/part/PartMessages.tsx +13 -8
- package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
- package/src/react/providers/MessageByIdProvider.tsx +23 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
- package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
- package/src/react/runtimes/createRuntimeExtras.ts +68 -0
- package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
- package/src/react/runtimes/useStreamingTiming.ts +68 -0
- package/src/react/types/scopes/interactables.ts +45 -23
- package/src/react/types/store-augmentation.ts +4 -2
- package/src/react/utils/useJSONEqual.ts +20 -0
- package/src/runtime/base/base-composer-runtime-core.ts +13 -0
- package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
- package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
- package/src/runtime/utils/message-repository.ts +11 -7
- package/src/runtime/utils/streaming-timing.test.ts +301 -0
- package/src/runtime/utils/streaming-timing.ts +161 -0
- package/src/runtime/utils/thread-message-like.ts +2 -1
- package/src/runtimes/external-store/external-store-adapter.ts +28 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
- package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
- package/src/runtimes/remote-thread-list/types.ts +13 -0
- package/src/tests/MessageRepository.test.ts +2 -0
- package/src/tests/composer-can-send.test.ts +1 -0
- package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
- package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
- package/src/tests/groupParts.test.ts +32 -0
- package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
- package/src/tests/remote-thread-list-test-helpers.ts +2 -1
- package/src/tests/thread-message-like.test.ts +50 -0
- package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
- package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
- package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
|
@@ -4,11 +4,20 @@ import type { AppendMessage, ThreadMessage } from "../types/message";
|
|
|
4
4
|
import type { CompleteAttachment } from "../types/attachment";
|
|
5
5
|
import type { ThreadRuntimeCore } from "../runtime/interfaces/thread-runtime-core";
|
|
6
6
|
|
|
7
|
-
const makeRuntime = (
|
|
7
|
+
const makeRuntime = (options?: {
|
|
8
|
+
messages?: ThreadMessage[];
|
|
9
|
+
composerMetadata?: Record<string, unknown>;
|
|
10
|
+
}) => {
|
|
8
11
|
const append = vi.fn();
|
|
9
12
|
const runtime = {
|
|
10
13
|
append,
|
|
11
14
|
composer: { runConfig: {} },
|
|
15
|
+
messages: options?.messages ?? [],
|
|
16
|
+
getModelContext: () => ({
|
|
17
|
+
...(options?.composerMetadata
|
|
18
|
+
? { unstable_composerMetadata: options.composerMetadata }
|
|
19
|
+
: {}),
|
|
20
|
+
}),
|
|
12
21
|
} as unknown as ThreadRuntimeCore & { adapters?: undefined };
|
|
13
22
|
return { runtime, append };
|
|
14
23
|
};
|
|
@@ -214,6 +223,74 @@ describe("DefaultEditComposerRuntimeCore", () => {
|
|
|
214
223
|
});
|
|
215
224
|
});
|
|
216
225
|
|
|
226
|
+
describe("interactables snapshot gating", () => {
|
|
227
|
+
const live = (state: unknown) => ({
|
|
228
|
+
interactables: [{ id: "n1", name: "note", state }],
|
|
229
|
+
});
|
|
230
|
+
const snapshotMessage = (id: string, state: unknown): ThreadMessage =>
|
|
231
|
+
makeUserMessage({
|
|
232
|
+
id,
|
|
233
|
+
metadata: {
|
|
234
|
+
custom: { interactables: [{ id: "n1", name: "note", state }] },
|
|
235
|
+
},
|
|
236
|
+
} as Partial<ThreadMessage>);
|
|
237
|
+
|
|
238
|
+
it("re-stamps the baseline when the edited message carried the only snapshot", async () => {
|
|
239
|
+
// The new branch's prefix (before the edited message) has no snapshot,
|
|
240
|
+
// so the live (unchanged) state becomes the branch's baseline.
|
|
241
|
+
const edited = snapshotMessage("msg-1", { v: 1 });
|
|
242
|
+
const { runtime, append } = makeRuntime({
|
|
243
|
+
messages: [edited],
|
|
244
|
+
composerMetadata: live({ v: 1 }),
|
|
245
|
+
});
|
|
246
|
+
const composer = new DefaultEditComposerRuntimeCore(runtime, () => {}, {
|
|
247
|
+
parentId: null,
|
|
248
|
+
message: edited,
|
|
249
|
+
});
|
|
250
|
+
composer.setText("new");
|
|
251
|
+
await composer.send();
|
|
252
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
253
|
+
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
254
|
+
{ id: "n1", name: "note", state: { v: 1 } },
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("stamps the newest live state when the interactable was edited after the original message", async () => {
|
|
259
|
+
const edited = snapshotMessage("msg-1", { v: 1 });
|
|
260
|
+
const { runtime, append } = makeRuntime({
|
|
261
|
+
messages: [edited],
|
|
262
|
+
composerMetadata: live({ v: 2 }),
|
|
263
|
+
});
|
|
264
|
+
const composer = new DefaultEditComposerRuntimeCore(runtime, () => {}, {
|
|
265
|
+
parentId: null,
|
|
266
|
+
message: edited,
|
|
267
|
+
});
|
|
268
|
+
composer.setText("new");
|
|
269
|
+
await composer.send();
|
|
270
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
271
|
+
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
272
|
+
{ id: "n1", name: "note", state: { v: 2 } },
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("stamps nothing when the branch prefix already carries the live state", async () => {
|
|
277
|
+
const parent = snapshotMessage("parent-1", { v: 1 });
|
|
278
|
+
const edited = makeUserMessage({ id: "msg-1" });
|
|
279
|
+
const { runtime, append } = makeRuntime({
|
|
280
|
+
messages: [parent, edited],
|
|
281
|
+
composerMetadata: live({ v: 1 }),
|
|
282
|
+
});
|
|
283
|
+
const composer = new DefaultEditComposerRuntimeCore(runtime, () => {}, {
|
|
284
|
+
parentId: "parent-1",
|
|
285
|
+
message: edited,
|
|
286
|
+
});
|
|
287
|
+
composer.setText("new");
|
|
288
|
+
await composer.send();
|
|
289
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
290
|
+
expect(appended.metadata?.custom?.interactables).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
217
294
|
describe("non-user messages", () => {
|
|
218
295
|
it("does not lift non-text parts for assistant messages", () => {
|
|
219
296
|
const { runtime } = makeRuntime();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DefaultThreadComposerRuntimeCore } from "../runtime/base/default-thread-composer-runtime-core";
|
|
3
|
+
import type { ThreadRuntimeCore } from "../runtime/interfaces/thread-runtime-core";
|
|
4
|
+
import type { AppendMessage, ThreadMessage } from "../types/message";
|
|
5
|
+
|
|
6
|
+
const makeRuntime = (options?: {
|
|
7
|
+
messages?: ThreadMessage[];
|
|
8
|
+
composerMetadata?: Record<string, unknown>;
|
|
9
|
+
}) => {
|
|
10
|
+
const append = vi.fn();
|
|
11
|
+
const runtime = {
|
|
12
|
+
append,
|
|
13
|
+
getModelContext: () => ({
|
|
14
|
+
...(options?.composerMetadata
|
|
15
|
+
? { unstable_composerMetadata: options.composerMetadata }
|
|
16
|
+
: {}),
|
|
17
|
+
}),
|
|
18
|
+
subscribe: () => () => {},
|
|
19
|
+
capabilities: { cancel: false },
|
|
20
|
+
messages: options?.messages ?? [],
|
|
21
|
+
isSendDisabled: false,
|
|
22
|
+
composer: { runConfig: {} },
|
|
23
|
+
} as unknown as Omit<ThreadRuntimeCore, "composer">;
|
|
24
|
+
return { runtime, append };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const snapshotMessage = (id: string, state: unknown): ThreadMessage =>
|
|
28
|
+
({
|
|
29
|
+
id,
|
|
30
|
+
role: "user",
|
|
31
|
+
createdAt: new Date(),
|
|
32
|
+
content: [{ type: "text", text: "old" }],
|
|
33
|
+
attachments: [],
|
|
34
|
+
metadata: {
|
|
35
|
+
custom: { interactables: [{ id: "n1", name: "note", state }] },
|
|
36
|
+
},
|
|
37
|
+
}) as ThreadMessage;
|
|
38
|
+
|
|
39
|
+
const live = (state: unknown) => ({
|
|
40
|
+
interactables: [{ id: "n1", name: "note", state }],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const send = async (runtime: Omit<ThreadRuntimeCore, "composer">) => {
|
|
44
|
+
const composer = new DefaultThreadComposerRuntimeCore(runtime);
|
|
45
|
+
composer.setText("hello");
|
|
46
|
+
await composer.send();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe("DefaultThreadComposerRuntimeCore interactables snapshot gating", () => {
|
|
50
|
+
it("stamps a baseline on the first send of the thread", async () => {
|
|
51
|
+
const { runtime, append } = makeRuntime({
|
|
52
|
+
composerMetadata: live({ v: 1 }),
|
|
53
|
+
});
|
|
54
|
+
await send(runtime);
|
|
55
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
56
|
+
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
57
|
+
{ id: "n1", name: "note", state: { v: 1 } },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("stamps nothing when the history already carries the live state", async () => {
|
|
62
|
+
const { runtime, append } = makeRuntime({
|
|
63
|
+
messages: [snapshotMessage("msg-1", { v: 1 })],
|
|
64
|
+
composerMetadata: live({ v: 1 }),
|
|
65
|
+
});
|
|
66
|
+
await send(runtime);
|
|
67
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
68
|
+
expect(appended.metadata?.custom?.interactables).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("stamps the new state when it diverged from the latest snapshot", async () => {
|
|
72
|
+
const { runtime, append } = makeRuntime({
|
|
73
|
+
messages: [snapshotMessage("msg-1", { v: 1 })],
|
|
74
|
+
composerMetadata: live({ v: 2 }),
|
|
75
|
+
});
|
|
76
|
+
await send(runtime);
|
|
77
|
+
const appended = append.mock.calls[0]![0] as AppendMessage;
|
|
78
|
+
expect(appended.metadata?.custom?.interactables).toEqual([
|
|
79
|
+
{ id: "n1", name: "note", state: { v: 2 } },
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -355,6 +355,343 @@ describe("ExternalStoreThreadRuntimeCore - optimistic message reconciliation", (
|
|
|
355
355
|
});
|
|
356
356
|
});
|
|
357
357
|
|
|
358
|
+
describe("ExternalStoreThreadRuntimeCore - branch change callback", () => {
|
|
359
|
+
type Raw = {
|
|
360
|
+
id: string;
|
|
361
|
+
role: "user" | "assistant";
|
|
362
|
+
text: string;
|
|
363
|
+
optimistic?: boolean;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const convertMessage = (m: Raw): ThreadMessageLike => ({
|
|
367
|
+
id: m.id,
|
|
368
|
+
role: m.role,
|
|
369
|
+
content: [{ type: "text", text: m.text }],
|
|
370
|
+
...(m.optimistic && { metadata: { isOptimistic: true } }),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const u: Raw = { id: "u", role: "user", text: "hi" };
|
|
374
|
+
|
|
375
|
+
// Build a runtime whose repository holds two sibling assistant branches
|
|
376
|
+
// (a1, a2) under the user message, with a2 the currently visible head.
|
|
377
|
+
const makeBranched = (extra?: Record<string, unknown>) => {
|
|
378
|
+
const base = { convertMessage, setMessages: vi.fn(), ...extra };
|
|
379
|
+
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
380
|
+
mockContextProvider,
|
|
381
|
+
makeStore({
|
|
382
|
+
...base,
|
|
383
|
+
messages: [u, { id: "a1", role: "assistant", text: "first" }],
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
runtime.__internal_setAdapter(
|
|
387
|
+
makeStore({
|
|
388
|
+
...base,
|
|
389
|
+
messages: [u, { id: "a2", role: "assistant", text: "second" }],
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
return runtime;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
it("fires on explicit switchToBranch with head and visible path", () => {
|
|
396
|
+
const onBranchChange = vi.fn();
|
|
397
|
+
const runtime = makeBranched({
|
|
398
|
+
unstable_onBranchChange: onBranchChange,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
runtime.switchToBranch("a1");
|
|
402
|
+
|
|
403
|
+
expect(onBranchChange).toHaveBeenCalledTimes(1);
|
|
404
|
+
expect(onBranchChange).toHaveBeenCalledWith({
|
|
405
|
+
headId: "a1",
|
|
406
|
+
visibleMessageIds: ["u", "a1"],
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("dedupes consecutive switches that resolve to the same head", () => {
|
|
411
|
+
const onBranchChange = vi.fn();
|
|
412
|
+
const runtime = makeBranched({
|
|
413
|
+
unstable_onBranchChange: onBranchChange,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
runtime.switchToBranch("a1");
|
|
417
|
+
runtime.switchToBranch("a1");
|
|
418
|
+
expect(onBranchChange).toHaveBeenCalledTimes(1);
|
|
419
|
+
|
|
420
|
+
runtime.switchToBranch("a2");
|
|
421
|
+
expect(onBranchChange).toHaveBeenCalledTimes(2);
|
|
422
|
+
expect(onBranchChange).toHaveBeenLastCalledWith({
|
|
423
|
+
headId: "a2",
|
|
424
|
+
visibleMessageIds: ["u", "a2"],
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("does not fire on adapter resync", () => {
|
|
429
|
+
const onBranchChange = vi.fn();
|
|
430
|
+
// makeBranched performs construction + one resync via __internal_setAdapter.
|
|
431
|
+
makeBranched({ unstable_onBranchChange: onBranchChange });
|
|
432
|
+
expect(onBranchChange).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("does not fire on messageRepository resync (resetHead)", () => {
|
|
436
|
+
const onBranchChange = vi.fn();
|
|
437
|
+
const source = new ExternalStoreThreadRuntimeCore(
|
|
438
|
+
mockContextProvider,
|
|
439
|
+
makeStore({
|
|
440
|
+
messages: [u, { id: "a", role: "assistant", text: "x" }],
|
|
441
|
+
convertMessage,
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
const exported = source.export();
|
|
445
|
+
|
|
446
|
+
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
447
|
+
mockContextProvider,
|
|
448
|
+
makeStore({
|
|
449
|
+
messageRepository: exported,
|
|
450
|
+
unstable_onBranchChange: onBranchChange,
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
runtime.__internal_setAdapter(
|
|
454
|
+
makeStore({
|
|
455
|
+
messageRepository: { ...exported },
|
|
456
|
+
unstable_onBranchChange: onBranchChange,
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
expect(onBranchChange).not.toHaveBeenCalled();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("does not fire on append, edit, or content-only resync", async () => {
|
|
464
|
+
const onBranchChange = vi.fn();
|
|
465
|
+
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
466
|
+
mockContextProvider,
|
|
467
|
+
makeStore({
|
|
468
|
+
messages: [u, { id: "a1", role: "assistant", text: "first" }],
|
|
469
|
+
convertMessage,
|
|
470
|
+
onNew: vi.fn(),
|
|
471
|
+
onEdit: vi.fn(),
|
|
472
|
+
setMessages: vi.fn(),
|
|
473
|
+
unstable_onBranchChange: onBranchChange,
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// append to the tail (onNew)
|
|
478
|
+
await runtime.append({
|
|
479
|
+
role: "user",
|
|
480
|
+
content: [{ type: "text", text: "again" }],
|
|
481
|
+
attachments: [],
|
|
482
|
+
createdAt: new Date(0),
|
|
483
|
+
parentId: "a1",
|
|
484
|
+
sourceId: null,
|
|
485
|
+
runConfig: {},
|
|
486
|
+
metadata: { custom: {} },
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// edit a non-tail message (onEdit)
|
|
490
|
+
await runtime.append({
|
|
491
|
+
role: "user",
|
|
492
|
+
content: [{ type: "text", text: "edited" }],
|
|
493
|
+
attachments: [],
|
|
494
|
+
createdAt: new Date(0),
|
|
495
|
+
parentId: null,
|
|
496
|
+
sourceId: null,
|
|
497
|
+
runConfig: {},
|
|
498
|
+
metadata: { custom: {} },
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// content-only resync of the same structure
|
|
502
|
+
runtime.__internal_setAdapter(
|
|
503
|
+
makeStore({
|
|
504
|
+
messages: [u, { id: "a1", role: "assistant", text: "first edited" }],
|
|
505
|
+
convertMessage,
|
|
506
|
+
onNew: vi.fn(),
|
|
507
|
+
onEdit: vi.fn(),
|
|
508
|
+
setMessages: vi.fn(),
|
|
509
|
+
unstable_onBranchChange: onBranchChange,
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(onBranchChange).not.toHaveBeenCalled();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("fires again when a resync moved the head away before switching back", () => {
|
|
517
|
+
const onBranchChange = vi.fn();
|
|
518
|
+
const runtime = makeBranched({
|
|
519
|
+
unstable_onBranchChange: onBranchChange,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
runtime.switchToBranch("a1");
|
|
523
|
+
expect(onBranchChange).toHaveBeenCalledTimes(1);
|
|
524
|
+
|
|
525
|
+
// Adapter resync moves the visible head back to a2 (does not fire).
|
|
526
|
+
runtime.__internal_setAdapter(
|
|
527
|
+
makeStore({
|
|
528
|
+
messages: [u, { id: "a2", role: "assistant", text: "second" }],
|
|
529
|
+
convertMessage,
|
|
530
|
+
setMessages: vi.fn(),
|
|
531
|
+
unstable_onBranchChange: onBranchChange,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
expect(onBranchChange).toHaveBeenCalledTimes(1);
|
|
535
|
+
|
|
536
|
+
// Switching back to a1 must fire — the dedupe compares the head observed
|
|
537
|
+
// just before the switch, not the last emitted head.
|
|
538
|
+
runtime.switchToBranch("a1");
|
|
539
|
+
expect(onBranchChange).toHaveBeenCalledTimes(2);
|
|
540
|
+
expect(onBranchChange).toHaveBeenLastCalledWith({
|
|
541
|
+
headId: "a1",
|
|
542
|
+
visibleMessageIds: ["u", "a1"],
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("does not fire while the thread is running", () => {
|
|
547
|
+
const onBranchChange = vi.fn();
|
|
548
|
+
const runtime = makeBranched({
|
|
549
|
+
unstable_onBranchChange: onBranchChange,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Flip into a running state without adding a trailing placeholder.
|
|
553
|
+
runtime.__internal_setAdapter(
|
|
554
|
+
makeStore({
|
|
555
|
+
messages: [u, { id: "a2", role: "assistant", text: "second" }],
|
|
556
|
+
convertMessage,
|
|
557
|
+
setMessages: vi.fn(),
|
|
558
|
+
isRunning: true,
|
|
559
|
+
unstable_onBranchChange: onBranchChange,
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
runtime.switchToBranch("a1");
|
|
564
|
+
|
|
565
|
+
expect(onBranchChange).not.toHaveBeenCalled();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it("emits the persisted canonical head, not an optimistic id", () => {
|
|
569
|
+
const onBranchChange = vi.fn();
|
|
570
|
+
|
|
571
|
+
// Build a repository whose visible head is the persisted a1, with an
|
|
572
|
+
// optimistic sibling a2 also present under the user message.
|
|
573
|
+
const source = new ExternalStoreThreadRuntimeCore(
|
|
574
|
+
mockContextProvider,
|
|
575
|
+
makeStore({
|
|
576
|
+
messages: [u, { id: "a1", role: "assistant", text: "first" }],
|
|
577
|
+
convertMessage,
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
const exported = source.export();
|
|
581
|
+
const a1Item = exported.messages.find((m) => m.message.id === "a1")!;
|
|
582
|
+
const optimisticRepo = {
|
|
583
|
+
headId: "a1",
|
|
584
|
+
messages: [
|
|
585
|
+
...exported.messages,
|
|
586
|
+
{
|
|
587
|
+
parentId: "u",
|
|
588
|
+
message: {
|
|
589
|
+
...a1Item.message,
|
|
590
|
+
id: "a2",
|
|
591
|
+
metadata: { ...a1Item.message.metadata, isOptimistic: true },
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
598
|
+
mockContextProvider,
|
|
599
|
+
makeStore({
|
|
600
|
+
messageRepository: optimisticRepo,
|
|
601
|
+
setMessages: vi.fn(),
|
|
602
|
+
unstable_onBranchChange: onBranchChange,
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Switch onto the optimistic leaf: the canonical head changes (a1 -> the
|
|
607
|
+
// persisted ancestor) so the callback fires.
|
|
608
|
+
runtime.switchToBranch("a2");
|
|
609
|
+
|
|
610
|
+
const event = onBranchChange.mock.calls.at(-1)![0];
|
|
611
|
+
// export() walks up from the optimistic a2 to its persisted ancestor.
|
|
612
|
+
expect(event.headId).toBe("u");
|
|
613
|
+
// The visible path still reflects the optimistic leaf.
|
|
614
|
+
expect(event.visibleMessageIds).toEqual(["u", "a2"]);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("is a no-op when no callback is provided", () => {
|
|
618
|
+
const runtime = makeBranched();
|
|
619
|
+
expect(() => runtime.switchToBranch("a1")).not.toThrow();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("does not read canonical heads when no callback is provided", () => {
|
|
623
|
+
const runtime = makeBranched();
|
|
624
|
+
const repository = (
|
|
625
|
+
runtime as unknown as { repository: { canonicalHeadId: string | null } }
|
|
626
|
+
).repository;
|
|
627
|
+
const canonicalHeadIdSpy = vi.spyOn(repository, "canonicalHeadId", "get");
|
|
628
|
+
|
|
629
|
+
runtime.switchToBranch("a1");
|
|
630
|
+
|
|
631
|
+
expect(canonicalHeadIdSpy).not.toHaveBeenCalled();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("does not export snapshots when emitting branch changes", () => {
|
|
635
|
+
const onBranchChange = vi.fn();
|
|
636
|
+
const runtime = makeBranched({ unstable_onBranchChange: onBranchChange });
|
|
637
|
+
const repository = (
|
|
638
|
+
runtime as unknown as { repository: { export: () => unknown } }
|
|
639
|
+
).repository;
|
|
640
|
+
const exportSpy = vi.spyOn(repository, "export");
|
|
641
|
+
|
|
642
|
+
runtime.switchToBranch("a1");
|
|
643
|
+
|
|
644
|
+
expect(onBranchChange).toHaveBeenCalledTimes(1);
|
|
645
|
+
expect(exportSpy).not.toHaveBeenCalled();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("uses the initiating adapter callback if setMessages swaps adapters synchronously", () => {
|
|
649
|
+
const initialOnBranchChange = vi.fn();
|
|
650
|
+
const swappedOnBranchChange = vi.fn();
|
|
651
|
+
let runtime!: ExternalStoreThreadRuntimeCore;
|
|
652
|
+
const setMessages = vi.fn(() => {
|
|
653
|
+
runtime.__internal_setAdapter(
|
|
654
|
+
makeStore({
|
|
655
|
+
messages: [u, { id: "a1", role: "assistant", text: "first" }],
|
|
656
|
+
convertMessage,
|
|
657
|
+
setMessages: vi.fn(),
|
|
658
|
+
unstable_onBranchChange: swappedOnBranchChange,
|
|
659
|
+
}),
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
runtime = makeBranched({
|
|
664
|
+
setMessages,
|
|
665
|
+
unstable_onBranchChange: initialOnBranchChange,
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
runtime.switchToBranch("a1");
|
|
669
|
+
|
|
670
|
+
expect(setMessages).toHaveBeenCalledTimes(1);
|
|
671
|
+
expect(initialOnBranchChange).toHaveBeenCalledTimes(1);
|
|
672
|
+
expect(initialOnBranchChange).toHaveBeenCalledWith({
|
|
673
|
+
headId: "a1",
|
|
674
|
+
visibleMessageIds: ["u", "a1"],
|
|
675
|
+
});
|
|
676
|
+
expect(swappedOnBranchChange).not.toHaveBeenCalled();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("still calls setMessages on branch switch", () => {
|
|
680
|
+
const setMessages = vi.fn();
|
|
681
|
+
const runtime = makeBranched({
|
|
682
|
+
setMessages,
|
|
683
|
+
unstable_onBranchChange: vi.fn(),
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
runtime.switchToBranch("a1");
|
|
687
|
+
|
|
688
|
+
expect(setMessages).toHaveBeenCalledWith([
|
|
689
|
+
expect.objectContaining({ id: "u" }),
|
|
690
|
+
expect.objectContaining({ id: "a1" }),
|
|
691
|
+
]);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
358
695
|
describe("ExternalStoreThreadRuntimeCore - initialize event replay", () => {
|
|
359
696
|
const message = { id: "m", role: "assistant" as const, content: [] };
|
|
360
697
|
const flushMicrotasks = () => Promise.resolve();
|
|
@@ -190,6 +190,38 @@ describe("groupPartByType", () => {
|
|
|
190
190
|
expect(fn(standalone, inlineCtx)).toEqual(["group-tool"]);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it("leaves regular and standalone tool calls ungrouped when both map to []", () => {
|
|
194
|
+
const fn = groupPartByType({
|
|
195
|
+
reasoning: ["group-chainOfThought", "group-reasoning"],
|
|
196
|
+
"tool-call": [],
|
|
197
|
+
"standalone-tool-call": [],
|
|
198
|
+
});
|
|
199
|
+
const regularTool = part({
|
|
200
|
+
type: "tool-call",
|
|
201
|
+
toolName: "search",
|
|
202
|
+
} as Partial<PartState>);
|
|
203
|
+
const standaloneTool = part({
|
|
204
|
+
type: "tool-call",
|
|
205
|
+
toolName: "ask_user",
|
|
206
|
+
} as Partial<PartState>);
|
|
207
|
+
|
|
208
|
+
const paths = [
|
|
209
|
+
fn(part({ type: "reasoning" })),
|
|
210
|
+
fn(regularTool),
|
|
211
|
+
fn(standaloneTool, standaloneContext("ask_user")),
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// reasoning groups into the chain-of-thought; both tool kinds stay flat.
|
|
215
|
+
expect(paths).toEqual([
|
|
216
|
+
["group-chainOfThought", "group-reasoning"],
|
|
217
|
+
[],
|
|
218
|
+
[],
|
|
219
|
+
]);
|
|
220
|
+
expect(dump(buildGroupTree(paths))).toBe(
|
|
221
|
+
"G:group-chainOfThought#0[0]{G:group-reasoning#0.0[0]{P:#0.0.0(0)}},P:#1(1),P:#2(2)",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
193
225
|
it("routes MCP-app parts through 'standalone-tool-call' from the part alone", () => {
|
|
194
226
|
const fn = groupPartByType({
|
|
195
227
|
"tool-call": ["group-tool"],
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { RemoteThreadListAdapter } from "../runtimes/remote-thread-list/types";
|
|
3
|
+
import { createCore, makeAdapter } from "./remote-thread-list-test-helpers";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Tests for the reactive threadId useEffect logic in useRemoteThreadListRuntime.
|
|
@@ -91,3 +93,91 @@ describe("threadId reactive effect", () => {
|
|
|
91
93
|
expect(switchToThread).toHaveBeenCalledTimes(3);
|
|
92
94
|
});
|
|
93
95
|
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tests for onThreadIdChange, driving the real
|
|
99
|
+
* RemoteThreadListThreadListRuntimeCore via a mock adapter (no React). These
|
|
100
|
+
* exercise the actual _notifyThreadIdChange / _mainThreadRemoteId path and the
|
|
101
|
+
* subscription + switchToThread wiring that invokes it.
|
|
102
|
+
*/
|
|
103
|
+
function createCoreWithCallback(
|
|
104
|
+
onThreadIdChange: (id: string | undefined) => void,
|
|
105
|
+
adapterOverrides: Partial<RemoteThreadListAdapter> = {},
|
|
106
|
+
) {
|
|
107
|
+
const adapter = makeAdapter(adapterOverrides);
|
|
108
|
+
const core = createCore(adapter, undefined, onThreadIdChange);
|
|
109
|
+
return { core, adapter };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Let the constructor's in-flight switchToNewThread settle.
|
|
113
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve));
|
|
114
|
+
|
|
115
|
+
describe("onThreadIdChange", () => {
|
|
116
|
+
it("does not emit while the active thread is still optimistic", async () => {
|
|
117
|
+
const cb = vi.fn();
|
|
118
|
+
createCoreWithCallback(cb);
|
|
119
|
+
await flush();
|
|
120
|
+
|
|
121
|
+
expect(cb).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("emits the remote ID once a new thread is initialized", async () => {
|
|
125
|
+
const cb = vi.fn();
|
|
126
|
+
const { core } = createCoreWithCallback(cb, {
|
|
127
|
+
initialize: vi.fn(async () => ({
|
|
128
|
+
remoteId: "remote-1",
|
|
129
|
+
externalId: "ext-1",
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
await flush();
|
|
133
|
+
|
|
134
|
+
const newThreadId = core.newThreadId!;
|
|
135
|
+
expect(newThreadId).toBeDefined();
|
|
136
|
+
expect(cb).not.toHaveBeenCalled(); // still optimistic, no remote ID
|
|
137
|
+
|
|
138
|
+
await core.initialize(newThreadId);
|
|
139
|
+
|
|
140
|
+
expect(cb).toHaveBeenLastCalledWith("remote-1");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("emits the remote ID when switching to an existing thread", async () => {
|
|
144
|
+
const cb = vi.fn();
|
|
145
|
+
const { core } = createCoreWithCallback(cb);
|
|
146
|
+
await flush();
|
|
147
|
+
|
|
148
|
+
await core.switchToThread("existing-1");
|
|
149
|
+
|
|
150
|
+
expect(cb).toHaveBeenLastCalledWith("existing-1");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("dedupes: switching to the already-active thread does not re-emit", async () => {
|
|
154
|
+
const cb = vi.fn();
|
|
155
|
+
const { core } = createCoreWithCallback(cb);
|
|
156
|
+
await flush();
|
|
157
|
+
|
|
158
|
+
await core.switchToThread("existing-1");
|
|
159
|
+
const callsAfterFirstSwitch = cb.mock.calls.length;
|
|
160
|
+
|
|
161
|
+
await core.switchToThread("existing-1");
|
|
162
|
+
|
|
163
|
+
expect(cb).toHaveBeenCalledTimes(callsAfterFirstSwitch);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("never surfaces the transient local ID", async () => {
|
|
167
|
+
const cb = vi.fn();
|
|
168
|
+
const { core } = createCoreWithCallback(cb, {
|
|
169
|
+
initialize: vi.fn(async () => ({
|
|
170
|
+
remoteId: "remote-1",
|
|
171
|
+
externalId: "ext-1",
|
|
172
|
+
})),
|
|
173
|
+
});
|
|
174
|
+
await flush();
|
|
175
|
+
|
|
176
|
+
const localId = core.newThreadId!;
|
|
177
|
+
await core.initialize(localId);
|
|
178
|
+
|
|
179
|
+
for (const [emitted] of cb.mock.calls) {
|
|
180
|
+
expect(emitted).not.toBe(localId);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -52,9 +52,10 @@ export function makeAdapter(
|
|
|
52
52
|
export function createCore(
|
|
53
53
|
adapter: RemoteThreadListAdapter,
|
|
54
54
|
threadId?: string,
|
|
55
|
+
onThreadIdChange?: (threadId: string | undefined) => void,
|
|
55
56
|
): RemoteThreadListThreadListRuntimeCore {
|
|
56
57
|
const core = new RemoteThreadListThreadListRuntimeCore(
|
|
57
|
-
{ adapter, runtimeHook: () => ({}) as never, threadId },
|
|
58
|
+
{ adapter, runtimeHook: () => ({}) as never, threadId, onThreadIdChange },
|
|
58
59
|
contextProvider,
|
|
59
60
|
);
|
|
60
61
|
// `startThreadRuntime` blocks until a React component attaches a runtime;
|