Просмотр исходного кода

Feat/improve insert block content (#2510)

* refactor: enhance insertGroups and insertContentTool for better handling of insertion operations

* refactor: simplify insert_content tool

- Remove operations-based implementation in favor of single line insertion
- Update parameters from operations to line and content
- Simplify insertion logic and error handling
- Update tool description and documentation
- Remove XML parsing for operations
- Clean up code and improve error messages

* refactor: remove insert_content experiment and related tests

* Remove the append_to_file tool

* Improvements to chat row and instructions

---------

Co-authored-by: Matt Rubens <[email protected]>
Sam Hoang Van 8 месяцев назад
Родитель
Сommit
f06567d579
39 измененных файлов с 176 добавлено и 2285 удалено
  1. 0 1
      evals/packages/types/src/roo-code-defaults.ts
  2. 1 2
      evals/packages/types/src/roo-code.ts
  3. 0 6
      src/core/Cline.ts
  4. 11 4
      src/core/diff/insert-groups.ts
  5. 0 1413
      src/core/prompts/__tests__/__snapshots__/system.test.ts.snap
  6. 1 160
      src/core/prompts/__tests__/system.test.ts
  7. 5 15
      src/core/prompts/sections/rules.ts
  8. 0 25
      src/core/prompts/tools/append-to-file.ts
  9. 0 3
      src/core/prompts/tools/index.ts
  10. 24 26
      src/core/prompts/tools/insert-content.ts
  11. 0 330
      src/core/tools/__tests__/appendToFileTool.test.ts
  12. 0 191
      src/core/tools/appendToFileTool.ts
  13. 34 32
      src/core/tools/insertContentTool.ts
  14. 0 2
      src/exports/roo-code.d.ts
  15. 0 2
      src/exports/types.ts
  16. 1 3
      src/schemas/index.ts
  17. 2 0
      src/shared/ExtensionMessage.ts
  18. 0 3
      src/shared/__tests__/experiments.test.ts
  19. 0 42
      src/shared/__tests__/modes.test.ts
  20. 0 2
      src/shared/experiments.ts
  21. 3 3
      src/shared/tools.ts
  22. 25 0
      webview-ui/src/components/chat/ChatRow.tsx
  23. 8 1
      webview-ui/src/components/chat/ChatView.tsx
  24. 1 4
      webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx
  25. 4 1
      webview-ui/src/i18n/locales/ca/chat.json
  26. 4 1
      webview-ui/src/i18n/locales/de/chat.json
  27. 4 1
      webview-ui/src/i18n/locales/en/chat.json
  28. 4 1
      webview-ui/src/i18n/locales/es/chat.json
  29. 4 1
      webview-ui/src/i18n/locales/fr/chat.json
  30. 4 1
      webview-ui/src/i18n/locales/hi/chat.json
  31. 4 1
      webview-ui/src/i18n/locales/it/chat.json
  32. 4 1
      webview-ui/src/i18n/locales/ja/chat.json
  33. 4 1
      webview-ui/src/i18n/locales/ko/chat.json
  34. 4 1
      webview-ui/src/i18n/locales/pl/chat.json
  35. 4 1
      webview-ui/src/i18n/locales/pt-BR/chat.json
  36. 4 1
      webview-ui/src/i18n/locales/tr/chat.json
  37. 4 1
      webview-ui/src/i18n/locales/vi/chat.json
  38. 4 1
      webview-ui/src/i18n/locales/zh-CN/chat.json
  39. 4 1
      webview-ui/src/i18n/locales/zh-TW/chat.json

+ 0 - 1
evals/packages/types/src/roo-code-defaults.ts

@@ -56,7 +56,6 @@ export const rooCodeDefaults: RooCodeSettings = {
 	diffEnabled: true,
 	fuzzyMatchThreshold: 1.0,
 	experiments: {
-		insert_content: false,
 		powerSteering: false,
 	},
 

+ 1 - 2
evals/packages/types/src/roo-code.ts

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

+ 0 - 6
src/core/Cline.ts

@@ -78,7 +78,6 @@ 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"
 
 // prompts
 import { formatResponse } from "./prompts/responses"
@@ -1242,8 +1241,6 @@ 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":
@@ -1428,9 +1425,6 @@ 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

+ 11 - 4
src/core/diff/insert-groups.ts

@@ -1,7 +1,8 @@
 /**
  * Inserts multiple groups of elements at specified indices in an array
  * @param original Array to insert into, split by lines
- * @param insertGroups Array of groups to insert, each with an index and elements to insert
+ * @param insertGroups Array of groups to insert, each with an index and elements to insert.
+ *                     If index is -1, the elements will be appended to the end of the array.
  * @returns New array with all insertions applied
  */
 export interface InsertGroup {
@@ -10,13 +11,14 @@ export interface InsertGroup {
 }
 
 export function insertGroups(original: string[], insertGroups: InsertGroup[]): string[] {
-	// Sort groups by index to maintain order
-	insertGroups.sort((a, b) => a.index - b.index)
+	// Handle groups with index -1 separately and sort remaining groups by index
+	const appendGroups = insertGroups.filter((group) => group.index === -1)
+	const normalGroups = insertGroups.filter((group) => group.index !== -1).sort((a, b) => a.index - b.index)
 
 	let result: string[] = []
 	let lastIndex = 0
 
-	insertGroups.forEach(({ index, elements }) => {
+	normalGroups.forEach(({ index, elements }) => {
 		// Add elements from original array up to insertion point
 		result.push(...original.slice(lastIndex, index))
 		// Add the group of elements
@@ -27,5 +29,10 @@ export function insertGroups(original: string[], insertGroups: InsertGroup[]): s
 	// Add remaining elements from original array
 	result.push(...original.slice(lastIndex))
 
+	// Append elements from groups with index -1 at the end
+	appendGroups.forEach(({ elements }) => {
+		result.push(...elements)
+	})
+
 	return result
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 1413
src/core/prompts/__tests__/__snapshots__/system.test.ts.snap


+ 1 - 160
src/core/prompts/__tests__/system.test.ts

@@ -170,9 +170,7 @@ describe("SYSTEM_PROMPT", () => {
 
 	beforeEach(() => {
 		// Reset experiments before each test to ensure they're disabled by default
-		experiments = {
-			[EXPERIMENT_IDS.INSERT_BLOCK]: false,
-		}
+		experiments = {}
 	})
 
 	beforeEach(() => {
@@ -477,163 +475,6 @@ describe("SYSTEM_PROMPT", () => {
 		expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE"))
 	})
 
-	describe("experimental tools", () => {
-		it("should disable experimental tools by default", async () => {
-			// Set experiments to explicitly disable experimental tools
-			const experimentsConfig = {
-				[EXPERIMENT_IDS.INSERT_BLOCK]: false,
-			}
-
-			// Reset experiments
-			experiments = experimentsConfig
-
-			const prompt = await SYSTEM_PROMPT(
-				mockContext,
-				"/test/path",
-				false, // supportsComputerUse
-				undefined, // mcpHub
-				undefined, // diffStrategy
-				undefined, // browserViewportSize
-				defaultModeSlug, // mode
-				undefined, // customModePrompts
-				undefined, // customModes
-				undefined, // globalCustomInstructions
-				undefined, // diffEnabled
-				experimentsConfig, // Explicitly disable experimental tools
-				true, // enableMcpServerCreation
-			)
-
-			// Check that experimental tool sections are not included
-			const toolSections = prompt.split("\n## ").slice(1)
-			const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
-			expect(toolNames).not.toContain("insert_content")
-			expect(prompt).toMatchSnapshot()
-		})
-
-		it("should enable experimental tools when explicitly enabled", async () => {
-			// Set experiments for testing experimental features
-			const experimentsEnabled = {
-				[EXPERIMENT_IDS.INSERT_BLOCK]: true,
-			}
-
-			// Reset default experiments
-			experiments = undefined
-
-			const prompt = await SYSTEM_PROMPT(
-				mockContext,
-				"/test/path",
-				false, // supportsComputerUse
-				undefined, // mcpHub
-				undefined, // diffStrategy
-				undefined, // browserViewportSize
-				defaultModeSlug, // mode
-				undefined, // customModePrompts
-				undefined, // customModes
-				undefined, // globalCustomInstructions
-				undefined, // diffEnabled
-				experimentsEnabled, // Use the enabled experiments
-				true, // enableMcpServerCreation
-			)
-
-			// Get all tool sections
-			const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
-			const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
-
-			// Verify experimental tools are included in the prompt when enabled
-			expect(toolNames).toContain("insert_content")
-			expect(prompt).toMatchSnapshot()
-		})
-
-		it("should selectively enable experimental tools", async () => {
-			// Set experiments for testing selective enabling
-			const experimentsSelective = {
-				[EXPERIMENT_IDS.INSERT_BLOCK]: false,
-			}
-
-			// Reset default experiments
-			experiments = undefined
-
-			const prompt = await SYSTEM_PROMPT(
-				mockContext,
-				"/test/path",
-				false, // supportsComputerUse
-				undefined, // mcpHub
-				undefined, // diffStrategy
-				undefined, // browserViewportSize
-				defaultModeSlug, // mode
-				undefined, // customModePrompts
-				undefined, // customModes
-				undefined, // globalCustomInstructions
-				undefined, // diffEnabled
-				experimentsSelective, // Use the selective experiments
-				true, // enableMcpServerCreation
-			)
-
-			// Get all tool sections
-			const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
-			const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
-
-			// Verify only enabled experimental tools are included
-			expect(toolNames).not.toContain("insert_content")
-			expect(prompt).toMatchSnapshot()
-		})
-
-		it("should list all available editing tools in base instruction", async () => {
-			const experiments = {
-				[EXPERIMENT_IDS.INSERT_BLOCK]: true,
-			}
-
-			const prompt = await SYSTEM_PROMPT(
-				mockContext,
-				"/test/path",
-				false,
-				undefined,
-				new MultiSearchReplaceDiffStrategy(),
-				undefined,
-				defaultModeSlug,
-				undefined,
-				undefined,
-				undefined,
-				true, // diffEnabled
-				experiments, // experiments
-				true, // enableMcpServerCreation
-			)
-
-			// Verify base instruction lists all available tools
-			expect(prompt).toContain("apply_diff (for replacing lines in existing files)")
-			expect(prompt).toContain("write_to_file (for creating new files or complete file rewrites)")
-			expect(prompt).toContain("insert_content (for adding lines to existing files)")
-			expect(prompt).toContain("search_and_replace (for finding and replacing individual pieces of text)")
-		})
-		it("should provide detailed instructions for each enabled tool", async () => {
-			const experiments = {
-				[EXPERIMENT_IDS.INSERT_BLOCK]: true,
-			}
-
-			const prompt = await SYSTEM_PROMPT(
-				mockContext,
-				"/test/path",
-				false,
-				undefined,
-				new MultiSearchReplaceDiffStrategy(),
-				undefined,
-				defaultModeSlug,
-				undefined,
-				undefined,
-				undefined,
-				true, // diffEnabled
-				experiments,
-				true, // enableMcpServerCreation
-			)
-
-			// Verify detailed instructions for each tool
-			expect(prompt).toContain(
-				"You should always prefer using other editing tools over write_to_file when making changes to existing files since write_to_file is much slower and cannot handle large files.",
-			)
-			expect(prompt).toContain("The insert_content tool adds lines of text to files")
-		})
-	})
-
 	afterAll(() => {
 		jest.restoreAllMocks()
 	})

+ 5 - 15
src/core/prompts/sections/rules.ts

@@ -14,28 +14,18 @@ function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Recor
 		availableTools.push("write_to_file (for creating new files or complete file rewrites)")
 	}
 
-	availableTools.push("append_to_file (for appending content to the end of files)")
-
-	if (experiments?.["insert_content"]) {
-		availableTools.push("insert_content (for adding lines to existing files)")
-	}
-
+	availableTools.push("insert_content (for adding lines to existing files)")
 	availableTools.push("search_and_replace (for finding and replacing individual pieces of text)")
 
 	// Base editing instruction mentioning all available tools
 	if (availableTools.length > 1) {
-		instructions.push(
-			`- For editing files, you have access to these tools: ${availableTools.join(", ")}.`,
-			"- 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.",
-		)
+		instructions.push(`- For editing files, you have access to these tools: ${availableTools.join(", ")}.`)
 	}
 
 	// Additional details for experimental features
-	if (experiments?.["insert_content"]) {
-		instructions.push(
-			"- The insert_content tool adds lines of text to files, such as adding a new function to a JavaScript file or inserting a new route in a Python file. This tool will insert it at the specified line location. It can support multiple operations at once.",
-		)
-	}
+	instructions.push(
+		"- The insert_content tool adds lines of text to files at a specific line number, such as adding a new function to a JavaScript file or inserting a new route in a Python file. Use line number 0 to append at the end of the file, or any positive number to insert before that line.",
+	)
 
 	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.",

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

@@ -1,25 +0,0 @@
-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>`
-}

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

@@ -8,7 +8,6 @@ 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"
@@ -28,7 +27,6 @@ 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),
@@ -113,7 +111,6 @@ export {
 	getReadFileDescription,
 	getFetchInstructionsDescription,
 	getWriteToFileDescription,
-	getAppendToFileDescription,
 	getSearchFilesDescription,
 	getListFilesDescription,
 	getListCodeDefinitionNamesDescription,

+ 24 - 26
src/core/prompts/tools/insert-content.ts

@@ -2,34 +2,32 @@ import { ToolArgs } from "./types"
 
 export function getInsertContentDescription(args: ToolArgs): string {
 	return `## 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.
+Description: Insert new content at a specific line position in a file.
+
 Parameters:
-- path: (required) The path of the file to insert content into (relative to the current workspace directory ${args.cwd.toPosix()})
-- operations: (required) A JSON array of insertion operations. Each operation is an object with:
-    * start_line: (required) The line number where the content should be inserted.  The content currently at that line will end up below the inserted content.
-    * content: (required) The content to insert at the specified position. IMPORTANT NOTE: If the content is a single line, it can be a string. If it's a multi-line content, it should be a string with newline characters (\n) for line breaks. Make sure to include the correct indentation for the content.
-Usage:
+- path: (required) File path relative to workspace directory ${args.cwd.toPosix()}
+- line: (required) Line number where content will be inserted (1-based)
+	      Use 0 to append at end of file
+	      Use any positive number to insert before that line
+- content: (required) The content to insert at the specified line
+
+Example for inserting imports at start of file:
 <insert_content>
-<path>File path here</path>
-<operations>[
-  {
-    "start_line": 10,
-    "content": "Your content here"
-  }
-]</operations>
+<path>src/utils.ts</path>
+<line>1</line>
+<content>
+// Add imports at start of file
+import { sum } from './math';
+</content>
 </insert_content>
-Example: Insert a new function and its import statement
+
+Example for appending to the end of file:
 <insert_content>
-<path>File path here</path>
-<operations>[
-  {
-    "start_line": 1,
-    "content": "import { sum } from './utils';"
-  },
-  {
-    "start_line": 10,
-    "content": "function calculateTotal(items: number[]): number {\n    return items.reduce((sum, item) => sum + item, 0);\n}"
-  }
-]</operations>
-</insert_content>`
+<path>src/utils.ts</path>
+<line>0</line>
+<content>
+// This is the end of the file
+</content>
+</insert_content>
+`
 }

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

@@ -1,330 +0,0 @@
-// 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 { formatResponse } from "../../prompts/responses"
-import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../../shared/tools"
-import { ClineAsk } from "../../../shared/ExtensionMessage"
-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()
-		})
-	})
-})

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

@@ -1,191 +0,0 @@
-import path from "path"
-import delay from "delay"
-
-import { Cline } from "../Cline"
-import { ClineSayTool } from "../../shared/ExtensionMessage"
-import { formatResponse } from "../prompts/responses"
-import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
-import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
-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 { 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++
-				cline.recordToolError("append_to_file")
-				pushToolResult(await cline.sayAndCreateMissingParamError("append_to_file", "path"))
-				await cline.diffViewProvider.reset()
-				return
-			}
-
-			if (!newContent) {
-				cline.consecutiveMistakeCount++
-				cline.recordToolError("append_to_file")
-				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
-	}
-}

+ 34 - 32
src/core/tools/insertContentTool.ts

@@ -1,12 +1,12 @@
 import delay from "delay"
 import fs from "fs/promises"
+import path from "path"
 
 import { getReadablePath } from "../../utils/path"
 import { Cline } from "../Cline"
 import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
 import { formatResponse } from "../prompts/responses"
 import { ClineSayTool } from "../../shared/ExtensionMessage"
-import path from "path"
 import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
 import { fileExistsAtPath } from "../../utils/fs"
 import { insertGroups } from "../diff/insert-groups"
@@ -20,11 +20,13 @@ export async function insertContentTool(
 	removeClosingTag: RemoveClosingTag,
 ) {
 	const relPath: string | undefined = block.params.path
-	const operations: string | undefined = block.params.operations
+	const line: string | undefined = block.params.line
+	const content: string | undefined = block.params.content
 
 	const sharedMessageProps: ClineSayTool = {
-		tool: "appliedDiff",
+		tool: "insertContent",
 		path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
+		lineNumber: line ? parseInt(line, 10) : undefined,
 	}
 
 	try {
@@ -42,10 +44,17 @@ export async function insertContentTool(
 			return
 		}
 
-		if (!operations) {
+		if (!line) {
+			cline.consecutiveMistakeCount++
+			cline.recordToolError("insert_content")
+			pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "line"))
+			return
+		}
+
+		if (!content) {
 			cline.consecutiveMistakeCount++
 			cline.recordToolError("insert_content")
-			pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "operations"))
+			pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "content"))
 			return
 		}
 
@@ -61,21 +70,11 @@ export async function insertContentTool(
 			return
 		}
 
-		let parsedOperations: Array<{
-			start_line: number
-			content: string
-		}>
-
-		try {
-			parsedOperations = JSON.parse(operations)
-			if (!Array.isArray(parsedOperations)) {
-				throw new Error("Operations must be an array")
-			}
-		} catch (error) {
+		const lineNumber = parseInt(line, 10)
+		if (isNaN(lineNumber) || lineNumber < 0) {
 			cline.consecutiveMistakeCount++
 			cline.recordToolError("insert_content")
-			await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
-			pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
+			pushToolResult(formatResponse.toolError("Invalid line number. Must be a non-negative integer."))
 			return
 		}
 
@@ -87,15 +86,12 @@ export async function insertContentTool(
 		cline.diffViewProvider.originalContent = fileContent
 		const lines = fileContent.split("\n")
 
-		const updatedContent = insertGroups(
-			lines,
-			parsedOperations.map((elem) => {
-				return {
-					index: elem.start_line - 1,
-					elements: elem.content.split("\n"),
-				}
-			}),
-		).join("\n")
+		const updatedContent = insertGroups(lines, [
+			{
+				index: lineNumber - 1,
+				elements: content.split("\n"),
+			},
+		]).join("\n")
 
 		// Show changes in diff view
 		if (!cline.diffViewProvider.isEditing) {
@@ -116,7 +112,11 @@ export async function insertContentTool(
 
 		await cline.diffViewProvider.update(updatedContent, true)
 
-		const completeMessage = JSON.stringify({ ...sharedMessageProps, diff } satisfies ClineSayTool)
+		const completeMessage = JSON.stringify({
+			...sharedMessageProps,
+			diff,
+			lineNumber: lineNumber,
+		} satisfies ClineSayTool)
 
 		const didApprove = await cline
 			.ask("tool", completeMessage, false)
@@ -138,23 +138,25 @@ export async function insertContentTool(
 		cline.didEditFile = true
 
 		if (!userEdits) {
-			pushToolResult(`The content was successfully inserted in ${relPath.toPosix()}.${newProblemsMessage}`)
+			pushToolResult(
+				`The content was successfully inserted in ${relPath.toPosix()} at line ${lineNumber}.${newProblemsMessage}`,
+			)
 			await cline.diffViewProvider.reset()
 			return
 		}
 
 		const userFeedbackDiff = JSON.stringify({
-			tool: "appliedDiff",
+			tool: "insertContent",
 			path: getReadablePath(cline.cwd, relPath),
+			lineNumber: lineNumber,
 			diff: userEdits,
 		} satisfies ClineSayTool)
 
-		console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
 		await cline.say("user_feedback_diff", userFeedbackDiff)
 
 		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:\n\n` +
+				`The updated content has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
 				`<final_file_content path="${relPath.toPosix()}">\n${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` +

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

@@ -288,7 +288,6 @@ type GlobalSettings = {
 	fuzzyMatchThreshold?: number | undefined
 	experiments?:
 		| {
-				insert_content: boolean
 				powerSteering: boolean
 		  }
 		| undefined
@@ -548,7 +547,6 @@ type RooCodeEvents = {
 			| "execute_command"
 			| "read_file"
 			| "write_to_file"
-			| "append_to_file"
 			| "apply_diff"
 			| "insert_content"
 			| "search_and_replace"

+ 0 - 2
src/exports/types.ts

@@ -291,7 +291,6 @@ type GlobalSettings = {
 	fuzzyMatchThreshold?: number | undefined
 	experiments?:
 		| {
-				insert_content: boolean
 				powerSteering: boolean
 		  }
 		| undefined
@@ -557,7 +556,6 @@ type RooCodeEvents = {
 			| "execute_command"
 			| "read_file"
 			| "write_to_file"
-			| "append_to_file"
 			| "apply_diff"
 			| "insert_content"
 			| "search_and_replace"

+ 1 - 3
src/schemas/index.ts

@@ -277,7 +277,7 @@ export type CustomSupportPrompts = z.infer<typeof customSupportPromptsSchema>
  * ExperimentId
  */
 
-export const experimentIds = ["insert_content", "powerSteering"] as const
+export const experimentIds = ["powerSteering"] as const
 
 export const experimentIdsSchema = z.enum(experimentIds)
 
@@ -288,7 +288,6 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
  */
 
 const experimentsSchema = z.object({
-	insert_content: z.boolean(),
 	powerSteering: z.boolean(),
 })
 
@@ -821,7 +820,6 @@ export const toolNames = [
 	"execute_command",
 	"read_file",
 	"write_to_file",
-	"append_to_file",
 	"apply_diff",
 	"insert_content",
 	"search_and_replace",

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -225,6 +225,7 @@ export interface ClineSayTool {
 		| "newTask"
 		| "finishTask"
 		| "searchAndReplace"
+		| "insertContent"
 	path?: string
 	diff?: string
 	content?: string
@@ -239,6 +240,7 @@ export interface ClineSayTool {
 	ignoreCase?: boolean
 	startLine?: number
 	endLine?: number
+	lineNumber?: number
 }
 
 // Must keep in sync with system prompt.

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

@@ -14,7 +14,6 @@ describe("experiments", () => {
 		it("returns false when experiment is not enabled", () => {
 			const experiments: Record<ExperimentId, boolean> = {
 				powerSteering: false,
-				insert_content: false,
 			}
 			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
 		})
@@ -22,14 +21,12 @@ describe("experiments", () => {
 		it("returns true when experiment is enabled", () => {
 			const experiments: Record<ExperimentId, boolean> = {
 				powerSteering: true,
-				insert_content: false,
 			}
 			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
 		})
 
 		it("returns false when experiment is not present", () => {
 			const experiments: Record<ExperimentId, boolean> = {
-				insert_content: false,
 				powerSteering: false,
 			}
 			expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)

+ 0 - 42
src/shared/__tests__/modes.test.ts

@@ -254,48 +254,6 @@ describe("isToolAllowedForMode", () => {
 
 		expect(isToolAllowedForMode("write_to_file", "markdown-editor", customModes, toolRequirements)).toBe(false)
 	})
-
-	describe("experimental tools", () => {
-		it("disables tools when experiment is disabled", () => {
-			const experiments = {
-				insert_content: false,
-			}
-
-			expect(
-				isToolAllowedForMode("insert_content", "test-exp-mode", customModes, undefined, undefined, experiments),
-			).toBe(false)
-		})
-
-		it("allows tools when experiment is enabled", () => {
-			const experiments = {
-				insert_content: true,
-			}
-
-			expect(
-				isToolAllowedForMode("insert_content", "test-exp-mode", customModes, undefined, undefined, experiments),
-			).toBe(true)
-		})
-
-		it("allows non-experimental tools when experiments are disabled", () => {
-			const experiments = {
-				insert_content: false,
-			}
-
-			expect(
-				isToolAllowedForMode("read_file", "markdown-editor", customModes, undefined, undefined, experiments),
-			).toBe(true)
-			expect(
-				isToolAllowedForMode(
-					"write_to_file",
-					"markdown-editor",
-					customModes,
-					undefined,
-					{ path: "test.md" },
-					experiments,
-				),
-			).toBe(true)
-		})
-	})
 })
 
 describe("FileRestrictionError", () => {

+ 0 - 2
src/shared/experiments.ts

@@ -4,7 +4,6 @@ import { AssertEqual, Equals, Keys, Values } from "../utils/type-fu"
 export type { ExperimentId }
 
 export const EXPERIMENT_IDS = {
-	INSERT_BLOCK: "insert_content",
 	POWER_STEERING: "powerSteering",
 } as const satisfies Record<string, ExperimentId>
 
@@ -17,7 +16,6 @@ interface ExperimentConfig {
 }
 
 export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
-	INSERT_BLOCK: { enabled: false },
 	POWER_STEERING: { enabled: false },
 }
 

+ 3 - 3
src/shared/tools.ts

@@ -50,6 +50,7 @@ export const toolParamNames = [
 	"mode_slug",
 	"reason",
 	"operations",
+	"line",
 	"mode",
 	"message",
 	"cwd",
@@ -97,7 +98,7 @@ export interface WriteToFileToolUse extends ToolUse {
 
 export interface InsertCodeBlockToolUse extends ToolUse {
 	name: "insert_content"
-	params: Partial<Pick<Record<ToolParamName, string>, "path" | "operations">>
+	params: Partial<Pick<Record<ToolParamName, string>, "path" | "line" | "content">>
 }
 
 export interface SearchFilesToolUse extends ToolUse {
@@ -167,7 +168,6 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
 	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",
@@ -191,7 +191,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", "append_to_file", "insert_content", "search_and_replace"],
+		tools: ["apply_diff", "write_to_file", "insert_content", "search_and_replace"],
 	},
 	browser: {
 		tools: ["browser_action"],

+ 25 - 0
webview-ui/src/components/chat/ChatRow.tsx

@@ -295,6 +295,31 @@ export const ChatRowContent = ({
 						/>
 					</>
 				)
+			case "insertContent":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("insert")}
+							<span style={{ fontWeight: "bold" }}>
+								{tool.isOutsideWorkspace
+									? t("chat:fileOperations.wantsToEditOutsideWorkspace")
+									: tool.lineNumber === 0
+										? t("chat:fileOperations.wantsToInsertAtEnd")
+										: t("chat:fileOperations.wantsToInsertWithLineNumber", {
+												lineNumber: tool.lineNumber,
+											})}
+							</span>
+						</div>
+						<CodeAccordian
+							progressStatus={message.progressStatus}
+							isLoading={message.partial}
+							diff={tool.diff!}
+							path={tool.path!}
+							isExpanded={isExpanded}
+							onToggleExpand={onToggleExpand}
+						/>
+					</>
+				)
 			case "searchAndReplace":
 				return (
 					<>

+ 8 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -169,6 +169,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 								case "editedExistingFile":
 								case "appliedDiff":
 								case "newFileCreated":
+								case "insertContent":
 									setPrimaryButtonText(t("chat:save.title"))
 									setSecondaryButtonText(t("chat:reject.title"))
 									break
@@ -629,7 +630,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				return true
 			}
 			const tool = JSON.parse(message.text)
-			return ["editedExistingFile", "appliedDiff", "newFileCreated", "searchAndReplace"].includes(tool.tool)
+			return [
+				"editedExistingFile",
+				"appliedDiff",
+				"newFileCreated",
+				"searchAndReplace",
+				"insertContent",
+			].includes(tool.tool)
 		}
 		return false
 	}, [])

+ 1 - 4
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

@@ -207,9 +207,7 @@ describe("mergeExtensionState", () => {
 		const prevState: ExtensionState = {
 			...baseState,
 			apiConfiguration: { modelMaxTokens: 1234, modelMaxThinkingTokens: 123 },
-			experiments: {
-				insert_content: true,
-			} as Record<ExperimentId, boolean>,
+			experiments: {} as Record<ExperimentId, boolean>,
 		}
 
 		const newState: ExtensionState = {
@@ -228,7 +226,6 @@ describe("mergeExtensionState", () => {
 		})
 
 		expect(result.experiments).toEqual({
-			insert_content: true,
 			powerSteering: true,
 		})
 	})

+ 4 - 1
webview-ui/src/i18n/locales/ca/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo vol editar aquest fitxer fora de l'espai de treball:",
 		"wantsToCreate": "Roo vol crear un nou fitxer:",
 		"wantsToSearchReplace": "Roo vol realitzar cerca i substitució en aquest fitxer:",
-		"didSearchReplace": "Roo ha realitzat cerca i substitució en aquest fitxer:"
+		"didSearchReplace": "Roo ha realitzat cerca i substitució en aquest fitxer:",
+		"wantsToInsert": "Roo vol inserir contingut en aquest fitxer:",
+		"wantsToInsertWithLineNumber": "Roo vol inserir contingut a la línia {{lineNumber}} d'aquest fitxer:",
+		"wantsToInsertAtEnd": "Roo vol afegir contingut al final d'aquest fitxer:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo vol veure els fitxers de nivell superior en aquest directori:",

+ 4 - 1
webview-ui/src/i18n/locales/de/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo möchte diese Datei außerhalb des Arbeitsbereichs bearbeiten:",
 		"wantsToCreate": "Roo möchte eine neue Datei erstellen:",
 		"wantsToSearchReplace": "Roo möchte Suchen und Ersetzen in dieser Datei durchführen:",
-		"didSearchReplace": "Roo hat Suchen und Ersetzen in dieser Datei durchgeführt:"
+		"didSearchReplace": "Roo hat Suchen und Ersetzen in dieser Datei durchgeführt:",
+		"wantsToInsert": "Roo möchte Inhalte in diese Datei einfügen:",
+		"wantsToInsertWithLineNumber": "Roo möchte Inhalte in diese Datei in Zeile {{lineNumber}} einfügen:",
+		"wantsToInsertAtEnd": "Roo möchte Inhalte am Ende dieser Datei anhängen:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo möchte die Dateien auf oberster Ebene in diesem Verzeichnis anzeigen:",

+ 4 - 1
webview-ui/src/i18n/locales/en/chat.json

@@ -117,7 +117,10 @@
 		"wantsToEditOutsideWorkspace": "Roo wants to edit this file outside of the workspace:",
 		"wantsToCreate": "Roo wants to create a new file:",
 		"wantsToSearchReplace": "Roo wants to perform search and replace on this file:",
-		"didSearchReplace": "Roo performed search and replace on this file:"
+		"didSearchReplace": "Roo performed search and replace on this file:",
+		"wantsToInsert": "Roo wants to insert content into this file:",
+		"wantsToInsertWithLineNumber": "Roo wants to insert content into this file at line {{lineNumber}}:",
+		"wantsToInsertAtEnd": "Roo wants to append content to the end of this file:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo wants to view the top level files in this directory:",

+ 4 - 1
webview-ui/src/i18n/locales/es/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo quiere editar este archivo fuera del espacio de trabajo:",
 		"wantsToCreate": "Roo quiere crear un nuevo archivo:",
 		"wantsToSearchReplace": "Roo quiere realizar búsqueda y reemplazo en este archivo:",
-		"didSearchReplace": "Roo realizó búsqueda y reemplazo en este archivo:"
+		"didSearchReplace": "Roo realizó búsqueda y reemplazo en este archivo:",
+		"wantsToInsert": "Roo quiere insertar contenido en este archivo:",
+		"wantsToInsertWithLineNumber": "Roo quiere insertar contenido en este archivo en la línea {{lineNumber}}:",
+		"wantsToInsertAtEnd": "Roo quiere añadir contenido al final de este archivo:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo quiere ver los archivos de nivel superior en este directorio:",

+ 4 - 1
webview-ui/src/i18n/locales/fr/chat.json

@@ -119,7 +119,10 @@
 		"wantsToEditOutsideWorkspace": "Roo veut éditer ce fichier en dehors de l'espace de travail :",
 		"wantsToCreate": "Roo veut créer un nouveau fichier :",
 		"wantsToSearchReplace": "Roo veut effectuer une recherche et remplacement sur ce fichier :",
-		"didSearchReplace": "Roo a effectué une recherche et remplacement sur ce fichier :"
+		"didSearchReplace": "Roo a effectué une recherche et remplacement sur ce fichier :",
+		"wantsToInsert": "Roo veut insérer du contenu dans ce fichier :",
+		"wantsToInsertWithLineNumber": "Roo veut insérer du contenu dans ce fichier à la ligne {{lineNumber}} :",
+		"wantsToInsertAtEnd": "Roo veut ajouter du contenu à la fin de ce fichier :"
 	},
 	"instructions": {
 		"wantsToFetch": "Roo veut récupérer des instructions détaillées pour aider à la tâche actuelle"

+ 4 - 1
webview-ui/src/i18n/locales/hi/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo कार्यक्षेत्र के बाहर इस फ़ाइल को संपादित करना चाहता है:",
 		"wantsToCreate": "Roo एक नई फ़ाइल बनाना चाहता है:",
 		"wantsToSearchReplace": "Roo इस फ़ाइल में खोज और प्रतिस्थापन करना चाहता है:",
-		"didSearchReplace": "Roo ने इस फ़ाइल में खोज और प्रतिस्थापन किया:"
+		"didSearchReplace": "Roo ने इस फ़ाइल में खोज और प्रतिस्थापन किया:",
+		"wantsToInsert": "Roo इस फ़ाइल में सामग्री डालना चाहता है:",
+		"wantsToInsertWithLineNumber": "Roo इस फ़ाइल की {{lineNumber}} लाइन पर सामग्री डालना चाहता है:",
+		"wantsToInsertAtEnd": "Roo इस फ़ाइल के अंत में सामग्री जोड़ना चाहता है:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo इस निर्देशिका में शीर्ष स्तर की फ़ाइलें देखना चाहता है:",

+ 4 - 1
webview-ui/src/i18n/locales/it/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo vuole modificare questo file al di fuori dell'area di lavoro:",
 		"wantsToCreate": "Roo vuole creare un nuovo file:",
 		"wantsToSearchReplace": "Roo vuole eseguire ricerca e sostituzione in questo file:",
-		"didSearchReplace": "Roo ha eseguito ricerca e sostituzione in questo file:"
+		"didSearchReplace": "Roo ha eseguito ricerca e sostituzione in questo file:",
+		"wantsToInsert": "Roo vuole inserire contenuto in questo file:",
+		"wantsToInsertWithLineNumber": "Roo vuole inserire contenuto in questo file alla riga {{lineNumber}}:",
+		"wantsToInsertAtEnd": "Roo vuole aggiungere contenuto alla fine di questo file:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo vuole visualizzare i file di primo livello in questa directory:",

+ 4 - 1
webview-ui/src/i18n/locales/ja/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Rooはワークスペース外のこのファイルを編集したい:",
 		"wantsToCreate": "Rooは新しいファイルを作成したい:",
 		"wantsToSearchReplace": "Rooはこのファイルで検索と置換を実行したい:",
-		"didSearchReplace": "Rooはこのファイルで検索と置換を実行しました:"
+		"didSearchReplace": "Rooはこのファイルで検索と置換を実行しました:",
+		"wantsToInsert": "Rooはこのファイルにコンテンツを挿入したい:",
+		"wantsToInsertWithLineNumber": "Rooはこのファイルの{{lineNumber}}行目にコンテンツを挿入したい:",
+		"wantsToInsertAtEnd": "Rooはこのファイルの末尾にコンテンツを追加したい:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Rooはこのディレクトリのトップレベルファイルを表示したい:",

+ 4 - 1
webview-ui/src/i18n/locales/ko/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo가 워크스페이스 외부의 이 파일을 편집하고 싶어합니다:",
 		"wantsToCreate": "Roo가 새 파일을 만들고 싶어합니다:",
 		"wantsToSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행하고 싶어합니다:",
-		"didSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행했습니다:"
+		"didSearchReplace": "Roo가 이 파일에서 검색 및 바꾸기를 수행했습니다:",
+		"wantsToInsert": "Roo가 이 파일에 내용을 삽입하고 싶어합니다:",
+		"wantsToInsertWithLineNumber": "Roo가 이 파일의 {{lineNumber}}번 줄에 내용을 삽입하고 싶어합니다:",
+		"wantsToInsertAtEnd": "Roo가 이 파일의 끝에 내용을 추가하고 싶어합니다:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo가 이 디렉토리의 최상위 파일을 보고 싶어합니다:",

+ 4 - 1
webview-ui/src/i18n/locales/pl/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo chce edytować ten plik poza obszarem roboczym:",
 		"wantsToCreate": "Roo chce utworzyć nowy plik:",
 		"wantsToSearchReplace": "Roo chce wykonać wyszukiwanie i zamianę w tym pliku:",
-		"didSearchReplace": "Roo wykonał wyszukiwanie i zamianę w tym pliku:"
+		"didSearchReplace": "Roo wykonał wyszukiwanie i zamianę w tym pliku:",
+		"wantsToInsert": "Roo chce wstawić zawartość do tego pliku:",
+		"wantsToInsertWithLineNumber": "Roo chce wstawić zawartość do tego pliku w linii {{lineNumber}}:",
+		"wantsToInsertAtEnd": "Roo chce dodać zawartość na końcu tego pliku:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo chce zobaczyć pliki najwyższego poziomu w tym katalogu:",

+ 4 - 1
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo quer editar este arquivo fora do espaço de trabalho:",
 		"wantsToCreate": "Roo quer criar um novo arquivo:",
 		"wantsToSearchReplace": "Roo quer realizar busca e substituição neste arquivo:",
-		"didSearchReplace": "Roo realizou busca e substituição neste arquivo:"
+		"didSearchReplace": "Roo realizou busca e substituição neste arquivo:",
+		"wantsToInsert": "Roo quer inserir conteúdo neste arquivo:",
+		"wantsToInsertWithLineNumber": "Roo quer inserir conteúdo neste arquivo na linha {{lineNumber}}:",
+		"wantsToInsertAtEnd": "Roo quer adicionar conteúdo ao final deste arquivo:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo quer visualizar os arquivos de nível superior neste diretório:",

+ 4 - 1
webview-ui/src/i18n/locales/tr/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo çalışma alanı dışındaki bu dosyayı düzenlemek istiyor:",
 		"wantsToCreate": "Roo yeni bir dosya oluşturmak istiyor:",
 		"wantsToSearchReplace": "Roo bu dosyada arama ve değiştirme yapmak istiyor:",
-		"didSearchReplace": "Roo bu dosyada arama ve değiştirme yaptı:"
+		"didSearchReplace": "Roo bu dosyada arama ve değiştirme yaptı:",
+		"wantsToInsert": "Roo bu dosyaya içerik eklemek istiyor:",
+		"wantsToInsertWithLineNumber": "Roo bu dosyanın {{lineNumber}}. satırına içerik eklemek istiyor:",
+		"wantsToInsertAtEnd": "Roo bu dosyanın sonuna içerik eklemek istiyor:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo bu dizindeki üst düzey dosyaları görüntülemek istiyor:",

+ 4 - 1
webview-ui/src/i18n/locales/vi/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo muốn chỉnh sửa tệp này bên ngoài không gian làm việc:",
 		"wantsToCreate": "Roo muốn tạo một tệp mới:",
 		"wantsToSearchReplace": "Roo muốn thực hiện tìm kiếm và thay thế trong tệp này:",
-		"didSearchReplace": "Roo đã thực hiện tìm kiếm và thay thế trong tệp này:"
+		"didSearchReplace": "Roo đã thực hiện tìm kiếm và thay thế trong tệp này:",
+		"wantsToInsert": "Roo muốn chèn nội dung vào tệp này:",
+		"wantsToInsertWithLineNumber": "Roo muốn chèn nội dung vào dòng {{lineNumber}} của tệp này:",
+		"wantsToInsertAtEnd": "Roo muốn thêm nội dung vào cuối tệp này:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo muốn xem các tệp cấp cao nhất trong thư mục này:",

+ 4 - 1
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "需要编辑外部文件:",
 		"wantsToCreate": "需要新建文件:",
 		"wantsToSearchReplace": "需要执行搜索和替换:",
-		"didSearchReplace": "已完成搜索和替换:"
+		"didSearchReplace": "已完成搜索和替换:",
+		"wantsToInsert": "需要在此文件中插入内容:",
+		"wantsToInsertWithLineNumber": "需要在第 {{lineNumber}} 行插入内容:",
+		"wantsToInsertAtEnd": "需要在文件末尾添加内容:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "需要查看目录文件列表:",

+ 4 - 1
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -122,7 +122,10 @@
 		"wantsToEditOutsideWorkspace": "Roo 想要編輯此工作區外的檔案:",
 		"wantsToCreate": "Roo 想要建立新檔案:",
 		"wantsToSearchReplace": "Roo 想要在此檔案執行搜尋和取代:",
-		"didSearchReplace": "Roo 已在此檔案執行搜尋和取代:"
+		"didSearchReplace": "Roo 已在此檔案執行搜尋和取代:",
+		"wantsToInsert": "Roo 想要在此檔案中插入內容:",
+		"wantsToInsertWithLineNumber": "Roo 想要在此檔案第 {{lineNumber}} 行插入內容:",
+		"wantsToInsertAtEnd": "Roo 想要在此檔案末尾新增內容:"
 	},
 	"directoryOperations": {
 		"wantsToViewTopLevel": "Roo 想要檢視此目錄中最上層的檔案:",

Некоторые файлы не были показаны из-за большого количества измененных файлов