bb471a0917
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)
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
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');
|
|
const sections: string[][] = [];
|
|
let current: string[] | null = null;
|
|
for (const line of lines) {
|
|
if (line.startsWith('diff --git ')) {
|
|
if (current) sections.push(current);
|
|
current = [line];
|
|
continue;
|
|
}
|
|
current?.push(line);
|
|
}
|
|
if (current) sections.push(current);
|
|
const normalizedPath = filePath.replace(/^\.\/+/, '');
|
|
const section = sections.find((item) => {
|
|
const header = item[0] ?? '';
|
|
return (
|
|
header.includes(` a/${normalizedPath} `) ||
|
|
header.endsWith(` a/${normalizedPath}`) ||
|
|
header.includes(` b/${normalizedPath}`) ||
|
|
header.endsWith(` b/${normalizedPath}`)
|
|
);
|
|
});
|
|
return section?.join('\n') ?? '';
|
|
};
|