diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index e6dbb57cc7..b625ee85c0 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -27,8 +27,7 @@ import { readLocalApi } from "../localApi"; import { resolvePathLinkTarget } from "../terminal-links"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { useTheme } from "../hooks/useTheme"; -import { buildPatchCacheKey } from "../lib/diffRendering"; -import { resolveDiffThemeName } from "../lib/diffRendering"; +import { buildPatchCacheKey, canRenderFileDiff, resolveDiffThemeName } from "../lib/diffRendering"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { selectProjectByRef, useStore } from "../store"; import { createThreadSelectorByRef } from "../storeSelectors"; @@ -306,12 +305,22 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { 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); + if (renderOrder !== 0) { + return renderOrder; + } + return left.filePath.localeCompare(right.filePath, undefined, { + numeric: true, + sensitivity: "base", + }); + }); }, [renderablePatch]); useEffect(() => { @@ -600,8 +609,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { intersectionObserverMargin: 1200, }} > - {renderableFiles.map((fileDiff) => { - const filePath = resolveFileDiffPath(fileDiff); + {renderableFiles.map(({ canRender, fileDiff, filePath }) => { const fileKey = buildFileDiffRenderKey(fileDiff); const themedFileKey = `${fileKey}:${resolvedTheme}`; return ( @@ -629,8 +637,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { theme: resolveDiffThemeName(resolvedTheme), themeType: resolvedTheme as DiffThemeType, unsafeCSS: DIFF_PANEL_UNSAFE_CSS, + collapsed: !canRender, }} /> + + {!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 9fd7045ff4..ac619a36be 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 "vitest"; -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 7218f72978..b67be8909e 100644 --- a/apps/web/src/lib/diffRendering.ts +++ b/apps/web/src/lib/diffRendering.ts @@ -1,3 +1,5 @@ +import type { FileDiffMetadata } from "@pierre/diffs"; + export const DIFF_THEME_NAMES = { light: "pierre-light", dark: "pierre-dark", @@ -5,6 +7,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; } @@ -37,3 +41,12 @@ export function buildPatchCacheKey(patch: string, scope = "diff-panel"): string ).toString(36); return `${scope}:${normalizedPatch.length}:${primary}:${secondary}`; } + +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) + ); +}