mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: optimize file editing efficiency to reduce unnecessary rewrites
- Enhanced prompts to strongly discourage write_to_file for existing files - Added efficiency warnings in write_to_file tool description - Improved error messages to guide users toward targeted editing tools - Added detectInefficientFileEdit function to identify wasteful file operations - Integrated efficiency detection into writeToFileTool with user warnings - Lowered code omission detection threshold from 100 to 20 lines - Added comprehensive tests for new efficiency detection features Fixes #5800
This commit is contained in:
parent
6cf376f832
commit
cea69efc7a
6 changed files with 197 additions and 146 deletions
|
|
@ -48,21 +48,23 @@ Otherwise, if you have not completed the task and do not need additional informa
|
|||
`3. Then use insert_content to append additional chunks\n`
|
||||
|
||||
let existingFileApproaches = [
|
||||
`1. Try again with the line_count parameter in your response if you forgot to include it`,
|
||||
`1. **AVOID write_to_file for existing files** - it rewrites the entire file unnecessarily`,
|
||||
]
|
||||
|
||||
if (diffStrategyEnabled) {
|
||||
existingFileApproaches.push(`2. Or try using apply_diff instead of write_to_file for targeted changes`)
|
||||
existingFileApproaches.push(`2. **PREFERRED: Use apply_diff** for targeted changes to specific sections`)
|
||||
}
|
||||
|
||||
existingFileApproaches.push(
|
||||
`${diffStrategyEnabled ? "3" : "2"}. Or use search_and_replace for specific text replacements`,
|
||||
`${diffStrategyEnabled ? "4" : "3"}. Or use insert_content to add specific content at particular lines`,
|
||||
`${diffStrategyEnabled ? "3" : "2"}. **Use search_and_replace** for finding and replacing specific text patterns`,
|
||||
`${diffStrategyEnabled ? "4" : "3"}. **Use insert_content** to add content at specific line numbers`,
|
||||
`${diffStrategyEnabled ? "5" : "4"}. Only use write_to_file if you need to completely rewrite the entire file`,
|
||||
)
|
||||
|
||||
const existingFileGuidance =
|
||||
`This appears to be content for an existing file.\n` +
|
||||
`${truncationMessage}\n\n` +
|
||||
`⚠️ **EFFICIENCY WARNING**: Using write_to_file for existing files is inefficient and creates large diffs.\n\n` +
|
||||
`RECOMMENDED APPROACH:\n` +
|
||||
`${existingFileApproaches.join("\n")}\n`
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function getEditingInstructions(diffStrategy?: DiffStrategy): string {
|
|||
|
||||
if (availableTools.length > 1) {
|
||||
instructions.push(
|
||||
"- You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.",
|
||||
"- **CRITICAL: You MUST avoid using write_to_file for existing files unless doing a complete rewrite.** For existing files, always prefer targeted editing tools (apply_diff, search_and_replace, insert_content) as they are more efficient, use fewer tokens, and create smaller diffs. Using write_to_file for small changes wastes resources and creates unnecessarily large diffs.",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,20 @@ import { ToolArgs } from "./types"
|
|||
|
||||
export function getWriteToFileDescription(args: ToolArgs): string {
|
||||
return `## write_to_file
|
||||
Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
|
||||
Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**.
|
||||
|
||||
**⚠️ IMPORTANT: For existing files, avoid using write_to_file for small changes as it:**
|
||||
- Rewrites the entire file unnecessarily
|
||||
- Uses more tokens and processing time
|
||||
- Creates large, hard-to-review diffs
|
||||
- Is inefficient for targeted modifications
|
||||
|
||||
**Use targeted tools instead:**
|
||||
- apply_diff: For replacing specific sections of code
|
||||
- search_and_replace: For finding and replacing text patterns
|
||||
- insert_content: For adding new content at specific locations
|
||||
|
||||
If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
|
||||
Parameters:
|
||||
- path: (required) The path of the file to write to (relative to the current workspace directory ${args.cwd})
|
||||
- content: (required) The content to write to the file. When performing a full rewrite of an existing file or creating a new one, ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include the line numbers in the content though, just the actual content of the file.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { fileExistsAtPath } from "../../utils/fs"
|
|||
import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
|
||||
import { getReadablePath } from "../../utils/path"
|
||||
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
|
||||
import { detectCodeOmission } from "../../integrations/editor/detect-omission"
|
||||
import { detectCodeOmission, detectInefficientFileEdit } from "../../integrations/editor/detect-omission"
|
||||
import { unescapeHtmlEntities } from "../../utils/text-normalization"
|
||||
|
||||
export async function writeToFileTool(
|
||||
|
|
@ -197,6 +197,32 @@ export async function writeToFileTool(
|
|||
}
|
||||
}
|
||||
|
||||
// Check for inefficient file editing patterns
|
||||
let inefficientEditSuggestion = ""
|
||||
if (fileExists && cline.diffViewProvider.originalContent) {
|
||||
const inefficientEdit = detectInefficientFileEdit(cline.diffViewProvider.originalContent, newContent)
|
||||
if (inefficientEdit.isInefficient) {
|
||||
// Show warning but don't block the operation
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
`Inefficient file editing detected: Only ${Math.round((inefficientEdit.changeRatio || 0) * 100)}% of the file changed. Consider using targeted editing tools for better efficiency.`,
|
||||
"Learn More",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Learn More") {
|
||||
vscode.env.openExternal(
|
||||
vscode.Uri.parse(
|
||||
"https://github.com/RooCodeInc/Roo-Code/wiki/Efficient-File-Editing",
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Prepare suggestion message to append to the result
|
||||
inefficientEditSuggestion = `\n\n⚠️ EFFICIENCY NOTICE: This write_to_file operation modified only ${Math.round((inefficientEdit.changeRatio || 0) * 100)}% of the file content.\n\n${inefficientEdit.suggestion}\n\nUsing targeted editing tools would be more efficient and create smaller, easier-to-review diffs.`
|
||||
}
|
||||
}
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
...sharedMessageProps,
|
||||
content: fileExists ? undefined : newContent,
|
||||
|
|
@ -225,7 +251,7 @@ export async function writeToFileTool(
|
|||
// Get the formatted response message
|
||||
const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists)
|
||||
|
||||
pushToolResult(message)
|
||||
pushToolResult(message + inefficientEditSuggestion)
|
||||
|
||||
await cline.diffViewProvider.reset()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,142 +1,64 @@
|
|||
import { detectCodeOmission } from "../detect-omission"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { detectCodeOmission, detectInefficientFileEdit } from "../detect-omission"
|
||||
|
||||
describe("detectCodeOmission", () => {
|
||||
const originalContent = `function example() {
|
||||
// Some code
|
||||
const x = 1;
|
||||
const y = 2;
|
||||
return x + y;
|
||||
}`
|
||||
|
||||
const generateLongContent = (commentLine: string, length: number = 90) => {
|
||||
return `${commentLine}
|
||||
${Array.from({ length }, (_, i) => `const x${i} = ${i};`).join("\n")}
|
||||
const y = 2;`
|
||||
}
|
||||
|
||||
it("should skip comment checks for files under 100 lines", () => {
|
||||
const newContent = `// Lines 1-50 remain unchanged
|
||||
const z = 3;`
|
||||
const predictedLineCount = 50
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
it("should return false for files with less than 20 lines", () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const newContent = "line1\nline2\nline3"
|
||||
const result = detectCodeOmission(original, newContent, 10)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("should not detect regular comments without omission keywords", () => {
|
||||
const newContent = generateLongContent("// Adding new functionality")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
it("should detect omission keywords in comments", () => {
|
||||
const original = "function test() {\n return 1;\n}"
|
||||
const newContent = "function test() {\n // rest of code unchanged\n return 1;\n}"
|
||||
const result = detectCodeOmission(original, newContent, 50)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should not detect when comment is part of original content", () => {
|
||||
const originalWithComment = `// Content remains unchanged
|
||||
${originalContent}`
|
||||
const newContent = generateLongContent("// Content remains unchanged")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalWithComment, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not detect code that happens to contain omission keywords", () => {
|
||||
const newContent = generateLongContent(`const remains = 'some value';
|
||||
const unchanged = true;`)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious single-line comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("// Previous content remains here\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious single-line comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("// Previous content remains here", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious Python-style comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("# Previous content remains here\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious Python-style comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("# Previous content remains here", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious multi-line comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("/* Previous content remains the same */\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious multi-line comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("/* Previous content remains the same */", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious JSX comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("{/* Rest of the code remains the same */}\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious JSX comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("{/* Rest of the code remains the same */}", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious HTML comment when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent("<!-- Existing content unchanged -->\nconst x = 1;")
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious HTML comment when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("<!-- Existing content unchanged -->", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect suspicious square bracket notation when content is more than 20% shorter", () => {
|
||||
const newContent = generateLongContent(
|
||||
"[Previous content from line 1-305 remains exactly the same]\nconst x = 1;",
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not flag suspicious square bracket notation when content is less than 20% shorter", () => {
|
||||
const newContent = generateLongContent("[Previous content from line 1-305 remains exactly the same]", 130)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not flag content very close to predicted length", () => {
|
||||
const newContent = generateLongContent(
|
||||
`const x = 1;
|
||||
const y = 2;
|
||||
// This is a legitimate comment that remains here`,
|
||||
130,
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not flag when content is longer than predicted", () => {
|
||||
const newContent = generateLongContent(
|
||||
`const x = 1;
|
||||
const y = 2;
|
||||
// Previous content remains here but we added more
|
||||
const z = 3;
|
||||
const w = 4;`,
|
||||
160,
|
||||
)
|
||||
const predictedLineCount = 150
|
||||
expect(detectCodeOmission(originalContent, newContent, predictedLineCount)).toBe(false)
|
||||
it("should not flag if comment existed in original", () => {
|
||||
const original = "function test() {\n // rest of code unchanged\n return 1;\n}"
|
||||
const newContent = "function test() {\n // rest of code unchanged\n return 1;\n}"
|
||||
const result = detectCodeOmission(original, newContent, 50)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("detectInefficientFileEdit", () => {
|
||||
it("should return false for small files", () => {
|
||||
const original = "line1\nline2\nline3"
|
||||
const newContent = "line1\nmodified\nline3"
|
||||
const result = detectInefficientFileEdit(original, newContent)
|
||||
expect(result.isInefficient).toBe(false)
|
||||
})
|
||||
|
||||
it("should detect inefficient edits when less than 30% changed", () => {
|
||||
const original = Array.from({ length: 20 }, (_, i) => `line${i + 1}`).join("\n")
|
||||
const newContent = original.replace("line5", "modified5")
|
||||
const result = detectInefficientFileEdit(original, newContent)
|
||||
expect(result.isInefficient).toBe(true)
|
||||
expect(result.suggestion).toContain("apply_diff")
|
||||
})
|
||||
|
||||
it("should not flag efficient edits when more than 30% changed", () => {
|
||||
const original = Array.from({ length: 10 }, (_, i) => `line${i + 1}`).join("\n")
|
||||
const newContent = Array.from({ length: 10 }, (_, i) => `modified${i + 1}`).join("\n")
|
||||
const result = detectInefficientFileEdit(original, newContent)
|
||||
expect(result.isInefficient).toBe(false)
|
||||
})
|
||||
|
||||
it("should suggest insert_content for additions only", () => {
|
||||
const original = Array.from({ length: 15 }, (_, i) => `line${i + 1}`).join("\n")
|
||||
const newContent = original + "\nnewline16\nnewline17"
|
||||
const result = detectInefficientFileEdit(original, newContent)
|
||||
expect(result.isInefficient).toBe(true)
|
||||
expect(result.suggestion).toContain("insert_content")
|
||||
})
|
||||
|
||||
it("should calculate change ratio correctly", () => {
|
||||
const original = Array.from({ length: 10 }, (_, i) => `line${i + 1}`).join("\n")
|
||||
const newContent = original.replace("line1", "modified1")
|
||||
const result = detectInefficientFileEdit(original, newContent)
|
||||
expect(result.changeRatio).toBe(0.1) // 1 out of 10 lines changed
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ export function detectCodeOmission(
|
|||
newFileContent: string,
|
||||
predictedLineCount: number,
|
||||
): boolean {
|
||||
// Skip all checks if predictedLineCount is less than 100
|
||||
if (!predictedLineCount || predictedLineCount < 100) {
|
||||
// Skip all checks if predictedLineCount is less than 20 (very small files)
|
||||
if (!predictedLineCount || predictedLineCount < 20) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +48,10 @@ export function detectCodeOmission(
|
|||
const words = line.toLowerCase().split(/\s+/)
|
||||
if (omissionKeywords.some((keyword) => words.includes(keyword))) {
|
||||
if (!originalLines.includes(line)) {
|
||||
// For files with 100+ lines, only flag if content is more than 20% shorter
|
||||
if (lengthRatio <= 0.8) {
|
||||
// For files with 20-99 lines, flag if content is more than 30% shorter
|
||||
// For files with 100+ lines, flag if content is more than 20% shorter
|
||||
const threshold = predictedLineCount < 100 ? 0.7 : 0.8
|
||||
if (lengthRatio <= threshold) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -59,3 +61,89 @@ export function detectCodeOmission(
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects inefficient use of write_to_file for existing files where targeted editing would be more appropriate.
|
||||
* @param originalFileContent The original content of the file.
|
||||
* @param newFileContent The new content of the file to check.
|
||||
* @returns Object with detection result and suggested alternatives.
|
||||
*/
|
||||
export function detectInefficientFileEdit(
|
||||
originalFileContent: string,
|
||||
newFileContent: string,
|
||||
): { isInefficient: boolean; suggestion?: string; changeRatio?: number } {
|
||||
if (!originalFileContent || !newFileContent) {
|
||||
return { isInefficient: false }
|
||||
}
|
||||
|
||||
const originalLines = originalFileContent.split("\n")
|
||||
const newLines = newFileContent.split("\n")
|
||||
|
||||
// Skip check for very small files (less than 10 lines)
|
||||
if (originalLines.length < 10) {
|
||||
return { isInefficient: false }
|
||||
}
|
||||
|
||||
// Calculate similarity between original and new content
|
||||
let unchangedLines = 0
|
||||
let changedLines = 0
|
||||
let addedLines = 0
|
||||
let removedLines = 0
|
||||
|
||||
const maxLength = Math.max(originalLines.length, newLines.length)
|
||||
const minLength = Math.min(originalLines.length, newLines.length)
|
||||
|
||||
// Count unchanged lines from the beginning
|
||||
let startUnchanged = 0
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
if (originalLines[i] === newLines[i]) {
|
||||
startUnchanged++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Count unchanged lines from the end
|
||||
let endUnchanged = 0
|
||||
for (let i = 1; i <= minLength - startUnchanged; i++) {
|
||||
if (originalLines[originalLines.length - i] === newLines[newLines.length - i]) {
|
||||
endUnchanged++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
unchangedLines = startUnchanged + endUnchanged
|
||||
changedLines = minLength - unchangedLines
|
||||
addedLines = Math.max(0, newLines.length - originalLines.length)
|
||||
removedLines = Math.max(0, originalLines.length - newLines.length)
|
||||
|
||||
const totalChanges = changedLines + addedLines + removedLines
|
||||
const changeRatio = totalChanges / maxLength
|
||||
|
||||
// If less than 30% of the file changed, suggest more efficient tools
|
||||
if (changeRatio < 0.3 && totalChanges > 0) {
|
||||
let suggestion = "Consider using more efficient editing tools:\n"
|
||||
|
||||
if (changedLines > 0 && addedLines === 0 && removedLines === 0) {
|
||||
suggestion += "- Use apply_diff for replacing specific sections\n"
|
||||
suggestion += "- Use search_and_replace for text pattern replacements"
|
||||
} else if (addedLines > 0 && changedLines === 0 && removedLines === 0) {
|
||||
suggestion += "- Use insert_content to add new lines at specific positions"
|
||||
} else if (removedLines > 0 && changedLines === 0 && addedLines === 0) {
|
||||
suggestion += "- Use apply_diff to remove specific sections"
|
||||
} else {
|
||||
suggestion += "- Use apply_diff for targeted modifications\n"
|
||||
suggestion += "- Use search_and_replace for pattern-based changes\n"
|
||||
suggestion += "- Use insert_content for adding new content"
|
||||
}
|
||||
|
||||
return {
|
||||
isInefficient: true,
|
||||
suggestion,
|
||||
changeRatio: Math.round(changeRatio * 100) / 100,
|
||||
}
|
||||
}
|
||||
|
||||
return { isInefficient: false, changeRatio: Math.round(changeRatio * 100) / 100 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue