Browse Source

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.
Sam Hoang Van 9 months ago
parent
commit
1e0e01b9f3

+ 6 - 0
src/core/Cline.ts

@@ -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

+ 6 - 0
src/core/assistant-message/index.ts

@@ -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">>

+ 308 - 0
src/core/prompts/__tests__/__snapshots__/system.test.ts.snap

@@ -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:

+ 9 - 0
src/core/prompts/sections/rules.ts

@@ -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.",

+ 25 - 0
src/core/prompts/tools/append-to-file.ts

@@ -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>`
+}

+ 3 - 0
src/core/prompts/tools/index.ts

@@ -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,

+ 331 - 0
src/core/tools/__tests__/appendToFileTool.test.ts

@@ -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()
+		})
+	})
+})

+ 175 - 0
src/core/tools/appendToFileTool.ts

@@ -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
+	}
+}

+ 1 - 0
src/exports/roo-code.d.ts

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

+ 1 - 0
src/exports/types.ts

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

+ 2 - 1
src/schemas/index.ts

@@ -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>

+ 3 - 0
src/shared/__tests__/experiments.test.ts

@@ -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)
 		})

+ 2 - 0
src/shared/experiments.ts

@@ -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(

+ 2 - 1
src/shared/tool-groups.ts

@@ -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"],