From 3a1c6df9e24672f0761a6ced18e1315d89334baf Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:06:38 +1000 Subject: [PATCH] fix(app): deduplicate diff summaries linearly (#37414) --- .../app/src/pages/session/timeline/rows.ts | 16 ++----- .../session/timeline/summary-diffs.test.ts | 42 +++++++++++++++++++ .../pages/session/timeline/summary-diffs.ts | 20 +++++++++ 3 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 packages/app/src/pages/session/timeline/summary-diffs.test.ts create mode 100644 packages/app/src/pages/session/timeline/summary-diffs.ts diff --git a/packages/app/src/pages/session/timeline/rows.ts b/packages/app/src/pages/session/timeline/rows.ts index 72cd9a180c..a9c9a2d288 100644 --- a/packages/app/src/pages/session/timeline/rows.ts +++ b/packages/app/src/pages/session/timeline/rows.ts @@ -1,7 +1,8 @@ import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import { AssistantMessage, Part, SessionStatus, SnapshotFileDiff, UserMessage } from "@opencode-ai/sdk/v2" +import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part" import { TimelineRow, type SummaryDiff } from "./timeline-row" +import { uniqueSummaryDiffs } from "./summary-diffs" export { TimelineRow, type SummaryDiff } from "./timeline-row" @@ -137,14 +138,7 @@ export namespace Timeline { if (isActive && status === "retry") rows.push(new TimelineRow.Retry({ userMessageID: userMessage.id })) - const diffs = (userMessage.summary?.diffs ?? []) - .reduceRight((result, diff) => { - if (!isSummaryDiff(diff)) return result - if (result.some((item) => item.file === diff.file)) return result - result.push(diff) - return result - }, []) - .reverse() + const diffs = uniqueSummaryDiffs(userMessage.summary?.diffs) if (diffs.length > 0 && (status === "idle" || !isActive)) { rows.push( new TimelineRow.DiffSummary({ @@ -169,10 +163,6 @@ export namespace Timeline { return rows } - function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff { - return typeof value.file === "string" - } - function reasoningHeading(text: string) { const markdown = text.replace(/\r\n?/g, "\n") const html = markdown.match(/]*>([\s\S]*?)<\/h[1-6]>/i) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.test.ts b/packages/app/src/pages/session/timeline/summary-diffs.test.ts new file mode 100644 index 0000000000..9b66bf6771 --- /dev/null +++ b/packages/app/src/pages/session/timeline/summary-diffs.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test" +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import { uniqueSummaryDiffs } from "./summary-diffs" + +const diff = (file: string, additions: number) => + ({ + file, + additions, + deletions: 0, + }) satisfies SnapshotFileDiff + +describe("uniqueSummaryDiffs", () => { + test("drops entries without files and preserves unique input", () => { + const alpha = diff("alpha.ts", 1) + const beta = diff("beta.ts", 1) + const invalid = { additions: 1, deletions: 0 } satisfies SnapshotFileDiff + + expect(uniqueSummaryDiffs(undefined)).toEqual([]) + expect(uniqueSummaryDiffs([])).toEqual([]) + expect(uniqueSummaryDiffs([invalid])).toEqual([]) + + const result = uniqueSummaryDiffs([alpha, invalid, beta]) + expect(result).toEqual([alpha, beta]) + expect(result[0]).toBe(alpha) + expect(result[1]).toBe(beta) + }) + + test("keeps the last diff per file in the legacy display order", () => { + const oldAlpha = diff("alpha.ts", 1) + const oldBeta = diff("beta.ts", 1) + const newAlpha = diff("alpha.ts", 2) + const charlie = diff("charlie.ts", 1) + const newBeta = diff("beta.ts", 2) + + const result = uniqueSummaryDiffs([oldAlpha, oldBeta, newAlpha, charlie, newBeta]) + + expect(result).toEqual([newAlpha, charlie, newBeta]) + expect(result[0]).toBe(newAlpha) + expect(result[1]).toBe(charlie) + expect(result[2]).toBe(newBeta) + }) +}) diff --git a/packages/app/src/pages/session/timeline/summary-diffs.ts b/packages/app/src/pages/session/timeline/summary-diffs.ts new file mode 100644 index 0000000000..2df37af5a6 --- /dev/null +++ b/packages/app/src/pages/session/timeline/summary-diffs.ts @@ -0,0 +1,20 @@ +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" +import type { SummaryDiff } from "./timeline-row" + +export function uniqueSummaryDiffs(diffs: SnapshotFileDiff[] | undefined) { + const files = new Set() + return (diffs ?? []) + .reduceRight((result, diff) => { + if (!isSummaryDiff(diff)) return result + const file = diff.file + if (files.has(file)) return result + files.add(file) + result.push(diff) + return result + }, []) + .reverse() +} + +function isSummaryDiff(diff: SnapshotFileDiff): diff is SummaryDiff { + return typeof diff.file === "string" +}