From a780cb65237f3a87d52ec61bdc184434fa2dbe32 Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Tue, 10 Mar 2026 23:19:52 +0300 Subject: [PATCH] fix(git-ui): prevent large commit diffs from freezing the history tab Harden commit diff loading/rendering so opening a very large commit no longer hangs the browser tab. Problem - commit history diff viewer rendered every diff line as a React node - very large commits could create thousands of nodes and lock the UI thread - backend always returned full commit patch payloads, amplifying frontend pressure Backend safeguards - add `COMMIT_DIFF_CHARACTER_LIMIT` (500,000 chars) in git routes - update GET `/api/git/commit-diff` to truncate oversized diff payloads - include `isTruncated` flag in response for observability/future UI handling - append truncation marker text when server-side limit is applied Frontend safeguards - update `GitDiffViewer` to use bounded preview rendering: - character cap: 200,000 - line cap: 1,500 - move diff preprocessing into `useMemo` for stable, one-pass preview computation - show a clear "Large diff preview" notice when truncation is active Impact - commit diff expansion remains responsive even for high-change commits - UI still shows useful diff content while avoiding tab lockups - changes apply to shared diff viewer usage and improve resilience broadly Validation - `node --check server/routes/git.js` - `npm run typecheck` - `npx eslint src/components/git-panel/view/shared/GitDiffViewer.tsx` --- server/routes/git.js | 10 ++++- .../git-panel/view/shared/GitDiffViewer.tsx | 37 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/server/routes/git.js b/server/routes/git.js index 6cd56f4..cf37364 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -7,6 +7,7 @@ import { queryClaudeSDK } from '../claude-sdk.js'; import { spawnCursor } from '../cursor-cli.js'; const router = express.Router(); +const COMMIT_DIFF_CHARACTER_LIMIT = 500_000; function spawnAsync(command, args, options = {}) { return new Promise((resolve, reject) => { @@ -769,8 +770,13 @@ router.get('/commit-diff', async (req, res) => { 'git', ['show', commit], { cwd: projectPath } ); - - res.json({ diff: stdout }); + + const isTruncated = stdout.length > COMMIT_DIFF_CHARACTER_LIMIT; + const diff = isTruncated + ? `${stdout.slice(0, COMMIT_DIFF_CHARACTER_LIMIT)}\n\n... Diff truncated to keep the UI responsive ...` + : stdout; + + res.json({ diff, isTruncated }); } catch (error) { console.error('Git commit diff error:', error); res.json({ error: error.message }); diff --git a/src/components/git-panel/view/shared/GitDiffViewer.tsx b/src/components/git-panel/view/shared/GitDiffViewer.tsx index 9a92a11..9c9934d 100644 --- a/src/components/git-panel/view/shared/GitDiffViewer.tsx +++ b/src/components/git-panel/view/shared/GitDiffViewer.tsx @@ -1,10 +1,38 @@ +import { useMemo } from 'react'; + type GitDiffViewerProps = { diff: string | null; isMobile: boolean; wrapText: boolean; }; +const PREVIEW_CHARACTER_LIMIT = 200_000; +const PREVIEW_LINE_LIMIT = 1_500; + +type DiffPreview = { + lines: string[]; + isCharacterTruncated: boolean; + isLineTruncated: boolean; +}; + +function buildDiffPreview(diff: string): DiffPreview { + const isCharacterTruncated = diff.length > PREVIEW_CHARACTER_LIMIT; + const previewText = isCharacterTruncated ? diff.slice(0, PREVIEW_CHARACTER_LIMIT) : diff; + const previewLines = previewText.split('\n'); + const isLineTruncated = previewLines.length > PREVIEW_LINE_LIMIT; + + return { + lines: isLineTruncated ? previewLines.slice(0, PREVIEW_LINE_LIMIT) : previewLines, + isCharacterTruncated, + isLineTruncated, + }; +} + export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewerProps) { + // Render a bounded preview to keep huge commit diffs from freezing the UI thread. + const preview = useMemo(() => buildDiffPreview(diff || ''), [diff]); + const isPreviewTruncated = preview.isCharacterTruncated || preview.isLineTruncated; + if (!diff) { return (
@@ -35,7 +63,12 @@ export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewe return (
- {diff.split('\n').map((line, index) => renderDiffLine(line, index))} + {isPreviewTruncated && ( +
+ Large diff preview: rendering is limited to keep the tab responsive. +
+ )} + {preview.lines.map((line, index) => renderDiffLine(line, index))}
); -} \ No newline at end of file +}