Improve workspace/chat diff viewer with syntax highlighting & per-file view
Replace the raw single-blob diff dump (Monaco, language=diff) and the plain <pre> file diffs in chat with @git-diff-view/react: - Parse the unified git diff into structured per-file entries (status, +/- counts, binary detection) via parseDiffFiles() - Workspace Diff tab: collapsible per-file cards with status badges, line counts, syntax highlighting, and a Unified/Split toggle - Agent chat: render each change's diff highlighted instead of plain text - Theme follows next-themes resolvedTheme (light/dark)
This commit is contained in:
@@ -1,3 +1,112 @@
|
||||
export type DiffFileStatus = 'added' | 'deleted' | 'modified' | 'renamed';
|
||||
|
||||
export type ParsedDiffFile = {
|
||||
id: string;
|
||||
oldPath: string;
|
||||
newPath: string;
|
||||
/** Path to show in the UI (new path, or old path for deletions). */
|
||||
displayPath: string;
|
||||
status: DiffFileStatus;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
isBinary: boolean;
|
||||
/** The full per-file unified diff section, fed as-is to the diff renderer. */
|
||||
hunkText: string;
|
||||
};
|
||||
|
||||
const stripABPrefix = (value: string) => value.replace(/^[ab]\//, '');
|
||||
|
||||
/**
|
||||
* Splits a raw unified git diff into structured, per-file entries. Replaces the
|
||||
* "one giant blob" rendering: each file can be shown, counted, and highlighted
|
||||
* independently.
|
||||
*/
|
||||
export const parseDiffFiles = (diff: string | undefined): ParsedDiffFile[] => {
|
||||
if (!diff?.trim()) return [];
|
||||
const sections: string[][] = [];
|
||||
let current: string[] | null = null;
|
||||
for (const line of diff.split('\n')) {
|
||||
if (line.startsWith('diff --git ')) {
|
||||
if (current) sections.push(current);
|
||||
current = [line];
|
||||
continue;
|
||||
}
|
||||
current?.push(line);
|
||||
}
|
||||
if (current) sections.push(current);
|
||||
|
||||
return sections.map((sectionLines, index) => {
|
||||
const header = sectionLines[0] ?? '';
|
||||
const gitMatch = /^diff --git a\/(.+?) b\/(.+)$/.exec(header);
|
||||
let oldPath = gitMatch?.[1] ?? '';
|
||||
let newPath = gitMatch?.[2] ?? oldPath;
|
||||
let status: DiffFileStatus = 'modified';
|
||||
let isBinary = false;
|
||||
let additions = 0;
|
||||
let deletions = 0;
|
||||
let renameFrom = '';
|
||||
let renameTo = '';
|
||||
|
||||
for (const line of sectionLines) {
|
||||
if (line.startsWith('new file mode')) status = 'added';
|
||||
else if (line.startsWith('deleted file mode')) status = 'deleted';
|
||||
else if (line.startsWith('rename from ')) {
|
||||
renameFrom = line.slice('rename from '.length);
|
||||
} else if (line.startsWith('rename to ')) {
|
||||
renameTo = line.slice('rename to '.length);
|
||||
} else if (
|
||||
line.startsWith('Binary files') ||
|
||||
line.startsWith('GIT binary patch')
|
||||
) {
|
||||
isBinary = true;
|
||||
} else if (line.startsWith('--- ')) {
|
||||
const value = line.slice(4).trim();
|
||||
if (value !== '/dev/null') oldPath = stripABPrefix(value);
|
||||
} else if (line.startsWith('+++ ')) {
|
||||
const value = line.slice(4).trim();
|
||||
if (value !== '/dev/null') newPath = stripABPrefix(value);
|
||||
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||
additions += 1;
|
||||
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
||||
deletions += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (renameFrom || renameTo) {
|
||||
status = 'renamed';
|
||||
oldPath = renameFrom || oldPath;
|
||||
newPath = renameTo || newPath;
|
||||
}
|
||||
|
||||
const displayPath = status === 'deleted' ? oldPath : newPath;
|
||||
return {
|
||||
id: `${index}-${displayPath}`,
|
||||
oldPath,
|
||||
newPath,
|
||||
displayPath,
|
||||
status,
|
||||
additions,
|
||||
deletions,
|
||||
isBinary,
|
||||
hunkText: sectionLines.join('\n'),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** Returns the single parsed file matching a path, if present in the diff. */
|
||||
export const parseDiffFileForPath = (
|
||||
diff: string | undefined,
|
||||
filePath: string,
|
||||
): ParsedDiffFile | undefined => {
|
||||
const normalized = filePath.replace(/^\.\/+/, '');
|
||||
return parseDiffFiles(diff).find(
|
||||
(file) =>
|
||||
file.displayPath === normalized ||
|
||||
file.newPath === normalized ||
|
||||
file.oldPath === normalized,
|
||||
);
|
||||
};
|
||||
|
||||
export const extractFileDiff = (diff: string | undefined, filePath: string) => {
|
||||
if (!diff?.trim() || filePath === '.') return '';
|
||||
const lines = diff.split('\n');
|
||||
|
||||
Reference in New Issue
Block a user