fix(app): preserve assistant order across outline jumps
Live Markdown block promotion discarded timeline positions, so merging a disjoint history window could sort the final block ahead of earlier blocks. Preserve the source position on every promoted block so timeline merges keep the response intact.
This commit is contained in:
@@ -2715,6 +2715,49 @@ describe("processTimelineResponse", () => {
|
||||
expect(result.sideEffects).not.toContainEqual(expect.objectContaining({ type: "catch_up" }));
|
||||
});
|
||||
|
||||
it("keeps live assistant blocks ordered when merging a disjoint prompt-jump window", () => {
|
||||
const live = processAgentStreamEvents({
|
||||
events: [
|
||||
makeStreamReducerEvent(
|
||||
makeAssistantTimelineEvent("First paragraph.\n\nSecond", "assistant-1"),
|
||||
80,
|
||||
),
|
||||
makeStreamReducerEvent(makeAssistantTimelineEvent(" paragraph.", "assistant-1"), 81),
|
||||
makeStreamReducerEvent(makeTimelineEvent("newest prompt", "user_message"), 82),
|
||||
],
|
||||
currentTail: [],
|
||||
currentHead: [],
|
||||
currentCursor: undefined,
|
||||
});
|
||||
expect(getAssistantTexts([...live.tail, ...live.head])).toEqual([
|
||||
"First paragraph.",
|
||||
"Second paragraph.",
|
||||
]);
|
||||
|
||||
const result = processTimelineResponse({
|
||||
...baseTimelineInput,
|
||||
currentTail: live.tail,
|
||||
currentHead: live.head,
|
||||
currentCursor: live.cursor ?? undefined,
|
||||
payload: {
|
||||
...baseTimelineInput.payload,
|
||||
direction: "before",
|
||||
mergeWindow: true,
|
||||
epoch: "epoch-1",
|
||||
startCursor: { seq: 1 },
|
||||
endCursor: { seq: 40 },
|
||||
hasOlder: false,
|
||||
hasNewer: true,
|
||||
entries: [makeTimelineEntry(1, "oldest prompt", "user_message")],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAssistantTexts([...result.tail, ...result.head])).toEqual([
|
||||
"First paragraph.",
|
||||
"Second paragraph.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("fills the gap between a retained prompt window and the contiguous tail", () => {
|
||||
const currentTail: StreamItem[] = [
|
||||
{
|
||||
|
||||
@@ -616,6 +616,30 @@ describe("stream reducer canonical tool calls", () => {
|
||||
expect(new Set(messages.map((message) => message.id)).size).toBe(messages.length);
|
||||
});
|
||||
|
||||
it("keeps the timeline position on every promoted assistant block", () => {
|
||||
const timelineCursor = { epoch: "epoch-1", seq: 42 };
|
||||
const result = applyStreamEvent({
|
||||
tail: [],
|
||||
head: [],
|
||||
event: assistantTimeline("First paragraph.\n\nSecond paragraph.", undefined, "message-1"),
|
||||
timestamp: new Date("2025-01-01T10:02:00Z"),
|
||||
timelineCursor,
|
||||
});
|
||||
|
||||
const messages = [...result.tail, ...result.head].filter(
|
||||
(item): item is Extract<StreamItem, { kind: "assistant_message" }> =>
|
||||
item.kind === "assistant_message",
|
||||
);
|
||||
expect(messages.map((message) => message.text)).toEqual([
|
||||
"First paragraph.",
|
||||
"Second paragraph.",
|
||||
]);
|
||||
expect(messages.map((message) => message.timelineCursor)).toEqual([
|
||||
timelineCursor,
|
||||
timelineCursor,
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves old assistant merge behavior when message ids are absent", () => {
|
||||
const state = hydrateStreamState([
|
||||
{
|
||||
|
||||
@@ -1573,16 +1573,14 @@ function promoteCompletedAssistantBlocks(params: { tail: StreamItem[]; head: Str
|
||||
const completedBlocks = blocks.slice(0, -1);
|
||||
const liveBlock = `${blocks[blocks.length - 1] ?? ""}${getTrailingNewlineSuffix(activeItem.text)}`;
|
||||
const promotedItems = completedBlocks.map<AssistantMessageItem>((block, offset) => ({
|
||||
kind: "assistant_message",
|
||||
...activeItem,
|
||||
id: createAssistantBlockId({
|
||||
groupId: blockGroupId,
|
||||
blockIndex: firstBlockIndex + offset,
|
||||
}),
|
||||
...(activeItem.messageId ? { messageId: activeItem.messageId } : {}),
|
||||
blockGroupId,
|
||||
blockIndex: firstBlockIndex + offset,
|
||||
text: block,
|
||||
timestamp: activeItem.timestamp,
|
||||
}));
|
||||
|
||||
const nextTail = flushHeadToTail(params.tail, promotedItems);
|
||||
|
||||
Reference in New Issue
Block a user