authorgravatar for hi@4bs3nt.comjan <hi@4bs3nt.com> 2026-07-27 22:05:15+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 22:05:15+02:00
logeea3ea4c6ffc4567ab1037cbf53feb33af0bb132
treefa368f15c832152f29728668781a8833c92fea03
parent10bca3f44e1be6a874438fe80badbad45cd00279
signature Signed by PGP key B5690EEEBB952194

feat(diff-panel): show total line additions and deletions (#4674)


3 files changed, 61 insertions(+), 1 deletions(-)

apps/web/src/components/DiffPanel.tsx+11
......@@ -30,6 +30,7 @@ import { useTheme } from "../hooks/useTheme";
3030import {
3131 buildFileDiffRenderKey,
3232 getDiffCollapseIconClassName,
33 getDiffLineStat,
3334 getRenderablePatch,
3435 resolveDiffThemeName,
3536 resolveFileDiffPath,
......@@ -41,6 +42,7 @@ import { resolveThreadRouteRef } from "../threadRoutes";
4142import { useClientSettings } from "../hooks/useSettings";
4243import { formatShortTimestamp } from "../timestampFormat";
4344import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
45import { DiffStatLabel } from "./chat/DiffStatLabel";
4446import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView";
4547import { Button } from "./ui/button";
4648import { ToggleGroup, Toggle } from "./ui/toggle-group";
......@@ -452,6 +454,7 @@ export default function DiffPanel({
452454 );
453455 const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]);
454456 const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys);
457 const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]);
455458
456459 useEffect(() => {
457460 if (!selectedFilePath) return;
......@@ -713,6 +716,14 @@ export default function DiffPanel({
713716 )}
714717 </div>
715718 <div className="flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">
719 {codeViewFiles.length > 0 && (
720 <DiffStatLabel
721 additions={diffLineStat.additions}
722 deletions={diffLineStat.deletions}
723 className="mr-1 text-[11px]"
724 layout="inline"
725 />
726 )}
716727 {codeViewFiles.length > 0 && (
717728 <Tooltip>
718729 <TooltipTrigger
apps/web/src/lib/diffRendering.test.ts+31-1
......@@ -1,5 +1,5 @@
11import { describe, expect, it } from "vite-plus/test";
2import { buildPatchCacheKey, getRenderablePatch } from "./diffRendering";
2import { buildPatchCacheKey, getDiffLineStat, getRenderablePatch } from "./diffRendering";
33
44describe("buildPatchCacheKey", () => {
55 it("returns a stable cache key for identical content", () => {
......@@ -82,3 +82,33 @@ describe("getRenderablePatch", () => {
8282 expect(parsed.files[0]?.hunks[0]?.unifiedLineStart).toBe(47);
8383 });
8484});
85
86describe("getDiffLineStat", () => {
87 it("totals additions and deletions across every file and hunk", () => {
88 const patch = [
89 "diff --git a/example.ts b/example.ts",
90 "--- a/example.ts",
91 "+++ b/example.ts",
92 "@@ -1,2 +1,3 @@",
93 "-before",
94 "+after",
95 "+added",
96 " context",
97 "@@ -10,2 +11,1 @@",
98 "-removed",
99 " context",
100 "diff --git a/README.md b/README.md",
101 "--- a/README.md",
102 "+++ b/README.md",
103 "@@ -1 +1,2 @@",
104 " title",
105 "+description",
106 ].join("\n");
107
108 const parsed = getRenderablePatch(patch);
109 expect(parsed?.kind).toBe("files");
110 if (parsed?.kind !== "files") return;
111
112 expect(getDiffLineStat(parsed.files)).toEqual({ additions: 3, deletions: 2 });
113 });
114});
apps/web/src/lib/diffRendering.ts+19
......@@ -52,6 +52,25 @@ export type RenderablePatch =
5252 reason: string;
5353 };
5454
55export interface DiffLineStat {
56 additions: number;
57 deletions: number;
58}
59
60export function getDiffLineStat(files: ReadonlyArray<FileDiffMetadata>): DiffLineStat {
61 return files.reduce<DiffLineStat>(
62 (total, file) => {
63 for (const hunk of file.hunks) {
64 total.additions += hunk.additionLines;
65 total.deletions += hunk.deletionLines;
66 }
67
68 return total;
69 },
70 { additions: 0, deletions: 0 },
71 );
72}
73
5574interface RenderablePatchOptions {
5675 /**
5776 * Pierre's partial-patch parser keeps hunk render starts in source-file