From 8cbe8513391bb36770827a8b0132ad80d6d246f2 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 9 Feb 2026 17:37:53 +1100 Subject: [PATCH] Fix newline insertion bug in replace tool (#18595) --- packages/core/src/tools/edit.test.ts | 37 ++++++++++++++++++++++++++++ packages/core/src/tools/edit.ts | 4 +-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/core/src/tools/edit.test.ts b/packages/core/src/tools/edit.test.ts index 445e048202..56dc2cb2c4 100644 --- a/packages/core/src/tools/edit.test.ts +++ b/packages/core/src/tools/edit.test.ts @@ -372,6 +372,43 @@ describe('EditTool', () => { expect(result.newContent).toBe(expectedContent); expect(result.occurrences).toBe(1); }); + + it('should NOT insert extra newlines when replacing a block preceded by a blank line (regression)', async () => { + const content = '\n function oldFunc() {\n // some code\n }'; + const result = await calculateReplacement(mockConfig, { + params: { + file_path: 'test.js', + instruction: 'test', + old_string: 'function oldFunc() {\n // some code\n }', // Two spaces after function to trigger regex + new_string: 'function newFunc() {\n // new code\n}', // Unindented + }, + currentContent: content, + abortSignal, + }); + + // The blank line at the start should be preserved as-is, + // and the discovered indentation (2 spaces) should be applied to each line. + const expectedContent = '\n function newFunc() {\n // new code\n }'; + expect(result.newContent).toBe(expectedContent); + }); + + it('should NOT insert extra newlines in flexible replacement when old_string starts with a blank line (regression)', async () => { + const content = ' // some comment\n\n function oldFunc() {}'; + const result = await calculateReplacement(mockConfig, { + params: { + file_path: 'test.js', + instruction: 'test', + old_string: '\nfunction oldFunc() {}', + new_string: '\n function newFunc() {}', // Include desired indentation + }, + currentContent: content, + abortSignal, + }); + + // The blank line at the start is preserved, and the new block is inserted. + const expectedContent = ' // some comment\n\n function newFunc() {}'; + expect(result.newContent).toBe(expectedContent); + }); }); describe('validateToolParams', () => { diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index 40ae914f50..d7c8973a91 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -167,7 +167,7 @@ async function calculateFlexibleReplacement( if (isMatch) { flexibleOccurrences++; const firstLineInMatch = window[0]; - const indentationMatch = firstLineInMatch.match(/^(\s*)/); + const indentationMatch = firstLineInMatch.match(/^([ \t]*)/); const indentation = indentationMatch ? indentationMatch[1] : ''; const newBlockWithIndent = replaceLines.map( (line: string) => `${indentation}${line}`, @@ -229,7 +229,7 @@ async function calculateRegexReplacement( // The final pattern captures leading whitespace (indentation) and then matches the token pattern. // 'm' flag enables multi-line mode, so '^' matches the start of any line. - const finalPattern = `^(\\s*)${pattern}`; + const finalPattern = `^([ \t]*)${pattern}`; const flexibleRegex = new RegExp(finalPattern, 'm'); const match = flexibleRegex.exec(currentContent);