feat: add append_to_file tool for appending content to files (#2712)

- Implemented the append_to_file tool to allow users to append content to existing files or create new ones if they do not exist.
- Updated the rules and instructions to include the new tool.
- Added tests for the append_to_file functionality, covering various scenarios including error handling and content preprocessing.
- Enhanced the experiment schema to include the new append_to_file experiment ID.
- Updated relevant interfaces and types to accommodate the new tool.
- Modified the UI to display the append_to_file tool in the appropriate sections.
This commit is contained in:
Sam Hoang Van 2025-04-18 02:58:22 +07:00 committed by GitHub
parent d3c37eaec8
commit 1e0e01b9f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 874 additions and 2 deletions

View file

@ -76,6 +76,7 @@ import { askFollowupQuestionTool } from "./tools/askFollowupQuestionTool"
import { switchModeTool } from "./tools/switchModeTool"
import { attemptCompletionTool } from "./tools/attemptCompletionTool"
import { newTaskTool } from "./tools/newTaskTool"
import { appendToFileTool } from "./tools/appendToFileTool"
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
@ -1390,6 +1391,8 @@ export class Cline extends EventEmitter<ClineEvents> {
return `[${block.name} for '${block.params.task}']`
case "write_to_file":
return `[${block.name} for '${block.params.path}']`
case "append_to_file":
return `[${block.name} for '${block.params.path}']`
case "apply_diff":
return `[${block.name} for '${block.params.path}']`
case "search_files":
@ -1573,6 +1576,9 @@ export class Cline extends EventEmitter<ClineEvents> {
case "write_to_file":
await writeToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
case "append_to_file":
await appendToFileTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
case "apply_diff":
await applyDiffTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
break

View file

@ -12,6 +12,7 @@ export const toolUseNames = [
"execute_command",
"read_file",
"write_to_file",
"append_to_file",
"apply_diff",
"insert_content",
"search_and_replace",
@ -94,6 +95,11 @@ export interface WriteToFileToolUse extends ToolUse {
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content" | "line_count">>
}
export interface AppendToFileToolUse extends ToolUse {
name: "append_to_file"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content">>
}
export interface InsertCodeBlockToolUse extends ToolUse {
name: "insert_content"
params: Partial<Pick<Record<ToolParamName, string>, "path" | "operations">>

View file

@ -179,6 +179,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -576,6 +598,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## insert_content
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content and code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new content and code to files.
Parameters:
@ -1062,6 +1106,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## search_and_replace
Description: Request to perform search and replace operations on a file. Each operation can specify a search pattern (string or regex) and replacement text, with optional line range restrictions and regex flags. Shows a diff preview before applying changes.
Parameters:
@ -1512,6 +1578,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -1909,6 +1997,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -2306,6 +2416,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -2703,6 +2835,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## browser_action
Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.
@ -3156,6 +3310,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -3621,6 +3797,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## browser_action
Description: Request to interact with a Puppeteer-controlled browser. Every action, except \`close\`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
- The sequence of actions **must always start with** launching the browser at a URL, and **must always end with** closing the browser. If you need to visit a new URL that is not possible to navigate to from the current webpage, you must first close the browser, then launch again at the new URL.
@ -4164,6 +4362,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -4563,6 +4783,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## execute_command
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
Parameters:
@ -5002,6 +5244,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## insert_content
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content and code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new content and code to files.
Parameters:
@ -5561,6 +5825,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## insert_content
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content and code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new content and code to files.
Parameters:
@ -6405,6 +6691,28 @@ Example: Requesting to write to frontend-config.json
<line_count>14</line_count>
</write_to_file>
## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory /test/path)
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>
## insert_content
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content and code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new content and code to files.
Parameters:

View file

@ -16,6 +16,9 @@ function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Recor
if (experiments?.["insert_content"]) {
availableTools.push("insert_content (for adding lines to existing files)")
}
if (experiments?.["append_to_file"]) {
availableTools.push("append_to_file (for appending content to the end of files)")
}
if (experiments?.["search_and_replace"]) {
availableTools.push("search_and_replace (for finding and replacing individual pieces of text)")
}
@ -32,6 +35,12 @@ function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Recor
)
}
if (experiments?.["append_to_file"]) {
instructions.push(
"- The append_to_file tool adds content to the end of files, such as appending new log entries or adding new data records. This tool will always add the content at the end of the file.",
)
}
if (experiments?.["search_and_replace"]) {
instructions.push(
"- The search_and_replace tool finds and replaces text or regex in files. This tool allows you to search for a specific regex pattern or text and replace it with another value. Be cautious when using this tool to ensure you are replacing the correct text. It can support multiple operations at once.",

View file

@ -0,0 +1,25 @@
import { ToolArgs } from "./types"
export function getAppendToFileDescription(args: ToolArgs): string {
return `## append_to_file
Description: Request to append content to a file at the specified path. If the file exists, the content will be appended to the end of the file. If the file doesn't exist, it will be created with the provided content. This tool will automatically create any directories needed to write the file.
Parameters:
- path: (required) The path of the file to append to (relative to the current workspace directory ${args.cwd})
- content: (required) The content to append to the file. The content will be added at the end of the existing file content. Do NOT include line numbers in the content.
Usage:
<append_to_file>
<path>File path here</path>
<content>
Your content to append here
</content>
</append_to_file>
Example: Requesting to append to a log file
<append_to_file>
<path>logs/app.log</path>
<content>
[2024-04-17 15:20:30] New log entry
[2024-04-17 15:20:31] Another log entry
</content>
</append_to_file>`
}

View file

@ -2,6 +2,7 @@ import { getExecuteCommandDescription } from "./execute-command"
import { getReadFileDescription } from "./read-file"
import { getFetchInstructionsDescription } from "./fetch-instructions"
import { getWriteToFileDescription } from "./write-to-file"
import { getAppendToFileDescription } from "./append-to-file"
import { getSearchFilesDescription } from "./search-files"
import { getListFilesDescription } from "./list-files"
import { getInsertContentDescription } from "./insert-content"
@ -26,6 +27,7 @@ const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined>
read_file: (args) => getReadFileDescription(args),
fetch_instructions: () => getFetchInstructionsDescription(),
write_to_file: (args) => getWriteToFileDescription(args),
append_to_file: (args) => getAppendToFileDescription(args),
search_files: (args) => getSearchFilesDescription(args),
list_files: (args) => getListFilesDescription(args),
list_code_definition_names: (args) => getListCodeDefinitionNamesDescription(args),
@ -101,6 +103,7 @@ export {
getReadFileDescription,
getFetchInstructionsDescription,
getWriteToFileDescription,
getAppendToFileDescription,
getSearchFilesDescription,
getListFilesDescription,
getListCodeDefinitionNamesDescription,

View file

@ -0,0 +1,331 @@
// npx jest src/core/tools/__tests__/appendToFileTool.test.ts
import { describe, expect, it, jest, beforeEach } from "@jest/globals"
import { appendToFileTool } from "../appendToFileTool"
import { Cline } from "../../Cline"
import { ToolUse } from "../../assistant-message"
import { formatResponse } from "../../prompts/responses"
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../types"
import { ClineSayTool, ClineAsk } from "../../../shared/ExtensionMessage"
import { RecordSource } from "../../context-tracking/FileContextTrackerTypes"
import { FileContextTracker } from "../../context-tracking/FileContextTracker"
import { DiffViewProvider } from "../../../integrations/editor/DiffViewProvider"
import { RooIgnoreController } from "../../ignore/RooIgnoreController"
// Mock dependencies
jest.mock("../../Cline")
jest.mock("../../prompts/responses")
jest.mock("delay")
describe("appendToFileTool", () => {
// Setup common test variables
let mockCline: jest.Mocked<Partial<Cline>> & {
consecutiveMistakeCount: number
didEditFile: boolean
cwd: string
}
let mockAskApproval: jest.Mock
let mockHandleError: jest.Mock
let mockPushToolResult: jest.Mock
let mockRemoveClosingTag: jest.Mock
let mockToolUse: ToolUse
let mockDiffViewProvider: jest.Mocked<Partial<DiffViewProvider>>
let mockFileContextTracker: jest.Mocked<Partial<FileContextTracker>>
beforeEach(() => {
// Reset mocks
jest.clearAllMocks()
mockDiffViewProvider = {
editType: undefined,
isEditing: false,
originalContent: "",
open: jest.fn().mockReturnValue(Promise.resolve()),
update: jest.fn().mockReturnValue(Promise.resolve()),
reset: jest.fn().mockReturnValue(Promise.resolve()),
revertChanges: jest.fn().mockReturnValue(Promise.resolve()),
saveChanges: jest.fn().mockReturnValue(
Promise.resolve({
newProblemsMessage: "",
userEdits: undefined,
finalContent: "",
}),
),
scrollToFirstDiff: jest.fn(),
createdDirs: [],
documentWasOpen: false,
streamedLines: [],
preDiagnostics: [],
postDiagnostics: [],
isEditorOpen: false,
hasChanges: false,
} as unknown as jest.Mocked<DiffViewProvider>
mockFileContextTracker = {
trackFileContext: jest.fn().mockReturnValue(Promise.resolve()),
} as unknown as jest.Mocked<FileContextTracker>
// Create mock implementations
const mockClineBase = {
ask: jest.fn().mockReturnValue(
Promise.resolve({
response: { type: "text" as ClineAsk },
text: "",
}),
),
say: jest.fn().mockReturnValue(Promise.resolve()),
sayAndCreateMissingParamError: jest.fn().mockReturnValue(Promise.resolve("Missing parameter error")),
consecutiveMistakeCount: 0,
didEditFile: false,
cwd: "/test/path",
diffViewProvider: mockDiffViewProvider,
getFileContextTracker: jest.fn().mockReturnValue(mockFileContextTracker),
rooIgnoreController: {
validateAccess: jest.fn().mockReturnValue(true),
} as unknown as RooIgnoreController,
api: {
getModel: jest.fn().mockReturnValue({
id: "gpt-4",
info: {
contextWindow: 8000,
supportsPromptCache: true,
maxTokens: null,
supportsImages: false,
supportsComputerUse: true,
supportsFunctionCalling: true,
supportsVision: false,
isMultiModal: false,
isChatBased: true,
isCompletionBased: false,
cachableFields: [],
},
}),
createMessage: jest.fn(),
countTokens: jest.fn(),
},
}
// Create a properly typed mock
mockCline = {
...mockClineBase,
consecutiveMistakeCount: 0,
didEditFile: false,
cwd: "/test/path",
} as unknown as jest.Mocked<Partial<Cline>> & {
consecutiveMistakeCount: number
didEditFile: boolean
cwd: string
}
mockAskApproval = jest.fn().mockReturnValue(Promise.resolve(true))
mockHandleError = jest.fn().mockReturnValue(Promise.resolve())
mockPushToolResult = jest.fn()
mockRemoveClosingTag = jest.fn().mockImplementation((tag, value) => value)
// Create a mock tool use object
mockToolUse = {
type: "tool_use",
name: "append_to_file",
params: {
path: "test.txt",
content: "test content",
},
partial: false,
}
})
describe("Basic functionality", () => {
it("should append content to a new file", async () => {
// Setup
mockDiffViewProvider.editType = "create"
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockDiffViewProvider.open).toHaveBeenCalledWith("test.txt")
expect(mockDiffViewProvider.update).toHaveBeenCalledWith("test content", true)
expect(mockAskApproval).toHaveBeenCalled()
expect(mockFileContextTracker.trackFileContext).toHaveBeenCalledWith("test.txt", "roo_edited")
expect(mockCline.didEditFile).toBe(true)
})
it("should append content to an existing file", async () => {
// Setup
mockDiffViewProvider.editType = "modify"
mockDiffViewProvider.originalContent = "existing content"
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockDiffViewProvider.open).toHaveBeenCalledWith("test.txt")
expect(mockDiffViewProvider.update).toHaveBeenCalledWith("existing content\ntest content", true)
// The tool adds its own newline between existing and new content
expect(mockAskApproval).toHaveBeenCalled()
expect(mockFileContextTracker.trackFileContext).toHaveBeenCalledWith("test.txt", "roo_edited")
})
})
describe("Content preprocessing", () => {
it("should remove code block markers", async () => {
// Setup
mockToolUse.params.content = "```\ntest content\n```"
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockDiffViewProvider.update).toHaveBeenCalledWith("test content", true)
})
it("should unescape HTML entities for non-Claude models", async () => {
// Setup
mockToolUse.params.content = "test &amp; content"
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockDiffViewProvider.update).toHaveBeenCalledWith("test & content", true)
})
})
describe("Error handling", () => {
it("should handle missing path parameter", async () => {
// Setup
mockToolUse.params.path = undefined
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockDiffViewProvider.open).not.toHaveBeenCalled()
})
it("should handle missing content parameter", async () => {
// Setup
mockToolUse.params.content = undefined
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockDiffViewProvider.open).not.toHaveBeenCalled()
})
it("should handle rooignore validation failures", async () => {
// Setup
const validateAccessMock = jest.fn().mockReturnValue(false) as jest.MockedFunction<
(filePath: string) => boolean
>
mockCline.rooIgnoreController = {
validateAccess: validateAccessMock,
} as unknown as RooIgnoreController
const mockRooIgnoreError = "RooIgnore error"
;(formatResponse.rooIgnoreError as jest.Mock).mockReturnValue(mockRooIgnoreError)
;(formatResponse.toolError as jest.Mock).mockReturnValue("Tool error")
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockCline.say).toHaveBeenCalledWith("rooignore_error", "test.txt")
expect(formatResponse.rooIgnoreError).toHaveBeenCalledWith("test.txt")
expect(mockPushToolResult).toHaveBeenCalled()
expect(mockDiffViewProvider.open).not.toHaveBeenCalled()
})
it("should handle user rejection", async () => {
// Setup
mockAskApproval.mockReturnValue(Promise.resolve(false))
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockDiffViewProvider.revertChanges).toHaveBeenCalled()
expect(mockFileContextTracker.trackFileContext).not.toHaveBeenCalled()
})
})
describe("Partial updates", () => {
it("should handle partial updates", async () => {
// Setup
mockToolUse.partial = true
// Execute
await appendToFileTool(
mockCline as unknown as Cline,
mockToolUse,
mockAskApproval as unknown as AskApproval,
mockHandleError as unknown as HandleError,
mockPushToolResult as unknown as PushToolResult,
mockRemoveClosingTag as unknown as RemoveClosingTag,
)
// Verify
expect(mockCline.ask).toHaveBeenCalledWith("tool", expect.any(String), true)
expect(mockDiffViewProvider.update).toHaveBeenCalledWith("test content", false)
expect(mockAskApproval).not.toHaveBeenCalled()
})
})
})

View file

@ -0,0 +1,175 @@
import * as vscode from "vscode"
import { Cline } from "../Cline"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { ToolUse } from "../assistant-message"
import { formatResponse } from "../prompts/responses"
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import path from "path"
import { fileExistsAtPath } from "../../utils/fs"
import { addLineNumbers, stripLineNumbers } from "../../integrations/misc/extract-text"
import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
import delay from "delay"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
export async function appendToFileTool(
cline: Cline,
block: ToolUse,
askApproval: AskApproval,
handleError: HandleError,
pushToolResult: PushToolResult,
removeClosingTag: RemoveClosingTag,
) {
const relPath: string | undefined = block.params.path
let newContent: string | undefined = block.params.content
if (!relPath || !newContent) {
return
}
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
if (!accessAllowed) {
await cline.say("rooignore_error", relPath)
pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
return
}
// Check if file exists using cached map or fs.access
let fileExists: boolean
if (cline.diffViewProvider.editType !== undefined) {
fileExists = cline.diffViewProvider.editType === "modify"
} else {
const absolutePath = path.resolve(cline.cwd, relPath)
fileExists = await fileExistsAtPath(absolutePath)
cline.diffViewProvider.editType = fileExists ? "modify" : "create"
}
// pre-processing newContent for cases where weaker models might add artifacts
if (newContent.startsWith("```")) {
newContent = newContent.split("\n").slice(1).join("\n").trim()
}
if (newContent.endsWith("```")) {
newContent = newContent.split("\n").slice(0, -1).join("\n").trim()
}
if (!cline.api.getModel().id.includes("claude")) {
newContent = unescapeHtmlEntities(newContent)
}
// Determine if the path is outside the workspace
const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)) : ""
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
const sharedMessageProps: ClineSayTool = {
tool: fileExists ? "appliedDiff" : "newFileCreated",
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
isOutsideWorkspace,
}
try {
if (block.partial) {
// update gui message
const partialMessage = JSON.stringify(sharedMessageProps)
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
// update editor
if (!cline.diffViewProvider.isEditing) {
await cline.diffViewProvider.open(relPath)
}
// If file exists, append newContent to existing content
if (fileExists && cline.diffViewProvider.originalContent) {
newContent = cline.diffViewProvider.originalContent + "\n" + newContent
}
// editor is open, stream content in
await cline.diffViewProvider.update(
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
false,
)
return
} else {
if (!relPath) {
cline.consecutiveMistakeCount++
pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "path"))
await cline.diffViewProvider.reset()
return
}
if (!newContent) {
cline.consecutiveMistakeCount++
pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "content"))
await cline.diffViewProvider.reset()
return
}
cline.consecutiveMistakeCount = 0
if (!cline.diffViewProvider.isEditing) {
const partialMessage = JSON.stringify(sharedMessageProps)
await cline.ask("tool", partialMessage, true).catch(() => {})
await cline.diffViewProvider.open(relPath)
}
// If file exists, append newContent to existing content
if (fileExists && cline.diffViewProvider.originalContent) {
newContent = cline.diffViewProvider.originalContent + "\n" + newContent
}
await cline.diffViewProvider.update(
everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent,
true,
)
await delay(300) // wait for diff view to update
cline.diffViewProvider.scrollToFirstDiff()
const completeMessage = JSON.stringify({
...sharedMessageProps,
content: fileExists ? undefined : newContent,
diff: fileExists
? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent)
: undefined,
} satisfies ClineSayTool)
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await cline.diffViewProvider.revertChanges()
return
}
const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges()
// Track file edit operation
if (relPath) {
await cline.getFileContextTracker().trackFileContext(relPath, "roo_edited" as RecordSource)
}
cline.didEditFile = true
if (userEdits) {
await cline.say(
"user_feedback_diff",
JSON.stringify({
tool: fileExists ? "appliedDiff" : "newFileCreated",
path: getReadablePath(cline.cwd, relPath),
diff: userEdits,
} satisfies ClineSayTool),
)
pushToolResult(
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
finalContent || "",
)}\n</final_file_content>\n\n` +
`Please note:\n` +
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
`2. Proceed with the task using this updated file content as the new baseline.\n` +
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
`${newProblemsMessage}`,
)
} else {
pushToolResult(`The content was successfully appended to ${relPath.toPosix()}.${newProblemsMessage}`)
}
await cline.diffViewProvider.reset()
return
}
} catch (error) {
await handleError("appending to file", error)
await cline.diffViewProvider.reset()
return
}
}

View file

@ -286,6 +286,7 @@ type GlobalSettings = {
search_and_replace: boolean
insert_content: boolean
powerSteering: boolean
append_to_file: boolean
}
| undefined
language?:

View file

@ -289,6 +289,7 @@ type GlobalSettings = {
search_and_replace: boolean
insert_content: boolean
powerSteering: boolean
append_to_file: boolean
}
| undefined
language?:

View file

@ -276,7 +276,7 @@ export type CustomSupportPrompts = z.infer<typeof customSupportPromptsSchema>
* ExperimentId
*/
export const experimentIds = ["search_and_replace", "insert_content", "powerSteering"] as const
export const experimentIds = ["search_and_replace", "insert_content", "powerSteering", "append_to_file"] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -290,6 +290,7 @@ const experimentsSchema = z.object({
search_and_replace: z.boolean(),
insert_content: z.boolean(),
powerSteering: z.boolean(),
append_to_file: z.boolean(),
})
export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -16,6 +16,7 @@ describe("experiments", () => {
powerSteering: false,
search_and_replace: false,
insert_content: false,
append_to_file: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})
@ -25,6 +26,7 @@ describe("experiments", () => {
powerSteering: true,
search_and_replace: false,
insert_content: false,
append_to_file: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
})
@ -34,6 +36,7 @@ describe("experiments", () => {
search_and_replace: false,
insert_content: false,
powerSteering: false,
append_to_file: false,
}
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
})

View file

@ -7,6 +7,7 @@ export const EXPERIMENT_IDS = {
INSERT_BLOCK: "insert_content",
SEARCH_AND_REPLACE: "search_and_replace",
POWER_STEERING: "powerSteering",
APPEND_BLOCK: "append_to_file",
} as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -21,6 +22,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
INSERT_BLOCK: { enabled: false },
SEARCH_AND_REPLACE: { enabled: false },
POWER_STEERING: { enabled: false },
APPEND_BLOCK: { enabled: false },
}
export const experimentDefault = Object.fromEntries(

View file

@ -12,6 +12,7 @@ export const TOOL_DISPLAY_NAMES = {
read_file: "read files",
fetch_instructions: "fetch instructions",
write_to_file: "write files",
append_to_file: "append to files",
apply_diff: "apply changes",
search_files: "search files",
list_files: "list files",
@ -33,7 +34,7 @@ export const TOOL_GROUPS: Record<ToolGroup, ToolGroupConfig> = {
tools: ["read_file", "fetch_instructions", "search_files", "list_files", "list_code_definition_names"],
},
edit: {
tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
tools: ["apply_diff", "write_to_file", "append_to_file", "insert_content", "search_and_replace"],
},
browser: {
tools: ["browser_action"],