diff --git a/src/core/diff/strategies/multi-file-search-replace.ts b/src/core/diff/strategies/multi-file-search-replace.ts index a212cf2b8e7..4125bcff34b 100644 --- a/src/core/diff/strategies/multi-file-search-replace.ts +++ b/src/core/diff/strategies/multi-file-search-replace.ts @@ -251,14 +251,16 @@ Each file requires its own path, start_line, and diff elements. const state = { current: State.START, line: 0 } - // Pattern allows optional '>' after SEARCH to handle AI-generated diffs - // (e.g., Sonnet 4 sometimes adds an extra '>') - const SEARCH_PATTERN = /^<<<<<<< SEARCH>?$/ - const SEARCH = SEARCH_PATTERN.source.replace(/[\^$]/g, "") // Remove regex anchors for display + // Pattern allows optional extra '<' or '>' for SEARCH to handle AI-generated diffs + // (e.g., Sonnet 4 sometimes adds extra markers) + const SEARCH_PATTERN = /^<{7,8} SEARCH>?$/ + const SEARCH = "<<<<<<< SEARCH" // Simplified for display const SEP = "=======" - const REPLACE = ">>>>>>> REPLACE" - const SEARCH_PREFIX = "<<<<<<< " - const REPLACE_PREFIX = ">>>>>>> " + // Pattern allows optional extra '>' or '<' for REPLACE + const REPLACE_PATTERN = /^>{7,8} REPLACE>>>>>> REPLACE" // Simplified for display + const SEARCH_PREFIX_PATTERN = /^<{7,8} / + const REPLACE_PREFIX_PATTERN = /^>{7,8} / const reportMergeConflictError = (found: string, _expected: string) => ({ success: false, @@ -326,7 +328,7 @@ Each file requires its own path, start_line, and diff elements. const lines = diffContent.split("\n") const searchCount = lines.filter((l) => SEARCH_PATTERN.test(l.trim())).length const sepCount = lines.filter((l) => l.trim() === SEP).length - const replaceCount = lines.filter((l) => l.trim() === REPLACE).length + const replaceCount = lines.filter((l) => REPLACE_PATTERN.test(l.trim())).length const likelyBadStructure = searchCount !== replaceCount || sepCount < searchCount @@ -350,29 +352,29 @@ Each file requires its own path, start_line, and diff elements. return likelyBadStructure ? reportInvalidDiffError(SEP, SEARCH) : reportMergeConflictError(SEP, SEARCH) - if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEARCH) - if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH) + if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEARCH) + if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH) if (SEARCH_PATTERN.test(marker)) state.current = State.AFTER_SEARCH - else if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH) + else if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH) break case State.AFTER_SEARCH: - if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, SEP) - if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH) - if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEP) - if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH) + if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, SEP) + if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH) + if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEP) + if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH) if (marker === SEP) state.current = State.AFTER_SEPARATOR break case State.AFTER_SEPARATOR: - if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, REPLACE) - if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, REPLACE) + if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, REPLACE) + if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE) if (marker === SEP) return likelyBadStructure ? reportInvalidDiffError(SEP, REPLACE) : reportMergeConflictError(SEP, REPLACE) - if (marker === REPLACE) state.current = State.START - else if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, REPLACE) + if (REPLACE_PATTERN.test(marker)) state.current = State.START + else if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE) break } } @@ -451,18 +453,18 @@ Each file requires its own path, start_line, and diff elements. /* Regex parts: 1. (?:^|\n) Ensures the first marker starts at the beginning of the file or right after a newline. - 2. (??\s*\n Matches the line "<<<<<<< SEARCH" with optional '>' (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped. + 2. (??\s*\n Matches "<<<<<<< SEARCH" or "<<<<<<< SEARCH>" or "<<<<<<<<" with 7-8 '<' chars (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped. 3. ((?:\:start_line:\s*(\d+)\s*\n))? Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2. 4. ((?:\:end_line:\s*(\d+)\s*\n))? Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits. 5. ((?>>>>>> REPLACE)(?=\n|$) Matches the final ">>>>>>> REPLACE" marker on its own line (and requires a following newline or the end of file). + 9. (?:(?<=\n)(?{7,8} REPLACE>>>>>> REPLACE" or ">>>>>>> REPLACE<" or ">>>>>>>>" with 7-8 '>' chars on its own line (and requires a following newline or the end of file). */ let matches = [ ...diffContent.matchAll( - /(?:^|\n)(??\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?>>>>>> REPLACE)(?=\n|$)/g, + /(?:^|\n)(??\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?{7,8} REPLACE