@assistant-ui/core 0.2.18 → 0.2.20
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.d.ts.map +1 -1
- package/dist/react/primitives/generativeUI/GenerativeUI.js +2 -2
- package/dist/react/primitives/generativeUI/GenerativeUI.js.map +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/generativeUI/GenerativeUI.tsx +5 -1
- 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
|
@@ -189,6 +189,16 @@ export class MessageRepository {
|
|
|
189
189
|
return this.head?.current.id ?? null;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
get canonicalHeadId() {
|
|
193
|
+
// Optimistic messages are ephemeral, so persisted callers need the nearest
|
|
194
|
+
// non-optimistic ancestor rather than the raw head.
|
|
195
|
+
let head = this.head;
|
|
196
|
+
while (head?.current.metadata?.isOptimistic) {
|
|
197
|
+
head = head.prev;
|
|
198
|
+
}
|
|
199
|
+
return head?.current.id ?? null;
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
getMessages(headId?: string) {
|
|
193
203
|
if (headId === undefined || headId === this.head?.current.id) {
|
|
194
204
|
return this._messages.value;
|
|
@@ -436,14 +446,8 @@ export class MessageRepository {
|
|
|
436
446
|
});
|
|
437
447
|
}
|
|
438
448
|
|
|
439
|
-
// The head may itself be optimistic; walk up to the nearest persisted ancestor.
|
|
440
|
-
let head = this.head;
|
|
441
|
-
while (head?.current.metadata?.isOptimistic) {
|
|
442
|
-
head = head.prev;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
449
|
return {
|
|
446
|
-
headId:
|
|
450
|
+
headId: this.canonicalHeadId,
|
|
447
451
|
messages: exportItems,
|
|
448
452
|
};
|
|
449
453
|
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
stepStreamingTiming,
|
|
4
|
+
type StreamingTimingAccessors,
|
|
5
|
+
} from "./streaming-timing";
|
|
6
|
+
|
|
7
|
+
type TestMessage = {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly role: "assistant" | "user";
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly toolCalls: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const accessors: StreamingTimingAccessors<TestMessage> = {
|
|
15
|
+
getAssistantMessageId: (messages) => {
|
|
16
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
17
|
+
if (messages[i]!.role === "assistant") return messages[i]!.id;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
},
|
|
21
|
+
getTextLength: (messages, id) =>
|
|
22
|
+
messages.find((m) => m.id === id)?.text.length ?? 0,
|
|
23
|
+
getToolCallCount: (messages, id) =>
|
|
24
|
+
messages.find((m) => m.id === id)?.toolCalls ?? 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const makeClock = () => {
|
|
28
|
+
let t = 0;
|
|
29
|
+
return () => (t += 10);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const msg = (id: string, text: string, toolCalls = 0): TestMessage => ({
|
|
33
|
+
id,
|
|
34
|
+
role: "assistant",
|
|
35
|
+
text,
|
|
36
|
+
toolCalls,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("stepStreamingTiming", () => {
|
|
40
|
+
it("is idle when not running and there is no state", () => {
|
|
41
|
+
const now = makeClock();
|
|
42
|
+
expect(
|
|
43
|
+
stepStreamingTiming(null, [], false, accessors, undefined, now),
|
|
44
|
+
).toEqual({ state: null, timings: {} });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("starts tracking on the first running update and counts content growth as chunks", () => {
|
|
48
|
+
const now = makeClock();
|
|
49
|
+
const r1 = stepStreamingTiming(
|
|
50
|
+
null,
|
|
51
|
+
[msg("m1", "")],
|
|
52
|
+
true,
|
|
53
|
+
accessors,
|
|
54
|
+
undefined,
|
|
55
|
+
now,
|
|
56
|
+
);
|
|
57
|
+
// No growth yet (length 0), state is initialised but no chunks.
|
|
58
|
+
expect(r1.timings).toEqual({});
|
|
59
|
+
expect(r1.state).toMatchObject({ messageId: "m1", totalChunks: 0 });
|
|
60
|
+
|
|
61
|
+
const r2 = stepStreamingTiming(
|
|
62
|
+
r1.state,
|
|
63
|
+
[msg("m1", "Hello")],
|
|
64
|
+
true,
|
|
65
|
+
accessors,
|
|
66
|
+
undefined,
|
|
67
|
+
now,
|
|
68
|
+
);
|
|
69
|
+
expect(r2.state).toMatchObject({
|
|
70
|
+
messageId: "m1",
|
|
71
|
+
totalChunks: 1,
|
|
72
|
+
lastContentLength: 5,
|
|
73
|
+
firstTokenTime: 10,
|
|
74
|
+
});
|
|
75
|
+
expect(r2.timings).toEqual({});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("finalizes timing when streaming stops and counts the final chunk", () => {
|
|
79
|
+
const now = makeClock();
|
|
80
|
+
const r1 = stepStreamingTiming(
|
|
81
|
+
null,
|
|
82
|
+
[msg("m1", "a")],
|
|
83
|
+
true,
|
|
84
|
+
accessors,
|
|
85
|
+
undefined,
|
|
86
|
+
now,
|
|
87
|
+
);
|
|
88
|
+
const r2 = stepStreamingTiming(
|
|
89
|
+
r1.state,
|
|
90
|
+
[msg("m1", "ab")],
|
|
91
|
+
true,
|
|
92
|
+
accessors,
|
|
93
|
+
undefined,
|
|
94
|
+
now,
|
|
95
|
+
);
|
|
96
|
+
// Final content delta arrives in the SAME update as isRunning -> false.
|
|
97
|
+
// The finalize re-reads the final length (3) rather than the last delta.
|
|
98
|
+
const r3 = stepStreamingTiming(
|
|
99
|
+
r2.state,
|
|
100
|
+
[msg("m1", "abc")],
|
|
101
|
+
false,
|
|
102
|
+
accessors,
|
|
103
|
+
undefined,
|
|
104
|
+
now,
|
|
105
|
+
);
|
|
106
|
+
expect(r3.state).toBeNull();
|
|
107
|
+
const timing = r3.timings["m1"]!;
|
|
108
|
+
expect(timing).toBeDefined();
|
|
109
|
+
// The final delta ("abc", arrived with the stop signal) is reconciled,
|
|
110
|
+
// so totalChunks and tokenCount count the same three-character content.
|
|
111
|
+
expect(timing.totalChunks).toBe(3);
|
|
112
|
+
expect(timing.tokenCount).toBe(Math.ceil(3 / 4));
|
|
113
|
+
expect(timing.toolCallCount).toBe(0);
|
|
114
|
+
expect(timing.tokensPerSecond).toBeCloseTo(
|
|
115
|
+
timing.tokenCount! / (timing.totalStreamTime! / 1000),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("reconciles totalChunks and tokenCount when the only growth lands at finalize", () => {
|
|
120
|
+
const now = makeClock();
|
|
121
|
+
// Running the whole time with empty content; the text arrives only with
|
|
122
|
+
// the stop signal.
|
|
123
|
+
const r1 = stepStreamingTiming(
|
|
124
|
+
null,
|
|
125
|
+
[msg("m1", "")],
|
|
126
|
+
true,
|
|
127
|
+
accessors,
|
|
128
|
+
undefined,
|
|
129
|
+
now,
|
|
130
|
+
);
|
|
131
|
+
const r2 = stepStreamingTiming(
|
|
132
|
+
r1.state,
|
|
133
|
+
[msg("m1", "done")],
|
|
134
|
+
false,
|
|
135
|
+
accessors,
|
|
136
|
+
undefined,
|
|
137
|
+
now,
|
|
138
|
+
);
|
|
139
|
+
const timing = r2.timings["m1"]!;
|
|
140
|
+
expect(timing.totalChunks).toBe(1);
|
|
141
|
+
expect(timing.tokenCount).toBe(Math.ceil(4 / 4));
|
|
142
|
+
expect(timing.firstTokenTime).toBe(timing.totalStreamTime);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("does not finalize while still running", () => {
|
|
146
|
+
const now = makeClock();
|
|
147
|
+
const r1 = stepStreamingTiming(
|
|
148
|
+
null,
|
|
149
|
+
[msg("m1", "content")],
|
|
150
|
+
true,
|
|
151
|
+
accessors,
|
|
152
|
+
undefined,
|
|
153
|
+
now,
|
|
154
|
+
);
|
|
155
|
+
const r2 = stepStreamingTiming(
|
|
156
|
+
r1.state,
|
|
157
|
+
[msg("m1", "content")],
|
|
158
|
+
true,
|
|
159
|
+
accessors,
|
|
160
|
+
undefined,
|
|
161
|
+
now,
|
|
162
|
+
);
|
|
163
|
+
expect(r2.timings).toEqual({});
|
|
164
|
+
expect(r2.state).not.toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("resets tracking when the assistant message id changes mid-stream", () => {
|
|
168
|
+
const now = makeClock();
|
|
169
|
+
const r1 = stepStreamingTiming(
|
|
170
|
+
null,
|
|
171
|
+
[msg("m1", "hi")],
|
|
172
|
+
true,
|
|
173
|
+
accessors,
|
|
174
|
+
undefined,
|
|
175
|
+
now,
|
|
176
|
+
);
|
|
177
|
+
const r2 = stepStreamingTiming(
|
|
178
|
+
r1.state,
|
|
179
|
+
[msg("m1", "hi"), msg("m2", "")],
|
|
180
|
+
true,
|
|
181
|
+
accessors,
|
|
182
|
+
undefined,
|
|
183
|
+
now,
|
|
184
|
+
);
|
|
185
|
+
expect(r2.state?.messageId).toBe("m2");
|
|
186
|
+
expect(r2.state?.totalChunks).toBe(0);
|
|
187
|
+
expect(r2.state?.lastContentLength).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("records firstTokenTime only once, at the first content growth", () => {
|
|
191
|
+
const now = makeClock();
|
|
192
|
+
const r1 = stepStreamingTiming(
|
|
193
|
+
null,
|
|
194
|
+
[msg("m1", "")],
|
|
195
|
+
true,
|
|
196
|
+
accessors,
|
|
197
|
+
undefined,
|
|
198
|
+
now,
|
|
199
|
+
);
|
|
200
|
+
const r2 = stepStreamingTiming(
|
|
201
|
+
r1.state,
|
|
202
|
+
[msg("m1", "first")],
|
|
203
|
+
true,
|
|
204
|
+
accessors,
|
|
205
|
+
undefined,
|
|
206
|
+
now,
|
|
207
|
+
);
|
|
208
|
+
expect(r2.state?.firstTokenTime).toBe(10);
|
|
209
|
+
const r3 = stepStreamingTiming(
|
|
210
|
+
r2.state,
|
|
211
|
+
[msg("m1", "first second")],
|
|
212
|
+
true,
|
|
213
|
+
accessors,
|
|
214
|
+
undefined,
|
|
215
|
+
now,
|
|
216
|
+
);
|
|
217
|
+
// firstTokenTime unchanged on later growth.
|
|
218
|
+
expect(r3.state?.firstTokenTime).toBe(10);
|
|
219
|
+
expect(r3.state?.totalChunks).toBe(2);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("counts tool calls from the final message", () => {
|
|
223
|
+
const now = makeClock();
|
|
224
|
+
const r1 = stepStreamingTiming(
|
|
225
|
+
null,
|
|
226
|
+
[msg("m1", "a", 2)],
|
|
227
|
+
true,
|
|
228
|
+
accessors,
|
|
229
|
+
undefined,
|
|
230
|
+
now,
|
|
231
|
+
);
|
|
232
|
+
const r2 = stepStreamingTiming(
|
|
233
|
+
r1.state,
|
|
234
|
+
[msg("m1", "ab", 2)],
|
|
235
|
+
false,
|
|
236
|
+
accessors,
|
|
237
|
+
undefined,
|
|
238
|
+
now,
|
|
239
|
+
);
|
|
240
|
+
expect(r2.timings["m1"]?.toolCallCount).toBe(2);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("omits tokenCount and tokensPerSecond when the final text is empty", () => {
|
|
244
|
+
const now = makeClock();
|
|
245
|
+
const r1 = stepStreamingTiming(
|
|
246
|
+
null,
|
|
247
|
+
[msg("m1", "")],
|
|
248
|
+
true,
|
|
249
|
+
accessors,
|
|
250
|
+
undefined,
|
|
251
|
+
now,
|
|
252
|
+
);
|
|
253
|
+
const r2 = stepStreamingTiming(
|
|
254
|
+
r1.state,
|
|
255
|
+
[msg("m1", "")],
|
|
256
|
+
false,
|
|
257
|
+
accessors,
|
|
258
|
+
undefined,
|
|
259
|
+
now,
|
|
260
|
+
);
|
|
261
|
+
const timing = r2.timings["m1"]!;
|
|
262
|
+
expect(timing.tokenCount).toBeUndefined();
|
|
263
|
+
expect(timing.tokensPerSecond).toBeUndefined();
|
|
264
|
+
expect(timing.totalChunks).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("respects a custom estimateTokens option", () => {
|
|
268
|
+
const now = makeClock();
|
|
269
|
+
const r1 = stepStreamingTiming(
|
|
270
|
+
null,
|
|
271
|
+
[msg("m1", "abcdefgh")],
|
|
272
|
+
true,
|
|
273
|
+
accessors,
|
|
274
|
+
{ estimateTokens: (n) => Math.floor(n / 2) },
|
|
275
|
+
now,
|
|
276
|
+
);
|
|
277
|
+
const r2 = stepStreamingTiming(
|
|
278
|
+
r1.state,
|
|
279
|
+
[msg("m1", "abcdefgh")],
|
|
280
|
+
false,
|
|
281
|
+
accessors,
|
|
282
|
+
{ estimateTokens: (n) => Math.floor(n / 2) },
|
|
283
|
+
now,
|
|
284
|
+
);
|
|
285
|
+
expect(r2.timings["m1"]?.tokenCount).toBe(4);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("produces no timings when running but no assistant message is present", () => {
|
|
289
|
+
const now = makeClock();
|
|
290
|
+
const r = stepStreamingTiming(
|
|
291
|
+
null,
|
|
292
|
+
[{ id: "u1", role: "user", text: "hi", toolCalls: 0 }],
|
|
293
|
+
true,
|
|
294
|
+
accessors,
|
|
295
|
+
undefined,
|
|
296
|
+
now,
|
|
297
|
+
);
|
|
298
|
+
expect(r.state).toBeNull();
|
|
299
|
+
expect(r.timings).toEqual({});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { MessageTiming } from "../../types/message";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shape-specific accessors that adapt {@link stepStreamingTiming} /
|
|
5
|
+
* {@link useStreamingTiming} to a runtime's message list. Each adapter
|
|
6
|
+
* provides three pure functions over its own message type; the timing state
|
|
7
|
+
* machine itself lives once in core.
|
|
8
|
+
*/
|
|
9
|
+
export type StreamingTimingAccessors<TMessage> = {
|
|
10
|
+
/** Resolve the id of the last assistant message, or `undefined` if none. */
|
|
11
|
+
readonly getAssistantMessageId: (
|
|
12
|
+
messages: readonly TMessage[],
|
|
13
|
+
) => string | undefined;
|
|
14
|
+
/** Total text length (incl. reasoning/thinking) of the assistant message. */
|
|
15
|
+
readonly getTextLength: (
|
|
16
|
+
messages: readonly TMessage[],
|
|
17
|
+
messageId: string,
|
|
18
|
+
) => number;
|
|
19
|
+
/** Number of tool calls recorded on the assistant message. */
|
|
20
|
+
readonly getToolCallCount: (
|
|
21
|
+
messages: readonly TMessage[],
|
|
22
|
+
messageId: string,
|
|
23
|
+
) => number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StreamingTimingOptions = {
|
|
27
|
+
/**
|
|
28
|
+
* Estimate token count from a text length. Defaults to `Math.ceil(n / 4)`.
|
|
29
|
+
* Adapters with real usage data can override this.
|
|
30
|
+
*/
|
|
31
|
+
readonly estimateTokens?: (textLength: number) => number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mutable tracking state for a single in-flight assistant message. Kept
|
|
36
|
+
* between updates by the caller (a ref in the React hook); `null` means no
|
|
37
|
+
* message is being tracked.
|
|
38
|
+
*
|
|
39
|
+
* `totalChunks` counts content growth deltas observed while streaming. The
|
|
40
|
+
* final delta that lands in the same update as the `isRunning -> false`
|
|
41
|
+
* transition is reconciled at finalize, so `totalChunks` and the token
|
|
42
|
+
* estimate (derived from final length) count the same content.
|
|
43
|
+
*/
|
|
44
|
+
export type StreamingTimingState = {
|
|
45
|
+
readonly messageId: string;
|
|
46
|
+
readonly startTime: number;
|
|
47
|
+
readonly firstTokenTime?: number;
|
|
48
|
+
readonly lastContentLength: number;
|
|
49
|
+
readonly totalChunks: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const defaultEstimateTokens = (textLength: number): number =>
|
|
53
|
+
Math.ceil(textLength / 4);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply one content growth delta to `state`, recording the first-token time
|
|
57
|
+
* on the first growth and bumping the chunk count. Returns `state` unchanged
|
|
58
|
+
* when `len` has not grown.
|
|
59
|
+
*/
|
|
60
|
+
const applyGrowth = (
|
|
61
|
+
state: StreamingTimingState,
|
|
62
|
+
len: number,
|
|
63
|
+
now: () => number,
|
|
64
|
+
): StreamingTimingState => {
|
|
65
|
+
if (len <= state.lastContentLength) return state;
|
|
66
|
+
return {
|
|
67
|
+
...state,
|
|
68
|
+
firstTokenTime: state.firstTokenTime ?? now() - state.startTime,
|
|
69
|
+
lastContentLength: len,
|
|
70
|
+
totalChunks: state.totalChunks + 1,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Advance the client-side streaming timing tracker by one update.
|
|
76
|
+
*
|
|
77
|
+
* Pure: given the previous state and the current `(messages, isRunning)`
|
|
78
|
+
* snapshot, returns the next state plus any `MessageTiming` finalized this
|
|
79
|
+
* update (empty when streaming is still in flight). The caller owns the
|
|
80
|
+
* state (e.g. a ref) and merges finalized timings into its store.
|
|
81
|
+
*
|
|
82
|
+
* At finalize the text length is recomputed from the final messages, and any
|
|
83
|
+
* final growth delta that landed in the same update as the
|
|
84
|
+
* `isRunning -> false` transition is reconciled, so the token estimate and
|
|
85
|
+
* `totalChunks` count the same content (the per-adapter hooks this replaces
|
|
86
|
+
* reuse a stale growth delta and miss that last chunk).
|
|
87
|
+
*/
|
|
88
|
+
export const stepStreamingTiming = <TMessage>(
|
|
89
|
+
state: StreamingTimingState | null,
|
|
90
|
+
messages: readonly TMessage[],
|
|
91
|
+
isRunning: boolean,
|
|
92
|
+
accessors: StreamingTimingAccessors<TMessage>,
|
|
93
|
+
options: StreamingTimingOptions | undefined,
|
|
94
|
+
now: () => number = Date.now,
|
|
95
|
+
): {
|
|
96
|
+
readonly state: StreamingTimingState | null;
|
|
97
|
+
readonly timings: Record<string, MessageTiming>;
|
|
98
|
+
} => {
|
|
99
|
+
const lastId = accessors.getAssistantMessageId(messages);
|
|
100
|
+
const estimateTokens = options?.estimateTokens ?? defaultEstimateTokens;
|
|
101
|
+
|
|
102
|
+
if (isRunning && lastId !== undefined) {
|
|
103
|
+
let next: StreamingTimingState;
|
|
104
|
+
if (state === null || state.messageId !== lastId) {
|
|
105
|
+
next = {
|
|
106
|
+
messageId: lastId,
|
|
107
|
+
startTime: now(),
|
|
108
|
+
lastContentLength: 0,
|
|
109
|
+
totalChunks: 0,
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
next = state;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
state: applyGrowth(
|
|
117
|
+
next,
|
|
118
|
+
accessors.getTextLength(messages, next.messageId),
|
|
119
|
+
now,
|
|
120
|
+
),
|
|
121
|
+
timings: {},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!isRunning && state !== null) {
|
|
126
|
+
const nowMs = now();
|
|
127
|
+
// Reconcile any final growth that landed with the stop signal so the
|
|
128
|
+
// token estimate (derived from final length) and totalChunks agree.
|
|
129
|
+
const reconciled = applyGrowth(
|
|
130
|
+
state,
|
|
131
|
+
accessors.getTextLength(messages, state.messageId),
|
|
132
|
+
() => nowMs,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const totalStreamTime = nowMs - reconciled.startTime;
|
|
136
|
+
const tokenCount = estimateTokens(reconciled.lastContentLength);
|
|
137
|
+
const toolCallCount = accessors.getToolCallCount(
|
|
138
|
+
messages,
|
|
139
|
+
reconciled.messageId,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const timing: MessageTiming = {
|
|
143
|
+
streamStartTime: reconciled.startTime,
|
|
144
|
+
totalStreamTime,
|
|
145
|
+
totalChunks: reconciled.totalChunks,
|
|
146
|
+
toolCallCount,
|
|
147
|
+
...(reconciled.firstTokenTime !== undefined && {
|
|
148
|
+
firstTokenTime: reconciled.firstTokenTime,
|
|
149
|
+
}),
|
|
150
|
+
...(tokenCount > 0 && { tokenCount }),
|
|
151
|
+
...(totalStreamTime > 0 &&
|
|
152
|
+
tokenCount > 0 && {
|
|
153
|
+
tokensPerSecond: tokenCount / (totalStreamTime / 1000),
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return { state: null, timings: { [reconciled.messageId]: timing } };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { state, timings: {} };
|
|
161
|
+
};
|
|
@@ -127,6 +127,7 @@ export const fromThreadMessageLike = (
|
|
|
127
127
|
image,
|
|
128
128
|
...rest
|
|
129
129
|
}: ImageMessagePart): ImageMessagePart | null => {
|
|
130
|
+
if (typeof image !== "string") return null;
|
|
130
131
|
const dataUri = image.match(
|
|
131
132
|
/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/,
|
|
132
133
|
);
|
|
@@ -160,7 +161,7 @@ export const fromThreadMessageLike = (
|
|
|
160
161
|
switch (type) {
|
|
161
162
|
case "text":
|
|
162
163
|
case "reasoning":
|
|
163
|
-
if (part.text
|
|
164
|
+
if (!part.text?.trim()) return null;
|
|
164
165
|
return part;
|
|
165
166
|
|
|
166
167
|
case "file":
|
|
@@ -64,6 +64,14 @@ export type ExternalStoreMessageConverter<T> = (
|
|
|
64
64
|
idx: number,
|
|
65
65
|
) => ThreadMessageLike;
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated This API is still under active development and might change without notice.
|
|
69
|
+
*/
|
|
70
|
+
export type ExternalStoreBranchChange = {
|
|
71
|
+
headId: string | null;
|
|
72
|
+
visibleMessageIds: readonly string[];
|
|
73
|
+
};
|
|
74
|
+
|
|
67
75
|
type ExternalStoreMessageConverterAdapter<T> = {
|
|
68
76
|
convertMessage: ExternalStoreMessageConverter<T>;
|
|
69
77
|
};
|
|
@@ -102,6 +110,26 @@ type ExternalStoreAdapterBase<T> = {
|
|
|
102
110
|
extras?: unknown;
|
|
103
111
|
|
|
104
112
|
setMessages?: ((messages: readonly T[]) => void) | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Fires when the user explicitly switches branches via the runtime's
|
|
115
|
+
* `switchToBranch` action (e.g. a BranchPicker click). It does not fire on
|
|
116
|
+
* adapter resync, `append`, edit/regenerate, content-only updates, or while
|
|
117
|
+
* the thread is running. Consecutive switches that resolve to the same
|
|
118
|
+
* canonical head are de-duped.
|
|
119
|
+
*
|
|
120
|
+
* `headId` is the canonical (persisted) head of the now-visible branch —
|
|
121
|
+
* optimistic/transient ids are never surfaced. `visibleMessageIds` lists the
|
|
122
|
+
* visible path in order.
|
|
123
|
+
*
|
|
124
|
+
* This complements `setMessages` rather than replacing it: switching still
|
|
125
|
+
* requires `setMessages`, and this callback does not on its own enable branch
|
|
126
|
+
* switching.
|
|
127
|
+
*
|
|
128
|
+
* @deprecated This API is still under active development and might change without notice.
|
|
129
|
+
*/
|
|
130
|
+
unstable_onBranchChange?:
|
|
131
|
+
| ((event: ExternalStoreBranchChange) => void)
|
|
132
|
+
| undefined;
|
|
105
133
|
onImport?: ((messages: readonly ThreadMessage[]) => void) | undefined;
|
|
106
134
|
onExportExternalState?: (() => any) | undefined;
|
|
107
135
|
onLoadExternalState?: ((state: any) => void) | undefined;
|
|
@@ -8,7 +8,10 @@ import type {
|
|
|
8
8
|
ThreadSuggestion,
|
|
9
9
|
} from "../../runtime/interfaces/thread-runtime-core";
|
|
10
10
|
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
ExternalStoreAdapter,
|
|
13
|
+
ExternalStoreBranchChange,
|
|
14
|
+
} from "./external-store-adapter";
|
|
12
15
|
import {
|
|
13
16
|
getExternalStoreMessages,
|
|
14
17
|
bindExternalStoreMessage,
|
|
@@ -405,8 +408,37 @@ export class ExternalStoreThreadRuntimeCore
|
|
|
405
408
|
return;
|
|
406
409
|
}
|
|
407
410
|
|
|
411
|
+
const onBranchChange = this._store.unstable_onBranchChange;
|
|
412
|
+
const previousHeadId = onBranchChange
|
|
413
|
+
? this.repository.canonicalHeadId
|
|
414
|
+
: null;
|
|
415
|
+
|
|
408
416
|
this.repository.switchToBranch(branchId);
|
|
409
417
|
this.updateMessages(this.repository.getMessages());
|
|
418
|
+
if (onBranchChange) {
|
|
419
|
+
this._notifyBranchChange(previousHeadId, onBranchChange);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Emit `unstable_onBranchChange` for an explicit branch switch. Reads the
|
|
425
|
+
* canonical head from the repository (which skips optimistic/transient
|
|
426
|
+
* messages) and de-dupes switches that leave the canonical head unchanged.
|
|
427
|
+
* Comparing against the head observed just before the switch — rather than the
|
|
428
|
+
* last emitted head — keeps a switch firing after an adapter resync moved the
|
|
429
|
+
* head elsewhere in the meantime.
|
|
430
|
+
*/
|
|
431
|
+
private _notifyBranchChange(
|
|
432
|
+
previousHeadId: string | null,
|
|
433
|
+
onBranchChange: (event: ExternalStoreBranchChange) => void,
|
|
434
|
+
): void {
|
|
435
|
+
const headId = this.repository.canonicalHeadId;
|
|
436
|
+
if (headId === previousHeadId) return;
|
|
437
|
+
|
|
438
|
+
onBranchChange({
|
|
439
|
+
headId,
|
|
440
|
+
visibleMessageIds: this.repository.getMessages().map((m) => m.id),
|
|
441
|
+
});
|
|
410
442
|
}
|
|
411
443
|
|
|
412
444
|
public async append(message: AppendMessage): Promise<void> {
|
|
@@ -493,7 +493,10 @@ export class LocalThreadRuntimeCore
|
|
|
493
493
|
|
|
494
494
|
try {
|
|
495
495
|
this._lastRunConfig = runConfig ?? {};
|
|
496
|
-
|
|
496
|
+
// unstable_composerMetadata is composer-only (stamped onto the outgoing
|
|
497
|
+
// message); never expose it to the chat-model adapter's run context.
|
|
498
|
+
const { unstable_composerMetadata: _, ...context } =
|
|
499
|
+
this.getModelContext();
|
|
497
500
|
|
|
498
501
|
runCallback =
|
|
499
502
|
runCallback ??
|
|
@@ -77,6 +77,19 @@ export type RemoteThreadListOptions = {
|
|
|
77
77
|
*/
|
|
78
78
|
threadId?: string | undefined;
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Called whenever the active thread's canonical (remote) ID changes, so the
|
|
82
|
+
* value can be treated as a managed/controlled variable (e.g. synced to a
|
|
83
|
+
* URL query param). Together with `threadId` this forms the controlled
|
|
84
|
+
* pattern: `threadId` in, `onThreadIdChange` out.
|
|
85
|
+
*
|
|
86
|
+
* Only the settled remote ID is emitted: while a freshly created thread is
|
|
87
|
+
* still optimistic (no remote ID yet) the value is `undefined`, and the real
|
|
88
|
+
* ID is emitted once the thread is initialized. The transient local ID is
|
|
89
|
+
* never surfaced.
|
|
90
|
+
*/
|
|
91
|
+
onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;
|
|
92
|
+
|
|
80
93
|
/**
|
|
81
94
|
* When true, if this runtime is used inside another RemoteThreadListRuntime,
|
|
82
95
|
* it becomes a no-op and simply calls the runtimeHook directly.
|
|
@@ -302,6 +302,8 @@ describe("MessageRepository", () => {
|
|
|
302
302
|
const exported = repository.export();
|
|
303
303
|
|
|
304
304
|
expect(exported.messages.map((m) => m.message.id)).toEqual(["u"]);
|
|
305
|
+
expect(repository.headId).toBe("placeholder");
|
|
306
|
+
expect(repository.canonicalHeadId).toBe("u");
|
|
305
307
|
// head was the optimistic placeholder; the exported head must fall back
|
|
306
308
|
// to the nearest persisted ancestor so it always resolves on import.
|
|
307
309
|
expect(exported.headId).toBe("u");
|