diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index a7169e99f2..4f04b271ac 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -137,7 +137,22 @@ async function calculateExactReplacement( const normalizedSearch = old_string.replace(/\r\n/g, '\n'); const normalizedReplace = new_string.replace(/\r\n/g, '\n'); - const exactOccurrences = normalizedCode.split(normalizedSearch).length - 1; + const matchRanges: Array<{ start: number; end: number }> = []; + let index = normalizedCode.indexOf(normalizedSearch); + while (index !== -1) { + const startLine = normalizedCode + .substring(0, index) + .split(String.fromCharCode(10)).length; + const endLine = + startLine + normalizedSearch.split(String.fromCharCode(10)).length - 1; + matchRanges.push({ start: startLine, end: endLine }); + index = normalizedCode.indexOf( + normalizedSearch, + index + normalizedSearch.length, + ); + } + + const exactOccurrences = matchRanges.length; if (!params.allow_multiple && exactOccurrences > 1) { return { @@ -145,6 +160,8 @@ async function calculateExactReplacement( occurrences: exactOccurrences, finalOldString: normalizedSearch, finalNewString: normalizedReplace, + strategy: 'exact', + matchRanges, }; } @@ -160,6 +177,8 @@ async function calculateExactReplacement( occurrences: exactOccurrences, finalOldString: normalizedSearch, finalNewString: normalizedReplace, + strategy: 'exact', + matchRanges, }; } @@ -178,9 +197,9 @@ async function calculateFlexibleReplacement( const sourceLines = normalizedCode.match(/.*(?:\n|$)/g)?.slice(0, -1) ?? []; const searchLinesStripped = normalizedSearch - .split('\n') + .split(String.fromCharCode(10)) .map((line: string) => line.trim()); - const replaceLines = normalizedReplace.split('\n'); + const replaceLines = normalizedReplace.split(String.fromCharCode(10)); let flexibleOccurrences = 0; let i = 0; @@ -238,11 +257,13 @@ async function calculateRegexReplacement( let processedString = normalizedSearch; for (const delim of delimiters) { - processedString = processedString.split(delim).join(` ${delim} `); + processedString = processedString + .split(String.fromCharCode(10)) + .join(` ${delim} `); } // Split by any whitespace and remove empty strings. - const tokens = processedString.split(/\s+/).filter(Boolean); + const tokens = processedString.split(String.fromCharCode(10)).filter(Boolean); if (tokens.length === 0) { return null; @@ -265,7 +286,7 @@ async function calculateRegexReplacement( } const occurrences = matches.length; - const newLines = normalizedReplace.split('\n'); + const newLines = normalizedReplace.split(String.fromCharCode(10)); // Use the appropriate regex for replacement based on allow_multiple. const replaceRegex = new RegExp( @@ -765,11 +786,13 @@ class EditToolInvocation } const oldStringSnippet = - this.params.old_string.split('\n')[0].substring(0, 30) + - (this.params.old_string.length > 30 ? '...' : ''); + this.params.old_string + .split(String.fromCharCode(10))[0] + .substring(0, 30) + (this.params.old_string.length > 30 ? '...' : ''); const newStringSnippet = - this.params.new_string.split('\n')[0].substring(0, 30) + - (this.params.new_string.length > 30 ? '...' : ''); + this.params.new_string + .split(String.fromCharCode(10))[0] + .substring(0, 30) + (this.params.new_string.length > 30 ? '...' : ''); if (this.params.old_string === this.params.new_string) { return `No file changes to ${shortenPath(relativePath)}`; @@ -877,10 +900,26 @@ class EditToolInvocation }; } + const totalLength = finalContent.split(String.fromCharCode(10)).length; + const metadataParts = []; + if (editData.matchRanges && editData.matchRanges.length > 0) { + if (editData.matchRanges.length === 1) { + metadataParts.push( + `start_line: ${editData.matchRanges[0].start}, end_line: ${editData.matchRanges[0].end}`, + ); + } else { + const ranges = editData.matchRanges + .map((r) => `${r.start}-${r.end}`) + .join(', '); + metadataParts.push(`ranges: ${ranges}`); + } + } + metadataParts.push(`file_length: ${totalLength}`); + const llmSuccessMessageParts = [ editData.isNewFile - ? `Created new file: ${this.params.file_path} with provided content.` - : `Successfully modified file: ${this.params.file_path} (${editData.occurrences} replacements).`, + ? `Created new file: ${this.params.file_path} with provided content. [file_length: ${totalLength}]` + : `Successfully modified file: ${this.params.file_path}. [${metadataParts.join(', ')}]`, ]; // Return a diff of the file before and after the write so that the agent @@ -1211,7 +1250,7 @@ async function calculateFuzzyReplacement( // so that indices remain valid selectedMatches.sort((a, b) => b.index - a.index); - const newLines = normalizedReplace.split('\n'); + const newLines = normalizedReplace.split(String.fromCharCode(10)); for (const match of selectedMatches) { // If we want to preserve the indentation of the first line of the match: