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