@assistant-ui/core 0.2.6 → 0.2.8
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.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -0
- package/dist/internal/duplicate-detection.d.ts +5 -0
- package/dist/internal/duplicate-detection.d.ts.map +1 -0
- package/dist/internal/duplicate-detection.js +11 -0
- package/dist/internal/duplicate-detection.js.map +1 -0
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/tool.d.ts.map +1 -1
- package/dist/react/AssistantProvider.d.ts.map +1 -1
- package/dist/react/AssistantProvider.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/Tools.js +26 -15
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +5 -4
- package/dist/react/index.js +2 -2
- package/dist/react/model-context/toolbox.d.ts +29 -2
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/toolbox.js +18 -0
- package/dist/react/model-context/toolbox.js.map +1 -0
- package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantTool.js +6 -3
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.d.ts +6 -0
- package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +4 -2
- package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
- package/dist/react/model-context/useInlineRender.js.map +1 -1
- package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts +49 -7
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +28 -3
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +2 -7
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -1
- package/dist/react/runtimes/RuntimeAdapterProvider.js +6 -5
- package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -1
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts +1 -1
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
- package/dist/react/runtimes/external-message-converter.js +1 -0
- package/dist/react/runtimes/external-message-converter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts +7 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.d.ts.map +1 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js +21 -0
- package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -0
- package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useLocalRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/react/types/scopes/tools.d.ts +19 -2
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/react/utils/groupParts.d.ts +32 -11
- package/dist/react/utils/groupParts.d.ts.map +1 -1
- package/dist/react/utils/groupParts.js +13 -6
- package/dist/react/utils/groupParts.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/attachment-runtime.js.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/thread-list-item-runtime.d.ts.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-assistant-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +8 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +9 -1
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +34 -14
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.d.ts +1 -0
- package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
- 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 +31 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-shared-options.d.ts +8 -0
- package/dist/runtimes/external-store/external-store-shared-options.d.ts.map +1 -0
- package/dist/runtimes/external-store/external-store-shared-options.js +11 -0
- package/dist/runtimes/external-store/external-store-shared-options.js.map +1 -0
- 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.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +25 -2
- 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 +106 -26
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/thread-message-converter.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/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/adapter/in-memory.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +168 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +449 -0
- package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -0
- package/dist/subscribable/subscribable.d.ts.map +1 -1
- package/dist/subscribable/subscribable.js.map +1 -1
- package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
- package/dist/types/message.d.ts +6 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/id.d.ts +1 -3
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +1 -4
- package/dist/utils/id.js.map +1 -1
- package/package.json +10 -10
- package/src/adapters/index.ts +1 -4
- package/src/adapters/speech.ts +0 -1
- package/src/index.ts +12 -0
- package/src/internal/duplicate-detection.ts +26 -0
- package/src/internal.ts +0 -2
- package/src/model-context/frame/host.ts +0 -1
- package/src/model-context/frame/provider.ts +0 -1
- package/src/react/AssistantProvider.tsx +2 -3
- package/src/react/client/Interactables.ts +0 -1
- package/src/react/client/Tools.ts +50 -25
- package/src/react/index.ts +9 -8
- package/src/react/model-context/toolbox.ts +46 -1
- package/src/react/model-context/useAssistantTool.ts +8 -3
- package/src/react/model-context/useAssistantToolUI.ts +9 -2
- package/src/react/model-context/useInlineRender.ts +0 -1
- package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -2
- package/src/react/primitives/message/MessageAttachments.test.tsx +1 -1
- package/src/react/primitives/message/MessageGroupedParts.tsx +102 -13
- package/src/react/primitives/message/MessageParts.tsx +4 -7
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +0 -3
- package/src/react/runtimes/RuntimeAdapterProvider.tsx +12 -7
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +0 -3
- package/src/react/runtimes/external-message-converter.ts +5 -1
- package/src/react/runtimes/useExternalStoreSharedOptions.ts +23 -0
- package/src/react/runtimes/useLocalRuntime.ts +0 -10
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +0 -6
- package/src/react/types/scopes/tools.ts +20 -1
- package/src/react/utils/groupParts.ts +49 -18
- package/src/runtime/api/attachment-runtime.ts +1 -2
- package/src/runtime/interfaces/thread-runtime-core.ts +8 -0
- package/src/runtime/internal.ts +1 -4
- package/src/runtime/utils/message-repository.ts +57 -16
- package/src/runtime/utils/thread-message-like.ts +2 -0
- package/src/runtimes/external-store/external-store-adapter.ts +33 -0
- package/src/runtimes/external-store/external-store-shared-options.ts +18 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +1 -3
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -37
- package/src/runtimes/tool-invocations/EDGE_CASES.md +194 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +1054 -0
- package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +782 -0
- package/src/subscribable/subscribable.ts +3 -3
- package/src/tests/MessageRepository.test.ts +83 -52
- package/src/tests/OptimisticState-delete-crash.test.ts +2 -0
- package/src/tests/OptimisticState-list-race.test.ts +2 -4
- package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +5 -5
- package/src/tests/auiV0Encode.test.ts +1 -1
- package/src/tests/composer-can-send.test.ts +8 -4
- package/src/tests/duplicate-detection.test.ts +34 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +1 -1
- package/src/tests/external-store-thread-runtime-core.test.ts +112 -79
- package/src/tests/groupParts.test.ts +70 -0
- package/src/tests/no-unsafe-process-env.test.ts +1 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +2 -5
- package/src/tests/thread-message-like.test.ts +4 -1
- package/src/types/index.ts +1 -4
- package/src/types/message.ts +6 -0
- package/src/utils/id.ts +0 -4
- package/dist/react/runtimes/useToolInvocations.d.ts +0 -53
- package/dist/react/runtimes/useToolInvocations.d.ts.map +0 -1
- package/dist/react/runtimes/useToolInvocations.js +0 -380
- package/dist/react/runtimes/useToolInvocations.js.map +0 -1
- package/src/react/runtimes/useToolInvocations.ts +0 -694
|
@@ -207,9 +207,9 @@ export class LazyMemoizeSubject<TState extends object, TPath>
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
export class NestedSubscriptionSubject<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
TState extends Subscribable | undefined,
|
|
211
|
+
TPath,
|
|
212
|
+
>
|
|
213
213
|
extends BaseSubject
|
|
214
214
|
implements
|
|
215
215
|
SubscribableWithState<TState, TPath>,
|
|
@@ -7,20 +7,14 @@ import type { ThreadMessage } from "../types/message";
|
|
|
7
7
|
import type { TextMessagePart } from "../types/message";
|
|
8
8
|
import type { ThreadMessageLike } from "../runtime/utils/thread-message-like";
|
|
9
9
|
|
|
10
|
-
// Mock generateId
|
|
10
|
+
// Mock generateId to make tests deterministic
|
|
11
11
|
const mockGenerateId = vi.fn();
|
|
12
|
-
const mockGenerateOptimisticId = vi.fn();
|
|
13
|
-
const mockIsOptimisticId = vi.fn((id: string) =>
|
|
14
|
-
id.startsWith("__optimistic__"),
|
|
15
|
-
);
|
|
16
12
|
|
|
17
13
|
vi.mock("../utils/id", async (importOriginal) => {
|
|
18
14
|
const original = await importOriginal<typeof import("../utils/id")>();
|
|
19
15
|
return {
|
|
20
16
|
...original,
|
|
21
17
|
generateId: () => mockGenerateId(),
|
|
22
|
-
generateOptimisticId: () => mockGenerateOptimisticId(),
|
|
23
|
-
isOptimisticId: (id: string) => mockIsOptimisticId(id),
|
|
24
18
|
};
|
|
25
19
|
});
|
|
26
20
|
|
|
@@ -58,23 +52,11 @@ describe("MessageRepository", () => {
|
|
|
58
52
|
...overrides,
|
|
59
53
|
});
|
|
60
54
|
|
|
61
|
-
/**
|
|
62
|
-
* Creates a test CoreMessage with the given overrides.
|
|
63
|
-
*/
|
|
64
|
-
const createThreadMessageLike = (overrides = {}): ThreadMessageLike => ({
|
|
65
|
-
role: "assistant",
|
|
66
|
-
content: [{ type: "text", text: "Test message" }],
|
|
67
|
-
...overrides,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
55
|
beforeEach(() => {
|
|
71
56
|
repository = new MessageRepository();
|
|
72
57
|
// Reset mocks with predictable counter-based values
|
|
73
58
|
nextMockId = 1;
|
|
74
59
|
mockGenerateId.mockImplementation(() => `mock-id-${nextMockId++}`);
|
|
75
|
-
mockGenerateOptimisticId.mockImplementation(
|
|
76
|
-
() => `__optimistic__mock-id-${nextMockId++}`,
|
|
77
|
-
);
|
|
78
60
|
});
|
|
79
61
|
|
|
80
62
|
afterEach(() => {
|
|
@@ -297,53 +279,102 @@ describe("MessageRepository", () => {
|
|
|
297
279
|
});
|
|
298
280
|
|
|
299
281
|
describe("Optimistic messages", () => {
|
|
300
|
-
|
|
301
|
-
|
|
282
|
+
const optimistic = (overrides = {}) =>
|
|
283
|
+
createTestMessage({
|
|
284
|
+
status: { type: "running" },
|
|
285
|
+
metadata: {
|
|
286
|
+
unstable_state: null,
|
|
287
|
+
unstable_annotations: [],
|
|
288
|
+
unstable_data: [],
|
|
289
|
+
steps: [],
|
|
290
|
+
custom: {},
|
|
291
|
+
isOptimistic: true,
|
|
292
|
+
},
|
|
293
|
+
...overrides,
|
|
294
|
+
});
|
|
302
295
|
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
);
|
|
296
|
+
it("excludes optimistic messages from export()", () => {
|
|
297
|
+
const parent = createTestMessage({ id: "u" });
|
|
298
|
+
repository.addOrUpdateMessage(null, parent);
|
|
299
|
+
repository.addOrUpdateMessage("u", optimistic({ id: "placeholder" }));
|
|
300
|
+
repository.resetHead("placeholder");
|
|
308
301
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
302
|
+
const exported = repository.export();
|
|
303
|
+
|
|
304
|
+
expect(exported.messages.map((m) => m.message.id)).toEqual(["u"]);
|
|
305
|
+
// head was the optimistic placeholder; the exported head must fall back
|
|
306
|
+
// to the nearest persisted ancestor so it always resolves on import.
|
|
307
|
+
expect(exported.headId).toBe("u");
|
|
313
308
|
});
|
|
314
309
|
|
|
315
|
-
it("
|
|
316
|
-
const parent = createTestMessage({ id: "
|
|
310
|
+
it("round-trips through export/import without resurrecting the placeholder", () => {
|
|
311
|
+
const parent = createTestMessage({ id: "u" });
|
|
312
|
+
const real = createTestMessage({ id: "a" });
|
|
317
313
|
repository.addOrUpdateMessage(null, parent);
|
|
314
|
+
repository.addOrUpdateMessage("u", real);
|
|
315
|
+
repository.resetHead("a");
|
|
318
316
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
"parent-id",
|
|
322
|
-
coreMessage,
|
|
323
|
-
);
|
|
317
|
+
const restored = new MessageRepository();
|
|
318
|
+
restored.import(repository.export());
|
|
324
319
|
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
expect(restored.export().messages.map((m) => m.message.id)).toEqual([
|
|
321
|
+
"u",
|
|
322
|
+
"a",
|
|
323
|
+
]);
|
|
327
324
|
});
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
describe("HEAD-branch invariant", () => {
|
|
327
|
+
it("evicts an off-branch optimistic sibling when resetHead moves the head", () => {
|
|
328
|
+
// u -> { client_id (optimistic), server_id (optimistic) }. When the
|
|
329
|
+
// head moves to server_id, the dangling client_id sibling is evicted.
|
|
330
|
+
const parent = createTestMessage({ id: "u" });
|
|
331
|
+
repository.addOrUpdateMessage(null, parent);
|
|
332
|
+
repository.addOrUpdateMessage("u", optimistic({ id: "client_id" }));
|
|
333
|
+
repository.addOrUpdateMessage("u", optimistic({ id: "server_id" }));
|
|
334
|
+
|
|
335
|
+
repository.resetHead("server_id");
|
|
331
336
|
|
|
332
|
-
|
|
333
|
-
|
|
337
|
+
expect(repository.getBranches("server_id")).toEqual(["server_id"]);
|
|
338
|
+
expect(() => repository.getMessage("client_id")).toThrow();
|
|
334
339
|
});
|
|
335
|
-
repository.addOrUpdateMessage(null, existingMessage);
|
|
336
340
|
|
|
337
|
-
|
|
341
|
+
it("keeps the optimistic message that is on the head branch", () => {
|
|
342
|
+
const parent = createTestMessage({ id: "u" });
|
|
343
|
+
repository.addOrUpdateMessage(null, parent);
|
|
344
|
+
repository.addOrUpdateMessage("u", optimistic({ id: "a" }));
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
);
|
|
346
|
+
repository.resetHead("a");
|
|
347
|
+
|
|
348
|
+
expect(repository.getMessage("a").message.id).toBe("a");
|
|
349
|
+
});
|
|
344
350
|
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
it("never evicts real (non-optimistic) sibling branches", () => {
|
|
352
|
+
const parent = createTestMessage({ id: "u" });
|
|
353
|
+
repository.addOrUpdateMessage(null, parent);
|
|
354
|
+
repository.addOrUpdateMessage("u", createTestMessage({ id: "a1" }));
|
|
355
|
+
repository.addOrUpdateMessage("u", createTestMessage({ id: "a2" }));
|
|
356
|
+
|
|
357
|
+
repository.resetHead("a2");
|
|
358
|
+
|
|
359
|
+
// a1 is off the head branch but not optimistic, so it survives.
|
|
360
|
+
expect(repository.getBranches("a2")).toEqual(["a1", "a2"]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("evicts optimistic messages from the previous branch on switchToBranch", () => {
|
|
364
|
+
// Two real branches under u; the head branch (a2) carries an optimistic
|
|
365
|
+
// child. Switching to a1 must drop the optimistic message left on a2.
|
|
366
|
+
const parent = createTestMessage({ id: "u" });
|
|
367
|
+
repository.addOrUpdateMessage(null, parent);
|
|
368
|
+
repository.addOrUpdateMessage("u", createTestMessage({ id: "a1" }));
|
|
369
|
+
repository.addOrUpdateMessage("u", createTestMessage({ id: "a2" }));
|
|
370
|
+
repository.addOrUpdateMessage("a2", optimistic({ id: "opt" }));
|
|
371
|
+
repository.resetHead("opt");
|
|
372
|
+
|
|
373
|
+
repository.switchToBranch("a1");
|
|
374
|
+
|
|
375
|
+
expect(() => repository.getMessage("opt")).toThrow();
|
|
376
|
+
expect(repository.getBranches("a1")).toEqual(["a1", "a2"]);
|
|
377
|
+
});
|
|
347
378
|
});
|
|
348
379
|
});
|
|
349
380
|
|
|
@@ -24,6 +24,8 @@ const createThreadState = (
|
|
|
24
24
|
const mappingId = createThreadMappingId(threadId);
|
|
25
25
|
return {
|
|
26
26
|
isLoading: false,
|
|
27
|
+
isLoadingMore: false,
|
|
28
|
+
cursor: undefined,
|
|
27
29
|
newThreadId: undefined,
|
|
28
30
|
threadIds: status === "regular" ? [threadId] : [],
|
|
29
31
|
archivedThreadIds: status === "archived" ? [threadId] : [],
|
|
@@ -22,6 +22,8 @@ import { deferred } from "./remote-thread-list-test-helpers";
|
|
|
22
22
|
|
|
23
23
|
const EMPTY_STATE: RemoteThreadState = {
|
|
24
24
|
isLoading: false,
|
|
25
|
+
isLoadingMore: false,
|
|
26
|
+
cursor: undefined,
|
|
25
27
|
newThreadId: undefined,
|
|
26
28
|
threadIds: [],
|
|
27
29
|
archivedThreadIds: [],
|
|
@@ -87,7 +89,6 @@ describe("list + delete race condition", () => {
|
|
|
87
89
|
const listPromise = state.optimisticUpdate({
|
|
88
90
|
execute: () => listDeferred.promise,
|
|
89
91
|
loading: (s) => ({ ...s, isLoading: true }),
|
|
90
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
91
92
|
then: applyListResult,
|
|
92
93
|
});
|
|
93
94
|
|
|
@@ -145,7 +146,6 @@ describe("list + delete race condition", () => {
|
|
|
145
146
|
const listPromise = state.optimisticUpdate({
|
|
146
147
|
execute: () => listDeferred.promise,
|
|
147
148
|
loading: (s) => ({ ...s, isLoading: true }),
|
|
148
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
149
149
|
then: applyListResult,
|
|
150
150
|
});
|
|
151
151
|
|
|
@@ -183,7 +183,6 @@ describe("list + delete race condition", () => {
|
|
|
183
183
|
const listPromise = state.optimisticUpdate({
|
|
184
184
|
execute: () => listDeferred.promise,
|
|
185
185
|
loading: (s) => ({ ...s, isLoading: true }),
|
|
186
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
187
186
|
then: applyListResult,
|
|
188
187
|
});
|
|
189
188
|
|
|
@@ -221,7 +220,6 @@ describe("list + delete race condition", () => {
|
|
|
221
220
|
const listPromise = state.optimisticUpdate({
|
|
222
221
|
execute: () => listDeferred.promise,
|
|
223
222
|
loading: (s) => ({ ...s, isLoading: true }),
|
|
224
|
-
// biome-ignore lint/suspicious/noThenProperty: OptimisticState reducer pattern
|
|
225
223
|
then: applyListResult,
|
|
226
224
|
});
|
|
227
225
|
|
|
@@ -20,7 +20,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
20
20
|
|
|
21
21
|
it("initial list response with nextCursor sets hasMore=true", async () => {
|
|
22
22
|
const adapter = makeAdapter({
|
|
23
|
-
list: vi.fn(async () => ({
|
|
23
|
+
list: vi.fn<ListFn>(async () => ({
|
|
24
24
|
threads: [
|
|
25
25
|
{
|
|
26
26
|
status: "regular",
|
|
@@ -41,7 +41,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
41
41
|
|
|
42
42
|
it("absent nextCursor leaves hasMore=false", async () => {
|
|
43
43
|
const adapter = makeAdapter({
|
|
44
|
-
list: vi.fn(async () => ({
|
|
44
|
+
list: vi.fn<ListFn>(async () => ({
|
|
45
45
|
threads: [
|
|
46
46
|
{
|
|
47
47
|
status: "regular",
|
|
@@ -243,7 +243,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
243
243
|
|
|
244
244
|
it("__internal_setOptions clears cursor and dedup handles on adapter swap, then refetches via the new adapter", async () => {
|
|
245
245
|
const firstAdapter = makeAdapter({
|
|
246
|
-
list: vi.fn(async () => ({
|
|
246
|
+
list: vi.fn<ListFn>(async () => ({
|
|
247
247
|
threads: [{ status: "regular", remoteId: "old", externalId: "old" }],
|
|
248
248
|
nextCursor: "old-cursor",
|
|
249
249
|
})),
|
|
@@ -253,7 +253,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
253
253
|
expect(core.threadIds).toEqual(["old"]);
|
|
254
254
|
expect(core.hasMore).toBe(true);
|
|
255
255
|
|
|
256
|
-
const secondList = vi.fn(async () => ({
|
|
256
|
+
const secondList = vi.fn<ListFn>(async () => ({
|
|
257
257
|
threads: [{ status: "regular", remoteId: "new", externalId: "new" }],
|
|
258
258
|
}));
|
|
259
259
|
const secondAdapter = makeAdapter({ list: secondList });
|
|
@@ -290,7 +290,7 @@ describe("RemoteThreadListThreadListRuntimeCore.loadMore", () => {
|
|
|
290
290
|
const stale = core.loadMore();
|
|
291
291
|
|
|
292
292
|
const secondAdapter = makeAdapter({
|
|
293
|
-
list: vi.fn(async () => ({
|
|
293
|
+
list: vi.fn<ListFn>(async () => ({
|
|
294
294
|
threads: [{ status: "regular", remoteId: "ignored", externalId: "x" }],
|
|
295
295
|
})),
|
|
296
296
|
});
|
|
@@ -102,10 +102,14 @@ describe("BaseComposerRuntimeCore.send", () => {
|
|
|
102
102
|
describe("DefaultEditComposerRuntimeCore.canSend", () => {
|
|
103
103
|
it("ignores runtime.isSendDisabled (thread-scoped flag does not block edits)", () => {
|
|
104
104
|
const stub = makeRuntimeStub({ isSendDisabled: true });
|
|
105
|
-
const composer = new DefaultEditComposerRuntimeCore(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const composer = new DefaultEditComposerRuntimeCore(
|
|
106
|
+
stub as unknown as ThreadRuntimeCore,
|
|
107
|
+
() => {},
|
|
108
|
+
{
|
|
109
|
+
parentId: null,
|
|
110
|
+
message: makeUserMessage("seed"),
|
|
111
|
+
},
|
|
112
|
+
);
|
|
109
113
|
|
|
110
114
|
expect(composer.canSend).toBe(true);
|
|
111
115
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { checkDuplicateCore } from "../internal/duplicate-detection";
|
|
3
|
+
|
|
4
|
+
const KEY = Symbol.for("@assistant-ui/core.loaded");
|
|
5
|
+
|
|
6
|
+
function reset(): void {
|
|
7
|
+
delete (globalThis as unknown as Record<symbol, unknown>)[KEY];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("checkDuplicateCore", () => {
|
|
11
|
+
let warn: ReturnType<typeof vi.spyOn>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reset();
|
|
15
|
+
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
warn.mockRestore();
|
|
20
|
+
reset();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not warn on the first load", () => {
|
|
24
|
+
checkDuplicateCore();
|
|
25
|
+
expect(warn).not.toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("warns when a second copy registers in the same runtime", () => {
|
|
29
|
+
checkDuplicateCore();
|
|
30
|
+
checkDuplicateCore();
|
|
31
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
32
|
+
expect(warn.mock.calls[0]![0]).toMatch(/npx assistant-ui doctor/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -184,7 +184,7 @@ describe("ExternalStoreThreadListRuntimeCore - isMain via ThreadListRuntimeImpl"
|
|
|
184
184
|
const core = new ExternalStoreThreadListRuntimeCore(adapter, makeFactory());
|
|
185
185
|
return new ThreadListRuntimeImpl(
|
|
186
186
|
core,
|
|
187
|
-
NoopThreadRuntime as unknown as
|
|
187
|
+
NoopThreadRuntime as unknown as ConstructorParameters<
|
|
188
188
|
typeof ThreadListRuntimeImpl
|
|
189
189
|
>[1],
|
|
190
190
|
);
|
|
@@ -2,18 +2,20 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import { ExternalStoreThreadRuntimeCore } from "../runtimes/external-store/external-store-thread-runtime-core";
|
|
3
3
|
import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
|
|
4
4
|
import type { ModelContextProvider } from "../model-context/types";
|
|
5
|
+
import type { ThreadMessageLike } from "../runtime/utils/thread-message-like";
|
|
5
6
|
|
|
6
7
|
const mockContextProvider: ModelContextProvider = {
|
|
7
8
|
getModelContext: () => ({}),
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
const makeStore = (
|
|
11
|
-
overrides?: Partial<ExternalStoreAdapter>,
|
|
12
|
-
): ExternalStoreAdapter =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
overrides?: Partial<ExternalStoreAdapter> | Record<string, unknown>,
|
|
13
|
+
): ExternalStoreAdapter =>
|
|
14
|
+
({
|
|
15
|
+
messages: [],
|
|
16
|
+
onNew: vi.fn(),
|
|
17
|
+
...overrides,
|
|
18
|
+
}) as ExternalStoreAdapter;
|
|
17
19
|
|
|
18
20
|
describe("ExternalStoreThreadRuntimeCore - state reference stability", () => {
|
|
19
21
|
describe("capabilities", () => {
|
|
@@ -150,117 +152,148 @@ describe("ExternalStoreThreadRuntimeCore - state reference stability", () => {
|
|
|
150
152
|
expect(runtime.capabilities).toBe(capsBefore);
|
|
151
153
|
});
|
|
152
154
|
});
|
|
155
|
+
describe("ExternalStoreThreadRuntimeCore - optimistic message reconciliation", () => {
|
|
156
|
+
type Raw = {
|
|
157
|
+
id: string;
|
|
158
|
+
role: "user" | "assistant";
|
|
159
|
+
text: string;
|
|
160
|
+
optimistic?: boolean;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const convertMessage = (m: Raw): ThreadMessageLike => ({
|
|
164
|
+
id: m.id,
|
|
165
|
+
role: m.role,
|
|
166
|
+
content: [{ type: "text", text: m.text }],
|
|
167
|
+
...(m.optimistic && { metadata: { isOptimistic: true } }),
|
|
168
|
+
});
|
|
153
169
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
const childrenOf = (
|
|
171
|
+
runtime: ExternalStoreThreadRuntimeCore,
|
|
172
|
+
parentId: string,
|
|
173
|
+
) =>
|
|
174
|
+
runtime
|
|
175
|
+
.export()
|
|
176
|
+
.messages.filter((m) => m.parentId === parentId)
|
|
177
|
+
.map((m) => m.message.id);
|
|
160
178
|
|
|
179
|
+
it("drops the orphaned placeholder when an optimistic id is swapped mid-run", () => {
|
|
180
|
+
const u: Raw = { id: "u", role: "user", text: "hi" };
|
|
161
181
|
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
162
182
|
mockContextProvider,
|
|
163
|
-
makeStore({
|
|
183
|
+
makeStore({
|
|
184
|
+
messages: [
|
|
185
|
+
u,
|
|
186
|
+
{ id: "client_id", role: "assistant", text: "", optimistic: true },
|
|
187
|
+
],
|
|
188
|
+
convertMessage,
|
|
189
|
+
isRunning: true,
|
|
190
|
+
}),
|
|
164
191
|
);
|
|
165
192
|
|
|
166
|
-
|
|
193
|
+
// AI SDK v6 swaps the client-generated id for the server-provided one.
|
|
194
|
+
runtime.__internal_setAdapter(
|
|
195
|
+
makeStore({
|
|
196
|
+
messages: [
|
|
197
|
+
u,
|
|
198
|
+
{
|
|
199
|
+
id: "server_id",
|
|
200
|
+
role: "assistant",
|
|
201
|
+
text: "hello",
|
|
202
|
+
optimistic: true,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
convertMessage,
|
|
206
|
+
isRunning: true,
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
167
209
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.map((m) => m.message.id);
|
|
173
|
-
expect(userChildren).toEqual(["a2"]);
|
|
210
|
+
// No phantom sibling in the live tree (what BranchPicker reads): the user
|
|
211
|
+
// message has a single child. export() omits the still-optimistic
|
|
212
|
+
// streaming message, so assert against the live getBranches instead.
|
|
213
|
+
expect(runtime.getBranches("server_id")).toEqual(["server_id"]);
|
|
174
214
|
});
|
|
175
215
|
|
|
176
|
-
it("
|
|
177
|
-
const
|
|
178
|
-
|
|
216
|
+
it("clears the optimistic flag once the run settles", () => {
|
|
217
|
+
const u: Raw = { id: "u", role: "user", text: "hi" };
|
|
179
218
|
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
180
219
|
mockContextProvider,
|
|
181
|
-
makeStore({
|
|
220
|
+
makeStore({
|
|
221
|
+
messages: [
|
|
222
|
+
u,
|
|
223
|
+
{ id: "a", role: "assistant", text: "...", optimistic: true },
|
|
224
|
+
],
|
|
225
|
+
convertMessage,
|
|
226
|
+
isRunning: true,
|
|
227
|
+
}),
|
|
182
228
|
);
|
|
183
229
|
|
|
184
|
-
runtime.__internal_setAdapter(
|
|
230
|
+
runtime.__internal_setAdapter(
|
|
231
|
+
makeStore({
|
|
232
|
+
messages: [u, { id: "a", role: "assistant", text: "done" }],
|
|
233
|
+
convertMessage,
|
|
234
|
+
isRunning: false,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
185
237
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
]);
|
|
238
|
+
const settled = runtime.export().messages.find((m) => m.message.id === "a");
|
|
239
|
+
expect(settled?.message.metadata.isOptimistic).toBeFalsy();
|
|
240
|
+
expect(childrenOf(runtime, "u")).toEqual(["a"]);
|
|
190
241
|
});
|
|
191
242
|
|
|
192
|
-
it("removes
|
|
193
|
-
const
|
|
194
|
-
const u2 = { id: "u2", role: "user" as const, content: [] };
|
|
195
|
-
|
|
243
|
+
it("removes the runtime placeholder once the store provides the assistant message", () => {
|
|
244
|
+
const u: Raw = { id: "u", role: "user", text: "hi" };
|
|
196
245
|
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
197
246
|
mockContextProvider,
|
|
198
|
-
makeStore({ messages: [
|
|
247
|
+
makeStore({ messages: [u], convertMessage, isRunning: true }),
|
|
199
248
|
);
|
|
200
249
|
|
|
201
|
-
|
|
250
|
+
// Running with a trailing user message: a placeholder is appended to the
|
|
251
|
+
// live tree. export() omits optimistic messages, so inspect the live
|
|
252
|
+
// messages (which include the placeholder on the head path). It's a plain
|
|
253
|
+
// assistant message flagged optimistic (no special id scheme).
|
|
254
|
+
const live = runtime.messages;
|
|
255
|
+
expect(live).toHaveLength(2);
|
|
256
|
+
expect(live[1]!.role).toBe("assistant");
|
|
257
|
+
expect(live[1]!.metadata.isOptimistic).toBe(true);
|
|
258
|
+
|
|
259
|
+
// The store now yields the real assistant message; the placeholder (whose
|
|
260
|
+
// synthetic id never appears in the snapshot) must be gone, leaving a
|
|
261
|
+
// single child under the user message.
|
|
262
|
+
runtime.__internal_setAdapter(
|
|
263
|
+
makeStore({
|
|
264
|
+
messages: [u, { id: "a", role: "assistant", text: "done" }],
|
|
265
|
+
convertMessage,
|
|
266
|
+
isRunning: false,
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
202
269
|
|
|
203
270
|
expect(runtime.export().messages.map((m) => m.message.id)).toEqual([
|
|
204
271
|
"u",
|
|
205
272
|
"a",
|
|
206
273
|
]);
|
|
274
|
+
expect(runtime.getBranches("a")).toEqual(["a"]);
|
|
207
275
|
});
|
|
208
276
|
|
|
209
|
-
it("
|
|
210
|
-
const
|
|
211
|
-
id: "u",
|
|
212
|
-
role: "user" as const,
|
|
213
|
-
content: [{ type: "text" as const, text: "hi" }],
|
|
214
|
-
};
|
|
215
|
-
|
|
277
|
+
it("keeps real sibling branches that were never flagged optimistic", () => {
|
|
278
|
+
const u: Raw = { id: "u", role: "user", text: "hi" };
|
|
216
279
|
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
217
280
|
mockContextProvider,
|
|
218
281
|
makeStore({
|
|
219
|
-
messages: [
|
|
220
|
-
|
|
221
|
-
isRunning: true,
|
|
222
|
-
}),
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
runtime.cancelRun();
|
|
226
|
-
|
|
227
|
-
expect(() => {
|
|
228
|
-
runtime.__internal_setAdapter(makeStore({ messages: [] }));
|
|
229
|
-
}).not.toThrow();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("drops phantom sibling when convertMessage swaps the assistant id", () => {
|
|
233
|
-
type Raw = { id: string; role: "user" | "assistant"; text: string };
|
|
234
|
-
const rawU: Raw = { id: "u", role: "user", text: "hi" };
|
|
235
|
-
const rawA1: Raw = { id: "client_id", role: "assistant", text: "" };
|
|
236
|
-
const rawA2: Raw = { id: "server_id", role: "assistant", text: "" };
|
|
237
|
-
|
|
238
|
-
const convertMessage = (m: Raw) => ({
|
|
239
|
-
id: m.id,
|
|
240
|
-
role: m.role,
|
|
241
|
-
content: [{ type: "text" as const, text: m.text }],
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const runtime = new ExternalStoreThreadRuntimeCore(
|
|
245
|
-
mockContextProvider,
|
|
246
|
-
makeStore({
|
|
247
|
-
messages: [rawU, rawA1] as any,
|
|
248
|
-
convertMessage: convertMessage as any,
|
|
282
|
+
messages: [u, { id: "a1", role: "assistant", text: "first" }],
|
|
283
|
+
convertMessage,
|
|
249
284
|
}),
|
|
250
285
|
);
|
|
251
286
|
|
|
287
|
+
// Simulates onEdit/onReload producing a new branch under the same parent;
|
|
288
|
+
// the prior branch must survive (regression guard for #4131).
|
|
252
289
|
runtime.__internal_setAdapter(
|
|
253
290
|
makeStore({
|
|
254
|
-
messages: [
|
|
255
|
-
convertMessage
|
|
291
|
+
messages: [u, { id: "a2", role: "assistant", text: "second" }],
|
|
292
|
+
convertMessage,
|
|
256
293
|
}),
|
|
257
294
|
);
|
|
258
295
|
|
|
259
|
-
|
|
260
|
-
.export()
|
|
261
|
-
.messages.filter((m) => m.parentId === "u")
|
|
262
|
-
.map((m) => m.message.id);
|
|
263
|
-
expect(userChildren).toEqual(["server_id"]);
|
|
296
|
+
expect(childrenOf(runtime, "u")).toEqual(["a1", "a2"]);
|
|
264
297
|
});
|
|
265
298
|
});
|
|
266
299
|
|