From 0563b849730e284e630e0bfc231ae0e9968c3ba7 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 27 Jul 2026 21:53:55 +0200 Subject: [PATCH] feat(diff-panel): show total line additions and deletions --- apps/web/src/components/DiffPanel.tsx | 11 +++++++++ apps/web/src/lib/diffRendering.test.ts | 32 +++++++++++++++++++++++++- apps/web/src/lib/diffRendering.ts | 19 +++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 3a30fbb2c7e..2142d898269 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -30,6 +30,7 @@ import { useTheme } from "../hooks/useTheme"; import { buildFileDiffRenderKey, getDiffCollapseIconClassName, + getDiffLineStat, getRenderablePatch, resolveDiffThemeName, resolveFileDiffPath, @@ -41,6 +42,7 @@ import { resolveThreadRouteRef } from "../threadRoutes"; import { useClientSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; +import { DiffStatLabel } from "./chat/DiffStatLabel"; import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView"; import { Button } from "./ui/button"; import { ToggleGroup, Toggle } from "./ui/toggle-group"; @@ -452,6 +454,7 @@ export default function DiffPanel({ ); const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]); const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys); + const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]); useEffect(() => { if (!selectedFilePath) return; @@ -713,6 +716,14 @@ export default function DiffPanel({ )}
+ {codeViewFiles.length > 0 && ( + + )} {codeViewFiles.length > 0 && ( { it("returns a stable cache key for identical content", () => { @@ -82,3 +82,33 @@ describe("getRenderablePatch", () => { expect(parsed.files[0]?.hunks[0]?.unifiedLineStart).toBe(47); }); }); + +describe("getDiffLineStat", () => { + it("totals additions and deletions across every file and hunk", () => { + const patch = [ + "diff --git a/example.ts b/example.ts", + "--- a/example.ts", + "+++ b/example.ts", + "@@ -1,2 +1,3 @@", + "-before", + "+after", + "+added", + " context", + "@@ -10,2 +11,1 @@", + "-removed", + " context", + "diff --git a/README.md b/README.md", + "--- a/README.md", + "+++ b/README.md", + "@@ -1 +1,2 @@", + " title", + "+description", + ].join("\n"); + + const parsed = getRenderablePatch(patch); + expect(parsed?.kind).toBe("files"); + if (parsed?.kind !== "files") return; + + expect(getDiffLineStat(parsed.files)).toEqual({ additions: 3, deletions: 2 }); + }); +}); diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts index cb8318b3d2d..493474d8aa2 100644 --- a/apps/web/src/lib/diffRendering.ts +++ b/apps/web/src/lib/diffRendering.ts @@ -52,6 +52,25 @@ export type RenderablePatch = reason: string; }; +export interface DiffLineStat { + additions: number; + deletions: number; +} + +export function getDiffLineStat(files: ReadonlyArray): DiffLineStat { + return files.reduce( + (total, file) => { + for (const hunk of file.hunks) { + total.additions += hunk.additionLines; + total.deletions += hunk.deletionLines; + } + + return total; + }, + { additions: 0, deletions: 0 }, + ); +} + interface RenderablePatchOptions { /** * Pierre's partial-patch parser keeps hunk render starts in source-file