diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx
index 7ea2d588477..2e1b6d0c51b 100644
--- a/apps/web/src/components/DiffPanel.tsx
+++ b/apps/web/src/components/DiffPanel.tsx
@@ -33,6 +33,7 @@ import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"
import { useTheme } from "../hooks/useTheme";
import {
buildFileDiffRenderKey,
+ canRenderFileDiff,
getDiffCollapseIconClassName,
getRenderablePatch,
resolveDiffThemeName,
@@ -325,12 +326,22 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
if (!renderablePatch || renderablePatch.kind !== "files") {
return [];
}
- return renderablePatch.files.toSorted((left, right) =>
- resolveFileDiffPath(left).localeCompare(resolveFileDiffPath(right), undefined, {
- numeric: true,
- sensitivity: "base",
- }),
- );
+ return renderablePatch.files
+ .map((fileDiff) => ({
+ canRender: canRenderFileDiff(fileDiff),
+ fileDiff,
+ filePath: resolveFileDiffPath(fileDiff),
+ }))
+ .toSorted((left, right) => {
+ const renderOrder = Number(!left.canRender) - Number(!right.canRender);
+ return (
+ renderOrder ||
+ left.filePath.localeCompare(right.filePath, undefined, {
+ numeric: true,
+ sensitivity: "base",
+ })
+ );
+ });
}, [renderablePatch]);
useEffect(() => {
@@ -679,11 +690,10 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
intersectionObserverMargin: 1200,
}}
>
- {renderableFiles.map((fileDiff) => {
- const filePath = resolveFileDiffPath(fileDiff);
+ {renderableFiles.map(({ canRender, fileDiff, filePath }) => {
const fileKey = buildFileDiffRenderKey(fileDiff);
const themedFileKey = `${fileKey}:${resolvedTheme}`;
- const collapsed = collapsedDiffFileKeys.has(fileKey);
+ const collapsed = !canRender || collapsedDiffFileKeys.has(fileKey);
return (
{
event.stopPropagation();
+ if (!canRender) return;
toggleDiffFileCollapsed(fileKey);
}}
/>
@@ -748,6 +760,11 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
unsafeCSS: DIFF_PANEL_UNSAFE_CSS,
}}
/>
+ {!canRender && (
+
+ This file is too large to display. Open the file to inspect the change.
+
+ )}
);
})}
diff --git a/apps/web/src/lib/diffRendering.test.ts b/apps/web/src/lib/diffRendering.test.ts
index c24f58b99dd..b848e3a4b05 100644
--- a/apps/web/src/lib/diffRendering.test.ts
+++ b/apps/web/src/lib/diffRendering.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vite-plus/test";
-import { buildPatchCacheKey } from "./diffRendering";
+import {
+ buildPatchCacheKey,
+ canRenderFileDiff,
+ MAX_RENDERABLE_DIFF_LINE_LENGTH,
+} from "./diffRendering";
describe("buildPatchCacheKey", () => {
it("returns a stable cache key for identical content", () => {
@@ -29,3 +33,23 @@ describe("buildPatchCacheKey", () => {
);
});
});
+
+describe("diff render line limits", () => {
+ it("rejects file diffs with pathological line lengths", () => {
+ expect(
+ canRenderFileDiff({
+ additionLines: ["small"],
+ deletionLines: ["x".repeat(MAX_RENDERABLE_DIFF_LINE_LENGTH + 1)],
+ }),
+ ).toBe(false);
+ });
+
+ it("allows file diffs within the line length limit", () => {
+ expect(
+ canRenderFileDiff({
+ additionLines: ["x".repeat(MAX_RENDERABLE_DIFF_LINE_LENGTH)],
+ deletionLines: ["small"],
+ }),
+ ).toBe(true);
+ });
+});
diff --git a/apps/web/src/lib/diffRendering.ts b/apps/web/src/lib/diffRendering.ts
index cb57ec7e065..8ce4764820a 100644
--- a/apps/web/src/lib/diffRendering.ts
+++ b/apps/web/src/lib/diffRendering.ts
@@ -8,6 +8,8 @@ export const DIFF_THEME_NAMES = {
export type DiffThemeName = (typeof DIFF_THEME_NAMES)[keyof typeof DIFF_THEME_NAMES];
+export const MAX_RENDERABLE_DIFF_LINE_LENGTH = 500_000;
+
export function resolveDiffThemeName(theme: "light" | "dark"): DiffThemeName {
return theme === "dark" ? DIFF_THEME_NAMES.dark : DIFF_THEME_NAMES.light;
}
@@ -110,3 +112,12 @@ export function getDiffCollapseIconClassName(fileDiff: FileDiffMetadata): string
return "text-muted-foreground/80";
}
}
+
+export function canRenderFileDiff(
+ fileDiff: Pick,
+): boolean {
+ return (
+ fileDiff.additionLines.every((line) => line.length <= MAX_RENDERABLE_DIFF_LINE_LENGTH) &&
+ fileDiff.deletionLines.every((line) => line.length <= MAX_RENDERABLE_DIFF_LINE_LENGTH)
+ );
+}