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`
This commit is contained in:
Haileyesus
2026-03-10 23:19:52 +03:00
parent 68787e01fc
commit a780cb6523
2 changed files with 43 additions and 4 deletions

View File

@@ -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 });

View File

@@ -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 (
<div className="p-4 text-center text-sm text-muted-foreground">
@@ -35,7 +63,12 @@ export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewe
return (
<div className="diff-viewer">
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
{isPreviewTruncated && (
<div className="mb-2 rounded-md border border-border bg-card px-3 py-2 text-xs text-muted-foreground">
Large diff preview: rendering is limited to keep the tab responsive.
</div>
)}
{preview.lines.map((line, index) => renderDiffLine(line, index))}
</div>
);
}
}