From 98a03d88c62826ff8ccd667dc9c309e34de15b67 Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Mon, 9 Feb 2026 14:46:19 +1100 Subject: [PATCH] Fix newline insertion bug in replace tool Restrict indentation capture to horizontal whitespace in calculateRegexReplacement and calculateFlexibleReplacement to prevent extra newlines from being inserted when the target block is preceded by blank lines. --- 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 445e0482023..56dc2cb2c4f 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 40ae914f50a..d7c8973a911 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);