diff --git a/src/core/tools/__tests__/writeToFileTool.test.ts b/src/core/tools/__tests__/writeToFileTool.test.ts new file mode 100644 index 00000000000..f3e84b9f7c3 --- /dev/null +++ b/src/core/tools/__tests__/writeToFileTool.test.ts @@ -0,0 +1,400 @@ +import * as path from "path" + +import { fileExistsAtPath } from "../../../utils/fs" +import { detectCodeOmission } from "../../../integrations/editor/detect-omission" +import { isPathOutsideWorkspace } from "../../../utils/pathUtils" +import { getReadablePath } from "../../../utils/path" +import { unescapeHtmlEntities } from "../../../utils/text-normalization" +import { everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" +import { ToolUse, ToolResponse } from "../../../shared/tools" +import { writeToFileTool } from "../writeToFileTool" + +jest.mock("path", () => { + const originalPath = jest.requireActual("path") + return { + ...originalPath, + resolve: jest.fn().mockImplementation((...args) => args.join("/")), + } +}) + +jest.mock("delay", () => jest.fn()) + +jest.mock("../../../utils/fs", () => ({ + fileExistsAtPath: jest.fn().mockResolvedValue(false), +})) + +jest.mock("../../prompts/responses", () => ({ + formatResponse: { + toolError: jest.fn((msg) => `Error: ${msg}`), + rooIgnoreError: jest.fn((path) => `Access denied: ${path}`), + lineCountTruncationError: jest.fn( + (count, isNew, diffEnabled) => `Line count error: ${count}, new: ${isNew}, diff: ${diffEnabled}`, + ), + createPrettyPatch: jest.fn(() => "mock-diff"), + }, +})) + +jest.mock("../../../integrations/editor/detect-omission", () => ({ + detectCodeOmission: jest.fn().mockReturnValue(false), +})) + +jest.mock("../../../utils/pathUtils", () => ({ + isPathOutsideWorkspace: jest.fn().mockReturnValue(false), +})) + +jest.mock("../../../utils/path", () => ({ + getReadablePath: jest.fn().mockReturnValue("test/path.txt"), +})) + +jest.mock("../../../utils/text-normalization", () => ({ + unescapeHtmlEntities: jest.fn().mockImplementation((content) => content), +})) + +jest.mock("../../../integrations/misc/extract-text", () => ({ + everyLineHasLineNumbers: jest.fn().mockReturnValue(false), + stripLineNumbers: jest.fn().mockImplementation((content) => content), + addLineNumbers: jest.fn().mockImplementation((content: string) => + content + .split("\n") + .map((line: string, i: number) => `${i + 1} | ${line}`) + .join("\n"), + ), +})) + +jest.mock("vscode", () => ({ + window: { + showWarningMessage: jest.fn().mockResolvedValue(undefined), + }, + env: { + openExternal: jest.fn(), + }, + Uri: { + parse: jest.fn(), + }, +})) + +jest.mock("../../ignore/RooIgnoreController", () => ({ + RooIgnoreController: class { + initialize() { + return Promise.resolve() + } + validateAccess() { + return true + } + }, +})) + +describe("writeToFileTool", () => { + // Test data + const testFilePath = "test/file.txt" + const absoluteFilePath = "/test/file.txt" + const testContent = "Line 1\nLine 2\nLine 3" + const testContentWithMarkdown = "```javascript\nLine 1\nLine 2\n```" + + // Mocked functions with correct types + const mockedFileExistsAtPath = fileExistsAtPath as jest.MockedFunction + const mockedDetectCodeOmission = detectCodeOmission as jest.MockedFunction + const mockedIsPathOutsideWorkspace = isPathOutsideWorkspace as jest.MockedFunction + const mockedGetReadablePath = getReadablePath as jest.MockedFunction + const mockedUnescapeHtmlEntities = unescapeHtmlEntities as jest.MockedFunction + const mockedEveryLineHasLineNumbers = everyLineHasLineNumbers as jest.MockedFunction + const mockedStripLineNumbers = stripLineNumbers as jest.MockedFunction + const mockedPathResolve = path.resolve as jest.MockedFunction + + const mockCline: any = {} + let mockAskApproval: jest.Mock + let mockHandleError: jest.Mock + let mockPushToolResult: jest.Mock + let mockRemoveClosingTag: jest.Mock + let toolResult: ToolResponse | undefined + + beforeEach(() => { + jest.clearAllMocks() + + mockedPathResolve.mockReturnValue(absoluteFilePath) + mockedFileExistsAtPath.mockResolvedValue(false) + mockedDetectCodeOmission.mockReturnValue(false) + mockedIsPathOutsideWorkspace.mockReturnValue(false) + mockedGetReadablePath.mockReturnValue("test/path.txt") + mockedUnescapeHtmlEntities.mockImplementation((content) => content) + mockedEveryLineHasLineNumbers.mockReturnValue(false) + mockedStripLineNumbers.mockImplementation((content) => content) + + mockCline.cwd = "/" + mockCline.consecutiveMistakeCount = 0 + mockCline.didEditFile = false + mockCline.diffStrategy = undefined + mockCline.rooIgnoreController = { + validateAccess: jest.fn().mockReturnValue(true), + } + mockCline.diffViewProvider = { + editType: undefined, + isEditing: false, + originalContent: "", + open: jest.fn().mockResolvedValue(undefined), + update: jest.fn().mockResolvedValue(undefined), + reset: jest.fn().mockResolvedValue(undefined), + revertChanges: jest.fn().mockResolvedValue(undefined), + saveChanges: jest.fn().mockResolvedValue({ + newProblemsMessage: "", + userEdits: null, + finalContent: "final content", + }), + scrollToFirstDiff: jest.fn(), + } + mockCline.api = { + getModel: jest.fn().mockReturnValue({ id: "claude-3" }), + } + mockCline.fileContextTracker = { + trackFileContext: jest.fn().mockResolvedValue(undefined), + } + mockCline.say = jest.fn().mockResolvedValue(undefined) + mockCline.ask = jest.fn().mockResolvedValue(undefined) + mockCline.recordToolError = jest.fn() + mockCline.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing param error") + + mockAskApproval = jest.fn().mockResolvedValue(true) + mockHandleError = jest.fn().mockResolvedValue(undefined) + mockRemoveClosingTag = jest.fn((tag, content) => content) + + toolResult = undefined + }) + + /** + * Helper function to execute the write file tool with different parameters + */ + async function executeWriteFileTool( + params: Partial = {}, + options: { + fileExists?: boolean + isPartial?: boolean + accessAllowed?: boolean + } = {}, + ): Promise { + // Configure mocks based on test scenario + const fileExists = options.fileExists ?? false + const isPartial = options.isPartial ?? false + const accessAllowed = options.accessAllowed ?? true + + mockedFileExistsAtPath.mockResolvedValue(fileExists) + mockCline.rooIgnoreController.validateAccess.mockReturnValue(accessAllowed) + + // Create a tool use object + const toolUse: ToolUse = { + type: "tool_use", + name: "write_to_file", + params: { + path: testFilePath, + content: testContent, + line_count: "3", + ...params, + }, + partial: isPartial, + } + + await writeToFileTool( + mockCline, + toolUse, + mockAskApproval, + mockHandleError, + (result: ToolResponse) => { + toolResult = result + }, + mockRemoveClosingTag, + ) + + return toolResult + } + + describe("parameter validation", () => { + it("errors and resets on missing path parameter", async () => { + await executeWriteFileTool({ path: undefined }) + + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file") + expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("write_to_file", "path") + expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + }) + + it("errors and resets on missing content parameter", async () => { + await executeWriteFileTool({ content: undefined }) + + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file") + expect(mockCline.sayAndCreateMissingParamError).toHaveBeenCalledWith("write_to_file", "content") + expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + }) + }) + + describe("access control", () => { + it("validates and allows access when rooIgnoreController permits", async () => { + await executeWriteFileTool({}, { accessAllowed: true }) + + expect(mockCline.rooIgnoreController.validateAccess).toHaveBeenCalledWith(testFilePath) + expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath) + }) + }) + + describe("file existence detection", () => { + it("detects existing file and sets editType to modify", async () => { + await executeWriteFileTool({}, { fileExists: true }) + + expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath) + expect(mockCline.diffViewProvider.editType).toBe("modify") + }) + + it("detects new file and sets editType to create", async () => { + await executeWriteFileTool({}, { fileExists: false }) + + expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath) + expect(mockCline.diffViewProvider.editType).toBe("create") + }) + + it("uses cached editType without filesystem check", async () => { + mockCline.diffViewProvider.editType = "modify" + + await executeWriteFileTool({}) + + expect(mockedFileExistsAtPath).not.toHaveBeenCalled() + }) + }) + + describe("content preprocessing", () => { + it("removes markdown code block markers from content", async () => { + await executeWriteFileTool({ content: testContentWithMarkdown }) + + expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("Line 1\nLine 2", true) + }) + + it("passes through empty content unchanged", async () => { + await executeWriteFileTool({ content: "" }) + + expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true) + }) + + it("unescapes HTML entities for non-Claude models", async () => { + mockCline.api.getModel.mockReturnValue({ id: "gpt-4" }) + + await executeWriteFileTool({ content: "<test>" }) + + expect(mockedUnescapeHtmlEntities).toHaveBeenCalledWith("<test>") + }) + + it("skips HTML unescaping for Claude models", async () => { + mockCline.api.getModel.mockReturnValue({ id: "claude-3" }) + + await executeWriteFileTool({ content: "<test>" }) + + expect(mockedUnescapeHtmlEntities).not.toHaveBeenCalled() + }) + + it("strips line numbers from numbered content", async () => { + const contentWithLineNumbers = "1 | line one\n2 | line two" + mockedEveryLineHasLineNumbers.mockReturnValue(true) + mockedStripLineNumbers.mockReturnValue("line one\nline two") + + await executeWriteFileTool({ content: contentWithLineNumbers }) + + expect(mockedEveryLineHasLineNumbers).toHaveBeenCalledWith(contentWithLineNumbers) + expect(mockedStripLineNumbers).toHaveBeenCalledWith(contentWithLineNumbers) + expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("line one\nline two", true) + }) + }) + + describe("file operations", () => { + it("successfully creates new files with full workflow", async () => { + await executeWriteFileTool({}, { fileExists: false }) + + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath) + expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(testContent, true) + expect(mockAskApproval).toHaveBeenCalled() + expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled() + expect(mockCline.fileContextTracker.trackFileContext).toHaveBeenCalledWith(testFilePath, "roo_edited") + expect(mockCline.didEditFile).toBe(true) + }) + + it("processes files outside workspace boundary", async () => { + mockedIsPathOutsideWorkspace.mockReturnValue(true) + + await executeWriteFileTool({}) + + expect(mockedIsPathOutsideWorkspace).toHaveBeenCalled() + }) + + it("processes files with very large line counts", async () => { + await executeWriteFileTool({ line_count: "999999" }) + + // Should process normally without issues + expect(mockCline.consecutiveMistakeCount).toBe(0) + }) + }) + + describe("partial block handling", () => { + it("returns early when path is missing in partial block", async () => { + await executeWriteFileTool({ path: undefined }, { isPartial: true }) + + expect(mockCline.diffViewProvider.open).not.toHaveBeenCalled() + }) + + it("returns early when content is undefined in partial block", async () => { + await executeWriteFileTool({ content: undefined }, { isPartial: true }) + + expect(mockCline.diffViewProvider.open).not.toHaveBeenCalled() + }) + + it("streams content updates during partial execution", async () => { + await executeWriteFileTool({}, { isPartial: true }) + + expect(mockCline.ask).toHaveBeenCalled() + expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath) + expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(testContent, false) + }) + }) + + describe("user interaction", () => { + it("reverts changes when user rejects approval", async () => { + mockAskApproval.mockResolvedValue(false) + + await executeWriteFileTool({}) + + expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled() + expect(mockCline.diffViewProvider.saveChanges).not.toHaveBeenCalled() + }) + + it("reports user edits with diff feedback", async () => { + mockCline.diffViewProvider.saveChanges.mockResolvedValue({ + newProblemsMessage: " with warnings", + userEdits: "- old line\n+ new line", + finalContent: "modified content", + }) + + await executeWriteFileTool({}, { fileExists: true }) + + expect(mockCline.say).toHaveBeenCalledWith( + "user_feedback_diff", + expect.stringContaining("editedExistingFile"), + ) + }) + }) + + describe("error handling", () => { + it("handles general file operation errors", async () => { + mockCline.diffViewProvider.open.mockRejectedValue(new Error("General error")) + + await executeWriteFileTool({}) + + expect(mockHandleError).toHaveBeenCalledWith("writing file", expect.any(Error)) + expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + }) + + it("handles partial streaming errors", async () => { + mockCline.diffViewProvider.open.mockRejectedValue(new Error("Open failed")) + + await executeWriteFileTool({}, { isPartial: true }) + + expect(mockHandleError).toHaveBeenCalledWith("writing file", expect.any(Error)) + expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index 2c37f95b74e..a61ca118b39 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -26,14 +26,95 @@ export async function writeToFileTool( let newContent: string | undefined = block.params.content let predictedLineCount: number | undefined = parseInt(block.params.line_count ?? "0") - if (!relPath || !newContent) { - // checking for newContent ensure relPath is complete - // wait so we can determine if it's a new file or editing an existing file + // Handle partial blocks first - minimal validation, just streaming + if (block.partial) { + if (!relPath || newContent === undefined) { + // checking for newContent ensure relPath is complete + // wait so we can determine if it's a new file or editing an existing file + 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 partial streaming + 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) + } + + const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)).toPosix() : "" + const isOutsideWorkspace = isPathOutsideWorkspace(fullPath) + + const sharedMessageProps: ClineSayTool = { + tool: fileExists ? "editedExistingFile" : "newFileCreated", + path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)), + content: newContent, + isOutsideWorkspace, + } + + try { + // update gui message + const partialMessage = JSON.stringify(sharedMessageProps) + await cline.ask("tool", partialMessage, block.partial).catch(() => {}) + + // update editor + if (!cline.diffViewProvider.isEditing) { + // open the editor and prepare to stream content in + await cline.diffViewProvider.open(relPath) + } + + // editor is open, stream content in + await cline.diffViewProvider.update( + everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, + false, + ) + + return + } catch (error) { + await handleError("writing file", error) + await cline.diffViewProvider.reset() + return + } + } + + // Handle non-partial blocks - full validation and processing + if (!relPath) { + cline.consecutiveMistakeCount++ + cline.recordToolError("write_to_file") + pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "path")) + await cline.diffViewProvider.reset() return } - const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) + if (newContent === undefined) { + cline.consecutiveMistakeCount++ + cline.recordToolError("write_to_file") + pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "content")) + await cline.diffViewProvider.reset() + return + } + const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath) if (!accessAllowed) { await cline.say("rooignore_error", relPath) pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath))) @@ -42,7 +123,6 @@ export async function writeToFileTool( // Check if file exists using cached map or fs.access let fileExists: boolean - if (cline.diffViewProvider.editType !== undefined) { fileExists = cline.diffViewProvider.editType === "modify" } else { @@ -66,7 +146,7 @@ export async function writeToFileTool( } // Determine if the path is outside the workspace - const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)) : "" + const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)).toPosix() : "" const isOutsideWorkspace = isPathOutsideWorkspace(fullPath) const sharedMessageProps: ClineSayTool = { @@ -77,176 +157,138 @@ export async function writeToFileTool( } try { - if (block.partial) { - // update gui message - const partialMessage = JSON.stringify(sharedMessageProps) - await cline.ask("tool", partialMessage, block.partial).catch(() => {}) + if (predictedLineCount === undefined) { + cline.consecutiveMistakeCount++ + cline.recordToolError("write_to_file") - // update editor - if (!cline.diffViewProvider.isEditing) { - // open the editor and prepare to stream content in - await cline.diffViewProvider.open(relPath) - } + // Calculate the actual number of lines in the content + const actualLineCount = newContent.split("\n").length - // editor is open, stream content in - await cline.diffViewProvider.update( - everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, - false, + // Check if this is a new file or existing file + const isNewFile = !fileExists + + // Check if diffStrategy is enabled + const diffStrategyEnabled = !!cline.diffStrategy + + // Use more specific error message for line_count that provides guidance based on the situation + await cline.say( + "error", + `Roo tried to use write_to_file${ + relPath ? ` for '${relPath.toPosix()}'` : "" + } but the required parameter 'line_count' was missing or truncated after ${actualLineCount} lines of content were written. Retrying...`, ) + pushToolResult( + formatResponse.toolError( + formatResponse.lineCountTruncationError(actualLineCount, isNewFile, diffStrategyEnabled), + ), + ) + await cline.diffViewProvider.revertChanges() return - } else { - if (!relPath) { - cline.consecutiveMistakeCount++ - cline.recordToolError("write_to_file") - pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "path")) - await cline.diffViewProvider.reset() - return - } - - if (!newContent) { - cline.consecutiveMistakeCount++ - cline.recordToolError("write_to_file") - pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "content")) - await cline.diffViewProvider.reset() - return - } + } - if (!predictedLineCount) { - cline.consecutiveMistakeCount++ - cline.recordToolError("write_to_file") + cline.consecutiveMistakeCount = 0 - // Calculate the actual number of lines in the content - const actualLineCount = newContent.split("\n").length + // if isEditingFile false, that means we have the full contents of the file already. + // it's important to note how cline function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So cline part of the logic will always be called. + // in other words, you must always repeat the block.partial logic here + if (!cline.diffViewProvider.isEditing) { + // show gui message before showing edit animation + const partialMessage = JSON.stringify(sharedMessageProps) + await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor + await cline.diffViewProvider.open(relPath) + } - // Check if this is a new file or existing file - const isNewFile = !fileExists + await cline.diffViewProvider.update( + everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, + true, + ) - // Check if diffStrategy is enabled - const diffStrategyEnabled = !!cline.diffStrategy + await delay(300) // wait for diff view to update + cline.diffViewProvider.scrollToFirstDiff() - // Use more specific error message for line_count that provides guidance based on the situation - await cline.say( - "error", - `Roo tried to use write_to_file${ - relPath ? ` for '${relPath.toPosix()}'` : "" - } but the required parameter 'line_count' was missing or truncated after ${actualLineCount} lines of content were written. Retrying...`, - ) + // Check for code omissions before proceeding + if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (cline.diffStrategy) { + await cline.diffViewProvider.revertChanges() pushToolResult( formatResponse.toolError( - formatResponse.lineCountTruncationError(actualLineCount, isNewFile, diffStrategyEnabled), + `Content appears to be truncated (file has ${ + newContent.split("\n").length + } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, ), ) - await cline.diffViewProvider.revertChanges() return - } - - cline.consecutiveMistakeCount = 0 - - // if isEditingFile false, that means we have the full contents of the file already. - // it's important to note how cline function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So cline part of the logic will always be called. - // in other words, you must always repeat the block.partial logic here - if (!cline.diffViewProvider.isEditing) { - // show gui message before showing edit animation - const partialMessage = JSON.stringify(sharedMessageProps) - await cline.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, cline shows the edit row before the content is streamed into the editor - await cline.diffViewProvider.open(relPath) - } - - await cline.diffViewProvider.update( - everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, - true, - ) - - await delay(300) // wait for diff view to update - cline.diffViewProvider.scrollToFirstDiff() - - // Check for code omissions before proceeding - if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { - if (cline.diffStrategy) { - await cline.diffViewProvider.revertChanges() - - pushToolResult( - formatResponse.toolError( - `Content appears to be truncated (file has ${ - newContent.split("\n").length - } lines but was predicted to have ${predictedLineCount} lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`, - ), + } else { + vscode.window + .showWarningMessage( + "Potential code truncation detected. This happens when the AI reaches its max output limit.", + "Follow cline guide to fix the issue", ) - return - } else { - vscode.window - .showWarningMessage( - "Potential code truncation detected. cline happens when the AI reaches its max output limit.", - "Follow cline guide to fix the issue", - ) - .then((selection) => { - if (selection === "Follow cline guide to fix the issue") { - vscode.env.openExternal( - vscode.Uri.parse( - "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", - ), - ) - } - }) - } + .then((selection) => { + if (selection === "Follow cline guide to fix the issue") { + vscode.env.openExternal( + vscode.Uri.parse( + "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments", + ), + ) + } + }) } + } - 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 completeMessage = JSON.stringify({ + ...sharedMessageProps, + content: fileExists ? undefined : newContent, + diff: fileExists + ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent) + : undefined, + } satisfies ClineSayTool) - const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() + const didApprove = await askApproval("tool", completeMessage) - // Track file edit operation - if (relPath) { - await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) - } + if (!didApprove) { + await cline.diffViewProvider.revertChanges() + return + } - cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request + const { newProblemsMessage, userEdits, finalContent } = await cline.diffViewProvider.saveChanges() - if (userEdits) { - await cline.say( - "user_feedback_diff", - JSON.stringify({ - tool: fileExists ? "editedExistingFile" : "newFileCreated", - path: getReadablePath(cline.cwd, relPath), - diff: userEdits, - } satisfies ClineSayTool), - ) + // Track file edit operation + if (relPath) { + await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) + } - 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` + - `\n${addLineNumbers( - finalContent || "", - )}\n\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 saved to ${relPath.toPosix()}.${newProblemsMessage}`) - } + cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request - await cline.diffViewProvider.reset() + if (userEdits) { + await cline.say( + "user_feedback_diff", + JSON.stringify({ + tool: fileExists ? "editedExistingFile" : "newFileCreated", + path: getReadablePath(cline.cwd, relPath), + diff: userEdits, + } satisfies ClineSayTool), + ) - return + 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` + + `\n${addLineNumbers( + finalContent || "", + )}\n\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 saved to ${relPath.toPosix()}.${newProblemsMessage}`) } + + await cline.diffViewProvider.reset() } catch (error) { await handleError("writing file", error) await cline.diffViewProvider.reset()