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 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 getUnifiedDiffPath = (patch: string, fallbackTitle: string): string => {
const plusHeader = patch.match(/^\+\+\+\s+(?:[ab]\/(.+)|(.+))$/m); const plusHeader = patch.match(/^\+\+\+\s+(?:[ab]\/(.+)|(.+))$/m);
const rawPath = plusHeader?.[1] ?? plusHeader?.[2]; const rawPath = plusHeader?.[1] ?? plusHeader?.[2];
@@ -1303,9 +1313,7 @@ const splitUnifiedDiffPatch = (patch: string): DiffPatchEntry[] => {
for (let index = 0; index < lines.length; index += 1) { for (let index = 0; index < lines.length; index += 1) {
const line = lines[index] ?? ''; const line = lines[index] ?? '';
const nextLine = lines[index + 1] ?? ''; const nextLine = lines[index + 1] ?? '';
const isUnifiedFileHeader = /^---\s+(?:[ab]\/|\/dev\/null|\/)/.test(line) if (line.startsWith('diff --git ') || line.startsWith('Index: ') || isUnifiedFileHeaderPair(line, nextLine)) {
&& /^\+\+\+\s+(?:[ab]\/|\/dev\/null|\/)/.test(nextLine);
if (line.startsWith('diff --git ') || line.startsWith('Index: ') || isUnifiedFileHeader) {
starts.push(index); starts.push(index);
} }
} }
@@ -1430,7 +1438,8 @@ const getDiffPatchEntries = (
const record = file as { relativePath?: unknown; filePath?: unknown; patch?: unknown; diff?: unknown }; const record = file as { relativePath?: unknown; filePath?: unknown; patch?: unknown; diff?: unknown };
const patch = getPatchText(record.patch) ?? getPatchText(record.diff) ?? ''; const patch = getPatchText(record.patch) ?? getPatchText(record.diff) ?? '';
if (!patch || !hasUnifiedDiffHunk(patch)) { const splitPatchEntries = splitUnifiedDiffPatch(patch);
if (!patch || splitPatchEntries.length === 0) {
return null; return null;
} }
@@ -1444,12 +1453,13 @@ const getDiffPatchEntries = (
? getRelativePath(rawPath, currentDirectory) ? getRelativePath(rawPath, currentDirectory)
: `File ${index + 1}`; : `File ${index + 1}`;
return { return splitPatchEntries.map((entry, splitIndex) => ({
id: `${title}-${index}`, id: `${title}-${index}-${splitIndex}`,
title, title: splitPatchEntries.length === 1 ? title : getRelativePath(entry.title, currentDirectory),
patch, patch: entry.patch,
} satisfies DiffPatchEntry; } satisfies DiffPatchEntry));
}) })
.flat()
.filter((entry): entry is DiffPatchEntry => entry !== null); .filter((entry): entry is DiffPatchEntry => entry !== null);
if (entries.length > 0) { if (entries.length > 0) {