fix: render multi-file tool diffs safely

Prevents chat crashes from multi-file apply_patch diffs
Splits aggregate patches before rendering each file diff
This commit is contained in:
Bohdan Triapitsyn
2026-05-19 14:24:05 +03:00
parent e0a194eed0
commit 237558130e
@@ -1282,6 +1282,16 @@ type DiffPatchEntry = {
const hasUnifiedDiffHunk = (patch: string): boolean => /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@/m.test(patch);
const isUnifiedFileHeaderPair = (minusLine: string, plusLine: string): boolean => {
if (!minusLine.startsWith('--- ') || !plusLine.startsWith('+++ ')) {
return false;
}
const oldPath = minusLine.slice(4).trim();
const newPath = plusLine.slice(4).trim();
return oldPath.length > 0 && newPath.length > 0;
};
const getUnifiedDiffPath = (patch: string, fallbackTitle: string): string => {
const plusHeader = patch.match(/^\+\+\+\s+(?:[ab]\/(.+)|(.+))$/m);
const rawPath = plusHeader?.[1] ?? plusHeader?.[2];
@@ -1303,9 +1313,7 @@ const splitUnifiedDiffPatch = (patch: string): DiffPatchEntry[] => {
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index] ?? '';
const nextLine = lines[index + 1] ?? '';
const isUnifiedFileHeader = /^---\s+(?:[ab]\/|\/dev\/null|\/)/.test(line)
&& /^\+\+\+\s+(?:[ab]\/|\/dev\/null|\/)/.test(nextLine);
if (line.startsWith('diff --git ') || line.startsWith('Index: ') || isUnifiedFileHeader) {
if (line.startsWith('diff --git ') || line.startsWith('Index: ') || isUnifiedFileHeaderPair(line, nextLine)) {
starts.push(index);
}
}
@@ -1430,7 +1438,8 @@ const getDiffPatchEntries = (
const record = file as { relativePath?: unknown; filePath?: unknown; patch?: unknown; diff?: unknown };
const patch = getPatchText(record.patch) ?? getPatchText(record.diff) ?? '';
if (!patch || !hasUnifiedDiffHunk(patch)) {
const splitPatchEntries = splitUnifiedDiffPatch(patch);
if (!patch || splitPatchEntries.length === 0) {
return null;
}
@@ -1444,12 +1453,13 @@ const getDiffPatchEntries = (
? getRelativePath(rawPath, currentDirectory)
: `File ${index + 1}`;
return {
id: `${title}-${index}`,
title,
patch,
} satisfies DiffPatchEntry;
return splitPatchEntries.map((entry, splitIndex) => ({
id: `${title}-${index}-${splitIndex}`,
title: splitPatchEntries.length === 1 ? title : getRelativePath(entry.title, currentDirectory),
patch: entry.patch,
} satisfies DiffPatchEntry));
})
.flat()
.filter((entry): entry is DiffPatchEntry => entry !== null);
if (entries.length > 0) {