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";...@@ -30,6 +30,7 @@ import { useTheme } from "../hooks/useTheme";
30import {30import {
31 buildFileDiffRenderKey,31 buildFileDiffRenderKey,
32 getDiffCollapseIconClassName,32 getDiffCollapseIconClassName,
33 getDiffLineStat,
33 getRenderablePatch,34 getRenderablePatch,
34 resolveDiffThemeName,35 resolveDiffThemeName,
35 resolveFileDiffPath,36 resolveFileDiffPath,
...@@ -41,6 +42,7 @@ import { resolveThreadRouteRef } from "../threadRoutes";...@@ -41,6 +42,7 @@ import { resolveThreadRouteRef } from "../threadRoutes";
41import { useClientSettings } from "../hooks/useSettings";42import { useClientSettings } from "../hooks/useSettings";
42import { formatShortTimestamp } from "../timestampFormat";43import { formatShortTimestamp } from "../timestampFormat";
43import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";44import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell";
45import { DiffStatLabel } from "./chat/DiffStatLabel";
44import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView";46import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView";
45import { Button } from "./ui/button";47import { Button } from "./ui/button";
46import { ToggleGroup, Toggle } from "./ui/toggle-group";48import { ToggleGroup, Toggle } from "./ui/toggle-group";
...@@ -452,6 +454,7 @@ export default function DiffPanel({...@@ -452,6 +454,7 @@ export default function DiffPanel({
452 );454 );
453 const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]);455 const diffFileKeys = useMemo(() => codeViewFiles.map((file) => file.fileKey), [codeViewFiles]);
454 const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys);456 const allDiffFilesCollapsed = areAllDiffFilesCollapsed(diffFileKeys, collapsedDiffFileKeys);
457 const diffLineStat = useMemo(() => getDiffLineStat(renderableFiles), [renderableFiles]);
455458
456 useEffect(() => {459 useEffect(() => {
457 if (!selectedFilePath) return;460 if (!selectedFilePath) return;
...@@ -713,6 +716,14 @@ export default function DiffPanel({...@@ -713,6 +716,14 @@ export default function DiffPanel({
713 )}716 )}
714 </div>717 </div>
715 <div className="flex shrink-0 items-center gap-1 [-webkit-app-region:no-drag]">718 <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 )}
716 {codeViewFiles.length > 0 && (727 {codeViewFiles.length > 0 && (
717 <Tooltip>728 <Tooltip>
718 <TooltipTrigger729 <TooltipTrigger
apps/web/src/lib/diffRendering.test.ts+31-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import { buildPatchCacheKey, getRenderablePatch } from "./diffRendering";2import { buildPatchCacheKey, getDiffLineStat, getRenderablePatch } from "./diffRendering";
33
4describe("buildPatchCacheKey", () => {4describe("buildPatchCacheKey", () => {
5 it("returns a stable cache key for identical content", () => {5 it("returns a stable cache key for identical content", () => {
...@@ -82,3 +82,33 @@ describe("getRenderablePatch", () => {...@@ -82,3 +82,33 @@ describe("getRenderablePatch", () => {
82 expect(parsed.files[0]?.hunks[0]?.unifiedLineStart).toBe(47);82 expect(parsed.files[0]?.hunks[0]?.unifiedLineStart).toBe(47);
83 });83 });
84});84});
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 =...@@ -52,6 +52,25 @@ export type RenderablePatch =
52 reason: string;52 reason: string;
53 };53 };
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
55interface RenderablePatchOptions {74interface RenderablePatchOptions {
56 /**75 /**
57 * Pierre's partial-patch parser keeps hunk render starts in source-file76 * Pierre's partial-patch parser keeps hunk render starts in source-file