From 5c66665a71617af74fb6eda85a6519236310c97b Mon Sep 17 00:00:00 2001 From: Kayden Humphries Date: Fri, 24 Apr 2026 14:04:07 -0600 Subject: [PATCH] fix(web): avoid rendering oversized diff lines Introduced canRenderFileDiff to skip browser rendering for pathological lines while preserving the updated annotatable diff flow. Added tests for accepted and rejected line lengths. Co-authored-by: codex --- apps/web/src/components/DiffPanel.tsx | 35 +++++++++++++++++++------- apps/web/src/lib/diffRendering.test.ts | 26 ++++++++++++++++++- apps/web/src/lib/diffRendering.ts | 11 ++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) 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) + ); +}