mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 02:15:34 +08:00
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:
@@ -7,6 +7,7 @@ import { queryClaudeSDK } from '../claude-sdk.js';
|
|||||||
import { spawnCursor } from '../cursor-cli.js';
|
import { spawnCursor } from '../cursor-cli.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
|
||||||
|
|
||||||
function spawnAsync(command, args, options = {}) {
|
function spawnAsync(command, args, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -769,8 +770,13 @@ router.get('/commit-diff', async (req, res) => {
|
|||||||
'git', ['show', commit],
|
'git', ['show', commit],
|
||||||
{ cwd: projectPath }
|
{ 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) {
|
} catch (error) {
|
||||||
console.error('Git commit diff error:', error);
|
console.error('Git commit diff error:', error);
|
||||||
res.json({ error: error.message });
|
res.json({ error: error.message });
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
type GitDiffViewerProps = {
|
type GitDiffViewerProps = {
|
||||||
diff: string | null;
|
diff: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
wrapText: 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) {
|
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) {
|
if (!diff) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
@@ -35,7 +63,12 @@ export default function GitDiffViewer({ diff, isMobile, wrapText }: GitDiffViewe
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="diff-viewer">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user