fix(app): keep fork history readable after restoring cached messages

Cached user messages omit attachments. Reconciliation preserved that incomplete presentation over the canonical snapshot, so a reload removed the history card. Preserve canonical attachments when the cached presentation has none.

Co-authored-by: wengshihao <45631373+wengshihao@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-11-06 05:56:08 +00:00
co-authored by wengshihao
parent 5800a626b9
commit 16425915e5
2 changed files with 35 additions and 0 deletions
+34
View File
@@ -9,6 +9,7 @@ import {
hydrateStreamState,
mergeToolCallDetail,
reduceStreamUpdate,
replaceWithCanonicalStream,
streamTimelineItemIdentity,
type AgentToolCallItem,
type StreamItem,
@@ -158,6 +159,39 @@ describe("plugin timeline rows", () => {
});
describe("user message identity", () => {
it.each(["", "Continue here"])(
"restores fork history onto a cached user message (%s)",
(text) => {
const cached = createUserMessage({
clientMessageId: "fork-client",
messageId: "fork-provider",
text,
timestamp: new Date(0),
timelineCursor: { epoch: "timeline", seq: 1 },
});
const history = {
type: "text",
mimeType: "text/plain",
contextKind: "chat_history",
text: "[User] Earlier question\n[Assistant] Earlier answer",
} as const;
const canonical = createUserMessage({
...cached,
attachments: [history],
});
const result = replaceWithCanonicalStream({
canonical: [canonical],
previousTail: [cached],
previousHead: [],
sendingClientMessageIds: [],
preserveContinuity: true,
canonicalCoverage: { epoch: "timeline", endSeq: 1 },
});
expect(result.tail).toHaveLength(1);
expect(result.tail[0]).toMatchObject({ text, attachments: [history] });
},
);
it.each(["", "Continue from here"])(
"hydrates fork history without optimistic state: %s",
(text) => {
+1
View File
@@ -263,6 +263,7 @@ function produceUserMessage(
clientMessageId: incoming.clientMessageId ?? existing.clientMessageId,
messageId: incoming.messageId ?? existing.messageId,
timelineCursor: incoming.timelineCursor ?? existing.timelineCursor,
attachments: presentation.attachments ?? incoming.attachments,
});
if (
existing.id === merged.id &&