diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a30550dce16..63b183507b0 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -107,6 +107,7 @@ export const globalSettingsSchema = z.object({ rateLimitSeconds: z.number().optional(), diffEnabled: z.boolean().optional(), + fileBasedEditing: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), experiments: experimentsSchema.optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 53b8ef5b87d..ad56a322f91 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -54,6 +54,8 @@ import { RepoPerTaskCheckpointService } from "../../services/checkpoints" // integrations import { DiffViewProvider } from "../../integrations/editor/DiffViewProvider" +import { FileWriter } from "../../integrations/editor/FileWriter" +import { IEditingProvider } from "../../integrations/editor/IEditingProvider" import { findToolName, formatContentBlockToMarkdown } from "../../integrations/misc/export-markdown" import { RooTerminalProcess } from "../../integrations/terminal/types" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" @@ -172,7 +174,7 @@ export class Task extends EventEmitter { browserSession: BrowserSession // Editing - diffViewProvider: DiffViewProvider + editingProvider: IEditingProvider diffStrategy?: DiffStrategy diffEnabled: boolean = false fuzzyMatchThreshold: number @@ -260,7 +262,28 @@ export class Task extends EventEmitter { this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT this.providerRef = new WeakRef(provider) this.globalStoragePath = provider.context.globalStorageUri.fsPath - this.diffViewProvider = new DiffViewProvider(this.cwd) + + // Default to DiffViewProvider initially + this.editingProvider = new DiffViewProvider(this.cwd) + + // Initialize editing provider based on settings + if (provider.getState) { + provider + .getState() + .then((state) => { + const fileBasedEditing = state?.fileBasedEditing ?? false + if (fileBasedEditing) { + this.editingProvider = new FileWriter(this.cwd) + } else { + this.editingProvider = new DiffViewProvider(this.cwd) + } + }) + .catch((error) => { + console.error("Failed to get provider state for editing provider initialization:", error) + // Keep the default DiffViewProvider + }) + } + this.enableCheckpoints = enableCheckpoints this.rootTask = rootTask @@ -1066,8 +1089,8 @@ export class Task extends EventEmitter { try { // If we're not streaming then `abortStream` won't be called - if (this.isStreaming && this.diffViewProvider.isEditing) { - this.diffViewProvider.revertChanges().catch(console.error) + if (this.isStreaming && this.editingProvider.isEditing) { + this.editingProvider.revertChanges().catch(console.error) } } catch (error) { console.error("Error reverting diff changes:", error) @@ -1296,8 +1319,8 @@ export class Task extends EventEmitter { } const abortStream = async (cancelReason: ClineApiReqCancelReason, streamingFailedMessage?: string) => { - if (this.diffViewProvider.isEditing) { - await this.diffViewProvider.revertChanges() // closes diff view + if (this.editingProvider.isEditing) { + await this.editingProvider.revertChanges() // closes diff view } // if last message is a partial we need to update and save it @@ -1349,7 +1372,7 @@ export class Task extends EventEmitter { this.presentAssistantMessageLocked = false this.presentAssistantMessageHasPendingUpdates = false - await this.diffViewProvider.reset() + await this.editingProvider.reset() // Yields only if the first chunk is successful, otherwise will // allow the user to retry the request (most likely due to rate diff --git a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts index e763125d4a4..16cc3b08e58 100644 --- a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts +++ b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts @@ -34,7 +34,7 @@ describe("applyDiffTool experiment routing", () => { applyDiff: vi.fn(), getProgressStatus: vi.fn(), }, - diffViewProvider: { + editingProvider: { reset: vi.fn(), }, api: { diff --git a/src/core/tools/__tests__/insertContentTool.spec.ts b/src/core/tools/__tests__/insertContentTool.spec.ts index e23d7aaa33c..577a01930e7 100644 --- a/src/core/tools/__tests__/insertContentTool.spec.ts +++ b/src/core/tools/__tests__/insertContentTool.spec.ts @@ -82,7 +82,7 @@ describe("insertContentTool", () => { rooIgnoreController: { validateAccess: vi.fn().mockReturnValue(true), }, - diffViewProvider: { + editingProvider: { editType: undefined, isEditing: false, originalContent: "", @@ -179,9 +179,9 @@ describe("insertContentTool", () => { const calledPath = mockedFileExistsAtPath.mock.calls[0][0] expect(toPosix(calledPath)).toContain(testFilePath) expect(mockedFsReadFile).not.toHaveBeenCalled() // Should not read if file doesn't exist - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(contentToInsert, true) - expect(mockCline.diffViewProvider.editType).toBe("create") - expect(mockCline.diffViewProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith(contentToInsert, true) + expect(mockCline.editingProvider.editType).toBe("create") + expect(mockCline.editingProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) }) it("creates a new file and inserts content at line 1 (beginning)", async () => { @@ -195,9 +195,9 @@ describe("insertContentTool", () => { const calledPath = mockedFileExistsAtPath.mock.calls[0][0] expect(toPosix(calledPath)).toContain(testFilePath) expect(mockedFsReadFile).not.toHaveBeenCalled() - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(contentToInsert, true) - expect(mockCline.diffViewProvider.editType).toBe("create") - expect(mockCline.diffViewProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith(contentToInsert, true) + expect(mockCline.editingProvider.editType).toBe("create") + expect(mockCline.editingProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) }) it("creates an empty new file if content is empty string", async () => { @@ -207,9 +207,9 @@ describe("insertContentTool", () => { const calledPath = mockedFileExistsAtPath.mock.calls[0][0] expect(toPosix(calledPath)).toContain(testFilePath) expect(mockedFsReadFile).not.toHaveBeenCalled() - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true) - expect(mockCline.diffViewProvider.editType).toBe("create") - expect(mockCline.diffViewProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith("", true) + expect(mockCline.editingProvider.editType).toBe("create") + expect(mockCline.editingProvider.pushToolWriteResult).toHaveBeenCalledWith(mockCline, mockCline.cwd, true) }) it("returns an error when inserting content at an arbitrary line number into a new file", async () => { @@ -226,8 +226,8 @@ describe("insertContentTool", () => { expect(mockCline.consecutiveMistakeCount).toBe(1) expect(mockCline.recordToolError).toHaveBeenCalledWith("insert_content") expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("non-existent file")) - expect(mockCline.diffViewProvider.update).not.toHaveBeenCalled() - expect(mockCline.diffViewProvider.pushToolWriteResult).not.toHaveBeenCalled() + expect(mockCline.editingProvider.update).not.toHaveBeenCalled() + expect(mockCline.editingProvider.pushToolWriteResult).not.toHaveBeenCalled() }) }) }) diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 1b8582c9cc4..bfa90114c50 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -143,7 +143,7 @@ describe("writeToFileTool", () => { mockCline.rooIgnoreController = { validateAccess: vi.fn().mockReturnValue(true), } - mockCline.diffViewProvider = { + mockCline.editingProvider = { editType: undefined, isEditing: false, originalContent: "", @@ -246,7 +246,7 @@ describe("writeToFileTool", () => { await executeWriteFileTool({}, { accessAllowed: true }) expect(mockCline.rooIgnoreController.validateAccess).toHaveBeenCalledWith(testFilePath) - expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath) + expect(mockCline.editingProvider.open).toHaveBeenCalledWith(testFilePath) }) }) @@ -255,18 +255,18 @@ describe("writeToFileTool", () => { await executeWriteFileTool({}, { fileExists: true }) expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath) - expect(mockCline.diffViewProvider.editType).toBe("modify") + expect(mockCline.editingProvider.editType).toBe("modify") }) it.skipIf(process.platform === "win32")("detects new file and sets editType to create", async () => { await executeWriteFileTool({}, { fileExists: false }) expect(mockedFileExistsAtPath).toHaveBeenCalledWith(absoluteFilePath) - expect(mockCline.diffViewProvider.editType).toBe("create") + expect(mockCline.editingProvider.editType).toBe("create") }) it("uses cached editType without filesystem check", async () => { - mockCline.diffViewProvider.editType = "modify" + mockCline.editingProvider.editType = "modify" await executeWriteFileTool({}) @@ -278,13 +278,13 @@ describe("writeToFileTool", () => { it("removes markdown code block markers from content", async () => { await executeWriteFileTool({ content: testContentWithMarkdown }) - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("Line 1\nLine 2", true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith("Line 1\nLine 2", true) }) it("passes through empty content unchanged", async () => { await executeWriteFileTool({ content: "" }) - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith("", true) }) it("unescapes HTML entities for non-Claude models", async () => { @@ -312,7 +312,7 @@ describe("writeToFileTool", () => { expect(mockedEveryLineHasLineNumbers).toHaveBeenCalledWith(contentWithLineNumbers) expect(mockedStripLineNumbers).toHaveBeenCalledWith(contentWithLineNumbers) - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("line one\nline two", true) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith("line one\nline two", true) }) }) @@ -321,10 +321,10 @@ describe("writeToFileTool", () => { await executeWriteFileTool({}, { fileExists: false }) expect(mockCline.consecutiveMistakeCount).toBe(0) - expect(mockCline.diffViewProvider.open).toHaveBeenCalledWith(testFilePath) - expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith(testContent, true) + expect(mockCline.editingProvider.open).toHaveBeenCalledWith(testFilePath) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith(testContent, true) expect(mockAskApproval).toHaveBeenCalled() - expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled() + expect(mockCline.editingProvider.saveChanges).toHaveBeenCalled() expect(mockCline.fileContextTracker.trackFileContext).toHaveBeenCalledWith(testFilePath, "roo_edited") expect(mockCline.didEditFile).toBe(true) }) @@ -349,21 +349,21 @@ describe("writeToFileTool", () => { it("returns early when path is missing in partial block", async () => { await executeWriteFileTool({ path: undefined }, { isPartial: true }) - expect(mockCline.diffViewProvider.open).not.toHaveBeenCalled() + expect(mockCline.editingProvider.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() + expect(mockCline.editingProvider.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) + expect(mockCline.editingProvider.open).toHaveBeenCalledWith(testFilePath) + expect(mockCline.editingProvider.update).toHaveBeenCalledWith(testContent, false) }) }) @@ -373,19 +373,19 @@ describe("writeToFileTool", () => { await executeWriteFileTool({}) - expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled() - expect(mockCline.diffViewProvider.saveChanges).not.toHaveBeenCalled() + expect(mockCline.editingProvider.revertChanges).toHaveBeenCalled() + expect(mockCline.editingProvider.saveChanges).not.toHaveBeenCalled() }) it("reports user edits with diff feedback", async () => { const userEditsValue = "- old line\n+ new line" - mockCline.diffViewProvider.saveChanges.mockResolvedValue({ + mockCline.editingProvider.saveChanges.mockResolvedValue({ newProblemsMessage: " with warnings", userEdits: userEditsValue, finalContent: "modified content", }) // Set the userEdits property on the diffViewProvider mock to simulate user edits - mockCline.diffViewProvider.userEdits = userEditsValue + mockCline.editingProvider.userEdits = userEditsValue await executeWriteFileTool({}, { fileExists: true }) @@ -398,21 +398,21 @@ describe("writeToFileTool", () => { describe("error handling", () => { it("handles general file operation errors", async () => { - mockCline.diffViewProvider.open.mockRejectedValue(new Error("General error")) + mockCline.editingProvider.open.mockRejectedValue(new Error("General error")) await executeWriteFileTool({}) expect(mockHandleError).toHaveBeenCalledWith("writing file", expect.any(Error)) - expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + expect(mockCline.editingProvider.reset).toHaveBeenCalled() }) it("handles partial streaming errors", async () => { - mockCline.diffViewProvider.open.mockRejectedValue(new Error("Open failed")) + mockCline.editingProvider.open.mockRejectedValue(new Error("Open failed")) await executeWriteFileTool({}, { isPartial: true }) expect(mockHandleError).toHaveBeenCalledWith("writing file", expect.any(Error)) - expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + expect(mockCline.editingProvider.reset).toHaveBeenCalled() }) }) }) diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index ad4bb0590f8..46028fa0257 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -143,10 +143,12 @@ export async function applyDiffToolLegacy( cline.consecutiveMistakeCountForApplyDiff.delete(relPath) // Show diff view before asking for approval - cline.diffViewProvider.editType = "modify" - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(diffResult.content, true) - cline.diffViewProvider.scrollToFirstDiff() + cline.editingProvider.editType = "modify" + await cline.editingProvider.open(relPath) + await cline.editingProvider.update(diffResult.content, true) + if (cline.editingProvider.scrollToFirstDiff) { + cline.editingProvider.scrollToFirstDiff() + } // Check if file is write-protected const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false @@ -166,7 +168,7 @@ export async function applyDiffToolLegacy( const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected) if (!didApprove) { - await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view + await cline.editingProvider.revertChanges() // Cline likely handles closing the diff view return } @@ -175,7 +177,7 @@ export async function applyDiffToolLegacy( const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.editingProvider.saveChanges(diagnosticsEnabled, writeDelayMs) // Track file edit operation if (relPath) { @@ -191,7 +193,7 @@ export async function applyDiffToolLegacy( } // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + const message = await cline.editingProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) if (partFailHint) { pushToolResult(partFailHint + message) @@ -199,13 +201,13 @@ export async function applyDiffToolLegacy( pushToolResult(message) } - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } } catch (error) { await handleError("applying diff", error) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } } diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index 2b312244006..a80c8bfe412 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -96,8 +96,8 @@ export async function insertContentTool( cline.consecutiveMistakeCount = 0 - cline.diffViewProvider.editType = fileExists ? "modify" : "create" - cline.diffViewProvider.originalContent = fileContent + cline.editingProvider.editType = fileExists ? "modify" : "create" + cline.editingProvider.originalContent = fileContent const lines = fileExists ? fileContent.split("\n") : [] const updatedContent = insertGroups(lines, [ @@ -108,12 +108,14 @@ export async function insertContentTool( ]).join("\n") // Show changes in diff view - if (!cline.diffViewProvider.isEditing) { + if (!cline.editingProvider.isEditing) { await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {}) // First open with original content - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(fileContent, false) - cline.diffViewProvider.scrollToFirstDiff() + await cline.editingProvider.open(relPath) + await cline.editingProvider.update(fileContent, false) + if (cline.editingProvider.scrollToFirstDiff) { + cline.editingProvider.scrollToFirstDiff() + } await delay(200) } @@ -135,7 +137,7 @@ export async function insertContentTool( approvalContent = updatedContent } - await cline.diffViewProvider.update(updatedContent, true) + await cline.editingProvider.update(updatedContent, true) const completeMessage = JSON.stringify({ ...sharedMessageProps, @@ -150,7 +152,7 @@ export async function insertContentTool( .then((response) => response.response === "yesButtonClicked") if (!didApprove) { - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() pushToolResult("Changes were rejected by the user.") return } @@ -160,7 +162,7 @@ export async function insertContentTool( const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.editingProvider.saveChanges(diagnosticsEnabled, writeDelayMs) // Track file edit operation if (relPath) { @@ -170,13 +172,13 @@ export async function insertContentTool( cline.didEditFile = true // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + const message = await cline.editingProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) pushToolResult(message) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() } catch (error) { handleError("insert content", error) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() } } diff --git a/src/core/tools/multiApplyDiffTool.ts b/src/core/tools/multiApplyDiffTool.ts index b41d409dbb7..f6c5c1a9fef 100644 --- a/src/core/tools/multiApplyDiffTool.ts +++ b/src/core/tools/multiApplyDiffTool.ts @@ -508,10 +508,12 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} cline.consecutiveMistakeCountForApplyDiff.delete(relPath) // Show diff view before asking for approval (only for single file or after batch approval) - cline.diffViewProvider.editType = "modify" - await cline.diffViewProvider.open(relPath) - await cline.diffViewProvider.update(originalContent!, true) - cline.diffViewProvider.scrollToFirstDiff() + cline.editingProvider.editType = "modify" + await cline.editingProvider.open(relPath) + await cline.editingProvider.update(originalContent!, true) + if (cline.editingProvider.scrollToFirstDiff) { + cline.editingProvider.scrollToFirstDiff() + } // For batch operations, we've already gotten approval const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false @@ -548,7 +550,7 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} } if (!didApprove) { - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() results.push(`Changes to ${relPath} were not approved by user`) continue } @@ -558,7 +560,7 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.editingProvider.saveChanges(diagnosticsEnabled, writeDelayMs) // Track file edit operation await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource) @@ -572,7 +574,7 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} } // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + const message = await cline.editingProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) if (partFailHint) { results.push(partFailHint + "\n" + message) @@ -580,7 +582,7 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} results.push(message) } - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error) updateOperationResult(relPath, { @@ -606,7 +608,7 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} return } catch (error) { await handleError("applying diff", error) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } } diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index b6ec3ed39b0..8a57e6f8241 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -188,27 +188,29 @@ export async function searchAndReplaceTool( } // Initialize diff view - cline.diffViewProvider.editType = "modify" - cline.diffViewProvider.originalContent = fileContent + cline.editingProvider.editType = "modify" + cline.editingProvider.originalContent = fileContent // Generate and validate diff const diff = formatResponse.createPrettyPatch(validRelPath, fileContent, newContent) if (!diff) { pushToolResult(`No changes needed for '${relPath}'`) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } // Show changes in diff view - if (!cline.diffViewProvider.isEditing) { + if (!cline.editingProvider.isEditing) { await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {}) - await cline.diffViewProvider.open(validRelPath) - await cline.diffViewProvider.update(fileContent, false) - cline.diffViewProvider.scrollToFirstDiff() + await cline.editingProvider.open(validRelPath) + await cline.editingProvider.update(fileContent, false) + if (cline.editingProvider.scrollToFirstDiff) { + cline.editingProvider.scrollToFirstDiff() + } await delay(200) } - await cline.diffViewProvider.update(newContent, true) + await cline.editingProvider.update(newContent, true) // Request user approval for changes const completeMessage = JSON.stringify({ @@ -221,9 +223,9 @@ export async function searchAndReplaceTool( .then((response) => response.response === "yesButtonClicked") if (!didApprove) { - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() pushToolResult("Changes were rejected by the user.") - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } @@ -232,7 +234,7 @@ export async function searchAndReplaceTool( const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.editingProvider.saveChanges(diagnosticsEnabled, writeDelayMs) // Track file edit operation if (relPath) { @@ -242,7 +244,7 @@ export async function searchAndReplaceTool( cline.didEditFile = true // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult( + const message = await cline.editingProvider.pushToolWriteResult( cline, cline.cwd, false, // Always false for search_and_replace @@ -252,10 +254,10 @@ export async function searchAndReplaceTool( // Record successful tool usage and cleanup cline.recordToolUsage("search_and_replace") - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() } catch (error) { handleError("search and replace", error) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() } } diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index fd9d158f3f7..fdd1ffa90c0 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -37,7 +37,7 @@ export async function writeToFileTool( cline.consecutiveMistakeCount++ cline.recordToolError("write_to_file") pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "path")) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } @@ -45,7 +45,7 @@ export async function writeToFileTool( cline.consecutiveMistakeCount++ cline.recordToolError("write_to_file") pushToolResult(await cline.sayAndCreateMissingParamError("write_to_file", "content")) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } @@ -63,12 +63,12 @@ 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" + if (cline.editingProvider.editType !== undefined) { + fileExists = cline.editingProvider.editType === "modify" } else { const absolutePath = path.resolve(cline.cwd, relPath) fileExists = await fileExistsAtPath(absolutePath) - cline.diffViewProvider.editType = fileExists ? "modify" : "create" + cline.editingProvider.editType = fileExists ? "modify" : "create" } // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini) @@ -104,13 +104,13 @@ export async function writeToFileTool( await cline.ask("tool", partialMessage, block.partial).catch(() => {}) // update editor - if (!cline.diffViewProvider.isEditing) { + if (!cline.editingProvider.isEditing) { // open the editor and prepare to stream content in - await cline.diffViewProvider.open(relPath) + await cline.editingProvider.open(relPath) } // editor is open, stream content in - await cline.diffViewProvider.update( + await cline.editingProvider.update( everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, false, ) @@ -143,7 +143,7 @@ export async function writeToFileTool( formatResponse.lineCountTruncationError(actualLineCount, isNewFile, diffStrategyEnabled), ), ) - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() return } @@ -152,25 +152,27 @@ export async function writeToFileTool( // 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) { + if (!cline.editingProvider.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.editingProvider.open(relPath) } - await cline.diffViewProvider.update( + await cline.editingProvider.update( everyLineHasLineNumbers(newContent) ? stripLineNumbers(newContent) : newContent, true, ) await delay(300) // wait for diff view to update - cline.diffViewProvider.scrollToFirstDiff() + if (cline.editingProvider.scrollToFirstDiff) { + cline.editingProvider.scrollToFirstDiff() + } // Check for code omissions before proceeding - if (detectCodeOmission(cline.diffViewProvider.originalContent || "", newContent, predictedLineCount)) { + if (detectCodeOmission(cline.editingProvider.originalContent || "", newContent, predictedLineCount)) { if (cline.diffStrategy) { - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() pushToolResult( formatResponse.toolError( @@ -202,14 +204,14 @@ export async function writeToFileTool( ...sharedMessageProps, content: fileExists ? undefined : newContent, diff: fileExists - ? formatResponse.createPrettyPatch(relPath, cline.diffViewProvider.originalContent, newContent) + ? formatResponse.createPrettyPatch(relPath, cline.editingProvider.originalContent, newContent) : undefined, } satisfies ClineSayTool) const didApprove = await askApproval("tool", completeMessage, undefined, isWriteProtected) if (!didApprove) { - await cline.diffViewProvider.revertChanges() + await cline.editingProvider.revertChanges() return } @@ -218,7 +220,7 @@ export async function writeToFileTool( const state = await provider?.getState() const diagnosticsEnabled = state?.diagnosticsEnabled ?? true const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS - await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs) + await cline.editingProvider.saveChanges(diagnosticsEnabled, writeDelayMs) // Track file edit operation if (relPath) { @@ -228,17 +230,17 @@ export async function writeToFileTool( cline.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request // Get the formatted response message - const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) + const message = await cline.editingProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) pushToolResult(message) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } } catch (error) { await handleError("writing file", error) - await cline.diffViewProvider.reset() + await cline.editingProvider.reset() return } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6231f081670..a28993f3853 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1440,6 +1440,7 @@ export class ClineProvider alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, diagnosticsEnabled, + fileBasedEditing, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1561,6 +1562,7 @@ export class ClineProvider alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, diagnosticsEnabled: diagnosticsEnabled ?? true, + fileBasedEditing: fileBasedEditing ?? false, } } @@ -1645,6 +1647,7 @@ export class ClineProvider alwaysAllowUpdateTodoList: stateValues.alwaysAllowUpdateTodoList ?? false, followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000, diagnosticsEnabled: stateValues.diagnosticsEnabled ?? true, + fileBasedEditing: stateValues.fileBasedEditing ?? false, allowedMaxRequests: stateValues.allowedMaxRequests, autoCondenseContext: stateValues.autoCondenseContext ?? true, autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 780d40df891..8c68996bdfc 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -932,6 +932,11 @@ export const webviewMessageHandler = async ( await updateGlobalState("diffEnabled", diffEnabled) await provider.postStateToWebview() break + case "fileBasedEditing": + const fileBasedEditing = message.bool ?? false + await updateGlobalState("fileBasedEditing", fileBasedEditing) + await provider.postStateToWebview() + break case "enableCheckpoints": const enableCheckpoints = message.bool ?? true await updateGlobalState("enableCheckpoints", enableCheckpoints) diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index f4133029c99..b0f30a33a85 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -15,12 +15,13 @@ import { Task } from "../../core/task/Task" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" import { DecorationController } from "./DecorationController" +import { IEditingProvider } from "./IEditingProvider" export const DIFF_VIEW_URI_SCHEME = "cline-diff" export const DIFF_VIEW_LABEL_CHANGES = "Original ↔ Roo's Changes" // TODO: https://github.com/cline/cline/pull/3354 -export class DiffViewProvider { +export class DiffViewProvider implements IEditingProvider { // Properties to store the results of saveChanges newProblemsMessage?: string userEdits?: string @@ -181,7 +182,10 @@ export class DiffViewProvider { } } - async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{ + async saveChanges( + diagnosticsEnabled: boolean = true, + writeDelayMs: number = DEFAULT_WRITE_DELAY_MS, + ): Promise<{ newProblemsMessage: string | undefined userEdits: string | undefined finalContent: string | undefined @@ -216,22 +220,22 @@ export class DiffViewProvider { // and can address them accordingly. If problems don't change immediately after // applying a fix, won't be notified, which is generally fine since the // initial fix is usually correct and it may just take time for linters to catch up. - + let newProblemsMessage = "" - + if (diagnosticsEnabled) { // Add configurable delay to allow linters time to process and clean up issues // like unused imports (especially important for Go and other languages) // Ensure delay is non-negative const safeDelayMs = Math.max(0, writeDelayMs) - + try { await delay(safeDelayMs) } catch (error) { // Log error but continue - delay failure shouldn't break the save operation console.warn(`Failed to apply write delay: ${error}`) } - + const postDiagnostics = vscode.languages.getDiagnostics() const newProblems = await diagnosticsToProblemsString( diff --git a/src/integrations/editor/FileWriter.ts b/src/integrations/editor/FileWriter.ts new file mode 100644 index 00000000000..3ca6430fb84 --- /dev/null +++ b/src/integrations/editor/FileWriter.ts @@ -0,0 +1,183 @@ +import * as path from "path" +import * as fs from "fs/promises" +import { XMLBuilder } from "fast-xml-parser" + +import { IEditingProvider } from "./IEditingProvider" +import { Task } from "../../core/task/Task" +import { ClineSayTool } from "../../shared/ExtensionMessage" +import { createDirectoriesForFile } from "../../utils/fs" +import { getReadablePath } from "../../utils/path" +import { formatResponse } from "../../core/prompts/responses" + +/** + * FileWriter implements direct file system writes without visual feedback. + * This provider bypasses the diff view and writes changes directly to disk. + */ +export class FileWriter implements IEditingProvider { + isEditing = false + editType?: "create" | "modify" + originalContent?: string + + private relPath?: string + private newContent?: string + private createdDirs: string[] = [] + + constructor(private cwd: string) {} + + async open(relPath: string): Promise { + this.relPath = relPath + const absolutePath = path.resolve(this.cwd, relPath) + + try { + // Check if file exists + await fs.access(absolutePath) + this.editType = "modify" + this.originalContent = await fs.readFile(absolutePath, "utf-8") + } catch { + // File doesn't exist + this.editType = "create" + this.originalContent = "" + + // Create necessary directories + this.createdDirs = await createDirectoriesForFile(absolutePath) + } + + this.isEditing = true + } + + async update(content: string, isFinal: boolean): Promise { + if (!this.relPath) { + throw new Error("No file path set for FileWriter") + } + + this.newContent = content + + // For file-based editing, we don't do anything until saveChanges is called + // This maintains compatibility with the streaming interface + } + + async saveChanges( + diagnosticsEnabled: boolean = true, + writeDelayMs: number = 0, + ): Promise<{ + newProblemsMessage: string | undefined + userEdits: string | undefined + finalContent: string | undefined + }> { + if (!this.relPath || !this.newContent) { + return { + newProblemsMessage: undefined, + userEdits: undefined, + finalContent: undefined, + } + } + + const absolutePath = path.resolve(this.cwd, this.relPath) + + // Write the file directly + await fs.writeFile(absolutePath, this.newContent, "utf-8") + + // For file-based editing, we don't check diagnostics or track user edits + // since there's no opportunity for the user to modify the content + return { + newProblemsMessage: undefined, + userEdits: undefined, + finalContent: this.newContent, + } + } + + async pushToolWriteResult(task: Task, cwd: string, isNewFile: boolean): Promise { + if (!this.relPath) { + throw new Error("No file path available in FileWriter") + } + + // Create say object for UI feedback (without diff since we're not showing it) + const say: ClineSayTool = { + tool: isNewFile ? "newFileCreated" : "editedExistingFile", + path: getReadablePath(cwd, this.relPath), + } + + // Send the feedback + await task.say("user_feedback_diff", JSON.stringify(say)) + + // Build XML response + const xmlObj = { + file_write_result: { + path: this.relPath, + operation: isNewFile ? "created" : "modified", + notice: { + i: [ + "File has been written directly to disk without visual diff", + "Proceed with the task using these changes as the new baseline.", + ], + }, + }, + } + + const builder = new XMLBuilder({ + format: true, + indentBy: "", + suppressEmptyNode: true, + processEntities: false, + tagValueProcessor: (name, value) => { + if (typeof value === "string") { + return value.replace(/&/g, "&").replace(//g, ">") + } + return value + }, + attributeValueProcessor: (name, value) => { + if (typeof value === "string") { + return value.replace(/&/g, "&").replace(//g, ">") + } + return value + }, + }) + + return builder.build(xmlObj) + } + + async revertChanges(): Promise { + if (!this.relPath) { + return + } + + const absolutePath = path.resolve(this.cwd, this.relPath) + + if (this.editType === "create") { + // Delete the file if it was newly created + try { + await fs.unlink(absolutePath) + } catch { + // File might not exist + } + + // Remove created directories in reverse order + for (let i = this.createdDirs.length - 1; i >= 0; i--) { + try { + await fs.rmdir(this.createdDirs[i]) + } catch { + // Directory might not be empty or already deleted + } + } + } else if (this.editType === "modify" && this.originalContent !== undefined) { + // Restore original content + await fs.writeFile(absolutePath, this.originalContent, "utf-8") + } + + await this.reset() + } + + async reset(): Promise { + this.isEditing = false + this.editType = undefined + this.originalContent = undefined + this.relPath = undefined + this.newContent = undefined + this.createdDirs = [] + } + + // Optional method - not applicable for file-based editing + scrollToFirstDiff(): void { + // No-op for file-based editing + } +} diff --git a/src/integrations/editor/IEditingProvider.ts b/src/integrations/editor/IEditingProvider.ts new file mode 100644 index 00000000000..26154098cb0 --- /dev/null +++ b/src/integrations/editor/IEditingProvider.ts @@ -0,0 +1,77 @@ +import { ClineSayTool } from "../../shared/ExtensionMessage" +import { Task } from "../../core/task/Task" + +/** + * Interface for file editing providers. + * This abstraction allows switching between different editing strategies: + * - DiffViewProvider: Shows visual diff in editor before applying changes + * - FileWriter: Writes directly to file system without visual feedback + */ +export interface IEditingProvider { + /** + * Whether the provider is currently editing a file + */ + isEditing: boolean + + /** + * The type of edit operation (create or modify) + */ + editType?: "create" | "modify" + + /** + * The original content of the file being edited + */ + originalContent?: string + + /** + * Open a file for editing + * @param relPath Relative path to the file + */ + open(relPath: string): Promise + + /** + * Update the file content + * @param content The new content + * @param isFinal Whether this is the final update + */ + update(content: string, isFinal: boolean): Promise + + /** + * Save the changes to the file + * @param diagnosticsEnabled Whether to check for diagnostics after saving + * @param writeDelayMs Delay in milliseconds before writing + * @returns Object containing diagnostic messages, user edits, and final content + */ + saveChanges( + diagnosticsEnabled?: boolean, + writeDelayMs?: number, + ): Promise<{ + newProblemsMessage: string | undefined + userEdits: string | undefined + finalContent: string | undefined + }> + + /** + * Push the result of a write operation to the task + * @param task The current task + * @param cwd Current working directory + * @param isNewFile Whether this is a new file + * @returns Formatted XML response message + */ + pushToolWriteResult(task: Task, cwd: string, isNewFile: boolean): Promise + + /** + * Revert any pending changes + */ + revertChanges(): Promise + + /** + * Reset the provider state + */ + reset(): Promise + + /** + * Scroll to the first difference (only applicable for diff-based providers) + */ + scrollToFirstDiff?(): void +} diff --git a/src/integrations/editor/__tests__/FileWriter.spec.ts b/src/integrations/editor/__tests__/FileWriter.spec.ts new file mode 100644 index 00000000000..db971609b18 --- /dev/null +++ b/src/integrations/editor/__tests__/FileWriter.spec.ts @@ -0,0 +1,278 @@ +import { FileWriter } from "../FileWriter" +import * as fs from "fs/promises" +import * as path from "path" +import { createDirectoriesForFile } from "../../../utils/fs" +import { Task } from "../../../core/task/Task" + +// Mock fs/promises +vi.mock("fs/promises", () => ({ + readFile: vi.fn(), + writeFile: vi.fn(), + access: vi.fn(), + unlink: vi.fn(), + rmdir: vi.fn(), +})) + +// Mock utils +vi.mock("../../../utils/fs", () => ({ + createDirectoriesForFile: vi.fn().mockResolvedValue([]), +})) + +// Mock path +vi.mock("path", () => ({ + resolve: vi.fn((cwd, relPath) => `${cwd}/${relPath}`), + dirname: vi.fn((filePath) => { + const parts = filePath.split("/") + parts.pop() + return parts.join("/") + }), +})) + +// Mock getReadablePath +vi.mock("../../../utils/path", () => ({ + getReadablePath: vi.fn((cwd, relPath) => relPath), +})) + +describe("FileWriter", () => { + let fileWriter: FileWriter + const mockCwd = "/mock/cwd" + + beforeEach(() => { + vi.clearAllMocks() + fileWriter = new FileWriter(mockCwd) + }) + + describe("open method", () => { + it("should set relPath and editType for existing file", async () => { + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue("existing content") + + await fileWriter.open("test.txt") + + expect(fileWriter["relPath"]).toBe("test.txt") + expect(fileWriter["editType"]).toBe("modify") + expect(fileWriter.isEditing).toBe(true) + }) + + it("should set editType to create for new file", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("File not found")) + + await fileWriter.open("newfile.txt") + + expect(fileWriter["relPath"]).toBe("newfile.txt") + expect(fileWriter["editType"]).toBe("create") + expect(fileWriter.isEditing).toBe(true) + }) + + it("should read file content for existing file", async () => { + const mockContent = "existing content" + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue(mockContent) + + await fileWriter.open("test.txt") + + expect(fs.readFile).toHaveBeenCalledWith(`${mockCwd}/test.txt`, "utf-8") + expect(fileWriter["originalContent"]).toBe(mockContent) + }) + + it("should set empty content for new file", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("File not found")) + + await fileWriter.open("newfile.txt") + + expect(fileWriter["originalContent"]).toBe("") + expect(fs.readFile).not.toHaveBeenCalled() + }) + + it("should create directories for new file", async () => { + vi.mocked(fs.access).mockRejectedValue(new Error("File not found")) + vi.mocked(createDirectoriesForFile).mockResolvedValue(["/mock/cwd/new", "/mock/cwd/new/dir"]) + + await fileWriter.open("new/dir/file.txt") + + expect(createDirectoriesForFile).toHaveBeenCalledWith(`${mockCwd}/new/dir/file.txt`) + expect(fileWriter["createdDirs"]).toEqual(["/mock/cwd/new", "/mock/cwd/new/dir"]) + }) + }) + + describe("update method", () => { + beforeEach(async () => { + // Setup file writer with a file + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue("original content") + await fileWriter.open("test.txt") + }) + + it("should update newContent", async () => { + await fileWriter.update("new content", false) + + expect(fileWriter["newContent"]).toBe("new content") + }) + + it("should handle multiple updates", async () => { + await fileWriter.update("first content", false) + await fileWriter.update("second content", false) + await fileWriter.update("final content", true) + + expect(fileWriter["newContent"]).toBe("final content") + }) + }) + + describe("saveChanges method", () => { + beforeEach(async () => { + // Setup file writer with a file + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue("original content") + await fileWriter.open("test.txt") + await fileWriter.update("new content", false) + }) + + it("should write content to file", async () => { + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await fileWriter.saveChanges() + + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.txt`, "new content", "utf-8") + expect(result.newProblemsMessage).toBeUndefined() + expect(result.userEdits).toBeUndefined() + expect(result.finalContent).toBe("new content") + }) + + it("should handle write errors", async () => { + const error = new Error("Write failed") + vi.mocked(fs.writeFile).mockRejectedValue(error) + + await expect(fileWriter.saveChanges()).rejects.toThrow("Write failed") + }) + + it("should handle saveChanges with parameters", async () => { + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + const result = await fileWriter.saveChanges(false, 1000) + + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.txt`, "new content", "utf-8") + expect(result.finalContent).toBe("new content") + }) + + it("should return empty result when no content to save", async () => { + const emptyWriter = new FileWriter(mockCwd) + + const result = await emptyWriter.saveChanges() + + expect(result.newProblemsMessage).toBeUndefined() + expect(result.userEdits).toBeUndefined() + expect(result.finalContent).toBeUndefined() + expect(fs.writeFile).not.toHaveBeenCalled() + }) + }) + + describe("revertChanges method", () => { + beforeEach(async () => { + // Setup file writer with a file + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue("original content") + await fileWriter.open("test.txt") + await fileWriter.update("new content", false) + }) + + it("should revert to original content for existing file", async () => { + vi.mocked(fs.writeFile).mockResolvedValue(undefined) + + await fileWriter.revertChanges() + + expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.txt`, "original content", "utf-8") + }) + + it("should delete file if it was newly created", async () => { + fileWriter["editType"] = "create" + fileWriter["createdDirs"] = ["/mock/cwd/new", "/mock/cwd/new/dir"] + vi.mocked(fs.unlink).mockResolvedValue(undefined) + vi.mocked(fs.rmdir).mockResolvedValue(undefined) + + await fileWriter.revertChanges() + + expect(fs.unlink).toHaveBeenCalledWith(`${mockCwd}/test.txt`) + expect(fs.rmdir).toHaveBeenCalledWith("/mock/cwd/new/dir") + expect(fs.rmdir).toHaveBeenCalledWith("/mock/cwd/new") + expect(fs.writeFile).not.toHaveBeenCalled() + }) + + it("should handle revert errors", async () => { + const error = new Error("Revert failed") + vi.mocked(fs.writeFile).mockRejectedValue(error) + + await expect(fileWriter.revertChanges()).rejects.toThrow("Revert failed") + }) + }) + + describe("pushToolWriteResult method", () => { + let mockTask: Task + + beforeEach(() => { + mockTask = { + say: vi.fn().mockResolvedValue(undefined), + } as any + }) + + it("should send user feedback and return XML for new file", async () => { + fileWriter["relPath"] = "test.txt" + fileWriter["editType"] = "create" + + const result = await fileWriter.pushToolWriteResult(mockTask, mockCwd, true) + + expect(mockTask.say).toHaveBeenCalledWith("user_feedback_diff", expect.stringContaining("newFileCreated")) + expect(result).toContain("") + expect(result).toContain("test.txt") + expect(result).toContain("created") + }) + + it("should send user feedback and return XML for modified file", async () => { + fileWriter["relPath"] = "test.txt" + fileWriter["editType"] = "modify" + + const result = await fileWriter.pushToolWriteResult(mockTask, mockCwd, false) + + expect(mockTask.say).toHaveBeenCalledWith( + "user_feedback_diff", + expect.stringContaining("editedExistingFile"), + ) + expect(result).toContain("") + expect(result).toContain("test.txt") + expect(result).toContain("modified") + }) + + it("should throw error when no relPath is set", async () => { + await expect(fileWriter.pushToolWriteResult(mockTask, mockCwd, true)).rejects.toThrow( + "No file path available in FileWriter", + ) + }) + }) + + describe("reset method", () => { + it("should reset all state", async () => { + // Setup some state + vi.mocked(fs.access).mockResolvedValue(undefined) + vi.mocked(fs.readFile).mockResolvedValue("original content") + await fileWriter.open("test.txt") + await fileWriter.update("new content", false) + + // Reset + await fileWriter.reset() + + // Verify all state is cleared + expect(fileWriter.isEditing).toBe(false) + expect(fileWriter["relPath"]).toBeUndefined() + expect(fileWriter["editType"]).toBeUndefined() + expect(fileWriter["originalContent"]).toBeUndefined() + expect(fileWriter["newContent"]).toBeUndefined() + expect(fileWriter["createdDirs"]).toEqual([]) + }) + }) + + describe("scrollToFirstDiff method", () => { + it("should be a no-op for file-based editing", () => { + // This method should do nothing for FileWriter + expect(() => fileWriter.scrollToFirstDiff()).not.toThrow() + }) + }) +}) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4f2aa2da159..b21d837325f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -217,6 +217,7 @@ export type ExtensionState = Pick< | "terminalCompressProgressBar" | "diagnosticsEnabled" | "diffEnabled" + | "fileBasedEditing" | "fuzzyMatchThreshold" // | "experiments" // Optional in GlobalSettings, required here. | "language" @@ -247,6 +248,7 @@ export type ExtensionState = Pick< writeDelayMs: number requestDelaySeconds: number + fileBasedEditing?: boolean enableCheckpoints: boolean maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500) maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1f56829f7b3..ae7b928a6eb 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -93,6 +93,7 @@ export interface WebviewMessage { | "ttsSpeed" | "soundVolume" | "diffEnabled" + | "fileBasedEditing" | "enableCheckpoints" | "browserViewportSize" | "screenshotQuality" diff --git a/webview-ui/src/components/settings/FileEditingOptions.tsx b/webview-ui/src/components/settings/FileEditingOptions.tsx new file mode 100644 index 00000000000..a0085dd8664 --- /dev/null +++ b/webview-ui/src/components/settings/FileEditingOptions.tsx @@ -0,0 +1,90 @@ +import React from "react" +import { FileText } from "lucide-react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { Input } from "@/components/ui" + +import { SetCachedStateField } from "./types" +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +interface FileEditingOptionsProps { + diffEnabled: boolean + fileBasedEditing: boolean + writeDelayMs: number + setCachedStateField: SetCachedStateField +} + +export const FileEditingOptions: React.FC = ({ + diffEnabled, + fileBasedEditing, + writeDelayMs, + setCachedStateField, +}) => { + const { t } = useAppTranslation() + + return ( +
+ +
+ +
{t("settings:sections.fileEditing")}
+
+
+ +
+
+
+ setCachedStateField("fileBasedEditing", e.target.checked)} + data-testid="file-based-editing-checkbox"> + {t("settings:fileEditing.fileBasedEditingLabel")} + +
+ {t("settings:fileEditing.fileBasedEditingDescription")} +
+
+ +
+ setCachedStateField("diffEnabled", e.target.checked)} + disabled={fileBasedEditing} + data-testid="diff-enabled-checkbox"> + {t("settings:fileEditing.diffEnabledLabel")} + +
+ {t("settings:fileEditing.diffEnabledDescription")} +
+
+ +
+ +
+ { + const value = parseInt(e.target.value, 10) + if (!isNaN(value) && value >= 0) { + setCachedStateField("writeDelayMs", value) + } + }} + className="w-24" + min="0" + step="100" + data-testid="write-delay-input" + /> + ms +
+
+ {t("settings:fileEditing.writeDelayDescription")} +
+
+
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 517c1c159d8..9bc0328634f 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -23,6 +23,7 @@ import { Info, MessageSquare, LucideIcon, + FileText, } from "lucide-react" import type { ProviderSettings, ExperimentId } from "@roo-code/types" @@ -65,6 +66,7 @@ import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" +import { FileEditingOptions } from "./FileEditingOptions" import { cn } from "@/lib/utils" export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden" @@ -81,6 +83,7 @@ export interface SettingsViewRef { const sectionNames = [ "providers", "autoApprove", + "fileEditing", "browser", "checkpoints", "notifications", @@ -177,6 +180,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + fileBasedEditing, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -294,6 +298,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "ttsSpeed", value: ttsSpeed }) vscode.postMessage({ type: "soundVolume", value: soundVolume }) vscode.postMessage({ type: "diffEnabled", bool: diffEnabled }) + vscode.postMessage({ type: "fileBasedEditing", bool: fileBasedEditing }) vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints }) vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize }) vscode.postMessage({ type: "remoteBrowserHost", text: remoteBrowserHost }) @@ -404,6 +409,7 @@ const SettingsView = forwardRef(({ onDone, t () => [ { id: "providers", icon: Webhook }, { id: "autoApprove", icon: CheckCheck }, + { id: "fileEditing", icon: FileText }, { id: "browser", icon: SquareMousePointer }, { id: "checkpoints", icon: GitBranch }, { id: "notifications", icon: Bell }, @@ -623,6 +629,16 @@ const SettingsView = forwardRef(({ onDone, t /> )} + {/* File Editing Section */} + {activeTab === "fileEditing" && ( + + )} + {/* Browser Section */} {activeTab === "browser" && ( void + fileBasedEditing?: boolean + setFileBasedEditing: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -171,6 +173,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode ttsEnabled: false, ttsSpeed: 1.0, diffEnabled: false, + fileBasedEditing: false, enableCheckpoints: true, fuzzyMatchThreshold: 1.0, language: "en", // Default language code @@ -405,6 +408,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })), setTtsSpeed: (value) => setState((prevState) => ({ ...prevState, ttsSpeed: value })), setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })), + setFileBasedEditing: (value) => setState((prevState) => ({ ...prevState, fileBasedEditing: value })), setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })), setBrowserViewportSize: (value: string) => setState((prevState) => ({ ...prevState, browserViewportSize: value })), diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index eaa83b1b0d8..de15de1aa3c 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -31,7 +31,8 @@ "prompts": "Indicacions", "experimental": "Experimental", "language": "Idioma", - "about": "Sobre Roo Code" + "about": "Sobre Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configura les indicacions de suport utilitzades per a accions ràpides com millorar indicacions, explicar codi i solucionar problemes. Aquestes indicacions ajuden Roo a proporcionar millor assistència per a tasques comunes de desenvolupament." @@ -613,6 +614,15 @@ "description": "Quan està activat, Roo pot editar múltiples fitxers en una sola petició. Quan està desactivat, Roo ha d'editar fitxers d'un en un. Desactivar això pot ajudar quan es treballa amb models menys capaços o quan vols més control sobre les modificacions de fitxers." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Desactivar la memòria cau de prompts", "description": "Quan està marcat, Roo no utilitzarà la memòria cau de prompts per a aquest model." diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7f7c2c22b72..585ea6f8eda 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -31,7 +31,8 @@ "prompts": "Eingabeaufforderungen", "experimental": "Experimentell", "language": "Sprache", - "about": "Über Roo Code" + "about": "Über Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Konfiguriere Support-Prompts, die für schnelle Aktionen wie das Verbessern von Prompts, das Erklären von Code und das Beheben von Problemen verwendet werden. Diese Prompts helfen Roo dabei, bessere Unterstützung für häufige Entwicklungsaufgaben zu bieten." @@ -613,6 +614,15 @@ "description": "Wenn aktiviert, kann Roo mehrere Dateien in einer einzigen Anfrage bearbeiten. Wenn deaktiviert, muss Roo Dateien einzeln bearbeiten. Das Deaktivieren kann hilfreich sein, wenn mit weniger fähigen Modellen gearbeitet wird oder wenn du mehr Kontrolle über Dateiänderungen haben möchtest." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Prompt-Caching deaktivieren", "description": "Wenn aktiviert, wird Roo für dieses Modell kein Prompt-Caching verwenden." diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7e3c2e3fccb..a8d811e93ae 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompts", "experimental": "Experimental", "language": "Language", - "about": "About Roo Code" + "about": "About Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configure support prompts that are used for quick actions like enhancing prompts, explaining code, and fixing issues. These prompts help Roo provide better assistance for common development tasks." @@ -579,6 +580,15 @@ } } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "experimental": { "DIFF_STRATEGY_UNIFIED": { "name": "Use experimental unified diff strategy", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index f00c2c9b427..962bc8cb2cb 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -31,7 +31,8 @@ "prompts": "Indicaciones", "experimental": "Experimental", "language": "Idioma", - "about": "Acerca de Roo Code" + "about": "Acerca de Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configura indicaciones de soporte que se utilizan para acciones rápidas como mejorar indicaciones, explicar código y solucionar problemas. Estas indicaciones ayudan a Roo a brindar mejor asistencia para tareas comunes de desarrollo." @@ -613,6 +614,15 @@ "description": "Cuando está habilitado, Roo puede editar múltiples archivos en una sola solicitud. Cuando está deshabilitado, Roo debe editar archivos de uno en uno. Deshabilitar esto puede ayudar cuando trabajas con modelos menos capaces o cuando quieres más control sobre las modificaciones de archivos." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Desactivar caché de prompts", "description": "Cuando está marcado, Roo no utilizará el caché de prompts para este modelo." diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 02dfac3552c..dd3a7c44e99 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -31,7 +31,8 @@ "prompts": "Invites", "experimental": "Expérimental", "language": "Langue", - "about": "À propos de Roo Code" + "about": "À propos de Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configurez les invites de support utilisées pour les actions rapides comme l'amélioration des invites, l'explication du code et la résolution des problèmes. Ces invites aident Roo à fournir une meilleure assistance pour les tâches de développement courantes." @@ -613,6 +614,15 @@ "description": "Lorsque cette option est activée, Roo peut éditer plusieurs fichiers en une seule requête. Lorsqu'elle est désactivée, Roo doit éditer les fichiers un par un. Désactiver cette option peut aider lorsque tu travailles avec des modèles moins capables ou lorsque tu veux plus de contrôle sur les modifications de fichiers." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Désactiver la mise en cache des prompts", "description": "Lorsque cette option est cochée, Roo n'utilisera pas la mise en cache des prompts pour ce modèle." diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2b7fd03d56c..eb6bad37cc4 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -31,7 +31,8 @@ "prompts": "प्रॉम्प्ट्स", "experimental": "प्रायोगिक", "language": "भाषा", - "about": "परिचय" + "about": "परिचय", + "fileEditing": "File Editing" }, "prompts": { "description": "प्रॉम्प्ट्स को बेहतर बनाना, कोड की व्याख्या करना और समस्याओं को ठीक करना जैसी त्वरित कार्रवाइयों के लिए उपयोग किए जाने वाले सहायक प्रॉम्प्ट्स को कॉन्फ़िगर करें। ये प्रॉम्प्ट्स Roo को सामान्य विकास कार्यों के लिए बेहतर सहायता प्रदान करने में मदद करते हैं।" @@ -613,6 +614,15 @@ "description": "जब सक्षम किया जाता है, तो Roo एक ही अनुरोध में कई फ़ाइलों को संपादित कर सकता है। जब अक्षम किया जाता है, तो Roo को एक समय में एक फ़ाइल संपादित करनी होगी। इसे अक्षम करना तब मदद कर सकता है जब आप कम सक्षम मॉडल के साथ काम कर रहे हों या जब आप फ़ाइल संशोधनों पर अधिक नियंत्रण चाहते हों।" } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "प्रॉम्प्ट कैशिंग अक्षम करें", "description": "जब चेक किया जाता है, तो Roo इस मॉडल के लिए प्रॉम्प्ट कैशिंग का उपयोग नहीं करेगा।" diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index c6ba2728a74..bee29f7b11a 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompt", "experimental": "Eksperimental", "language": "Bahasa", - "about": "Tentang Roo Code" + "about": "Tentang Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Konfigurasi support prompt yang digunakan untuk aksi cepat seperti meningkatkan prompt, menjelaskan kode, dan memperbaiki masalah. Prompt ini membantu Roo memberikan bantuan yang lebih baik untuk tugas pengembangan umum." @@ -642,6 +643,15 @@ "description": "Ketika diaktifkan, Roo dapat mengedit beberapa file dalam satu permintaan. Ketika dinonaktifkan, Roo harus mengedit file satu per satu. Menonaktifkan ini dapat membantu saat bekerja dengan model yang kurang mampu atau ketika kamu ingin kontrol lebih terhadap modifikasi file." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Nonaktifkan prompt caching", "description": "Ketika dicentang, Roo tidak akan menggunakan prompt caching untuk model ini." diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 26c776d60db..d40269148a0 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompt", "experimental": "Sperimentale", "language": "Lingua", - "about": "Informazioni su Roo Code" + "about": "Informazioni su Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configura i prompt di supporto utilizzati per azioni rapide come il miglioramento dei prompt, la spiegazione del codice e la risoluzione dei problemi. Questi prompt aiutano Roo a fornire una migliore assistenza per le attività di sviluppo comuni." @@ -613,6 +614,15 @@ "description": "Quando abilitato, Roo può modificare più file in una singola richiesta. Quando disabilitato, Roo deve modificare i file uno alla volta. Disabilitare questa opzione può aiutare quando lavori con modelli meno capaci o quando vuoi più controllo sulle modifiche dei file." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Disattiva la cache dei prompt", "description": "Quando selezionato, Roo non utilizzerà la cache dei prompt per questo modello." diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 9eb4328c126..e0ed33ee21f 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -31,7 +31,8 @@ "prompts": "プロンプト", "experimental": "実験的", "language": "言語", - "about": "Roo Codeについて" + "about": "Roo Codeについて", + "fileEditing": "File Editing" }, "prompts": { "description": "プロンプトの強化、コードの説明、問題の修正などの迅速なアクションに使用されるサポートプロンプトを設定します。これらのプロンプトは、Rooが一般的な開発タスクでより良いサポートを提供するのに役立ちます。" @@ -613,6 +614,15 @@ "description": "有効にすると、Rooは単一のリクエストで複数のファイルを編集できます。無効にすると、Rooはファイルを一つずつ編集する必要があります。これを無効にすることで、能力の低いモデルで作業する場合や、ファイル変更をより細かく制御したい場合に役立ちます。" } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "プロンプトキャッシュを無効化", "description": "チェックすると、Rooはこのモデルに対してプロンプトキャッシュを使用しません。" diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 18b054bbb8b..20fceab6638 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -31,7 +31,8 @@ "prompts": "프롬프트", "experimental": "실험적", "language": "언어", - "about": "Roo Code 정보" + "about": "Roo Code 정보", + "fileEditing": "File Editing" }, "prompts": { "description": "프롬프트 향상, 코드 설명, 문제 해결과 같은 빠른 작업에 사용되는 지원 프롬프트를 구성합니다. 이러한 프롬프트는 Roo가 일반적인 개발 작업에 대해 더 나은 지원을 제공하는 데 도움이 됩니다." @@ -613,6 +614,15 @@ "description": "활성화하면 Roo가 단일 요청으로 여러 파일을 편집할 수 있습니다. 비활성화하면 Roo는 파일을 하나씩 편집해야 합니다. 이 기능을 비활성화하면 덜 강력한 모델로 작업하거나 파일 수정에 대한 더 많은 제어가 필요할 때 도움이 됩니다." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "프롬프트 캐싱 비활성화", "description": "체크하면 Roo가 이 모델에 대해 프롬프트 캐싱을 사용하지 않습니다." diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 73ecf87d349..1ea333efd35 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompts", "experimental": "Experimenteel", "language": "Taal", - "about": "Over Roo Code" + "about": "Over Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Configureer ondersteuningsprompts die worden gebruikt voor snelle acties zoals het verbeteren van prompts, het uitleggen van code en het oplossen van problemen. Deze prompts helpen Roo om betere ondersteuning te bieden voor veelvoorkomende ontwikkelingstaken." @@ -613,6 +614,15 @@ "description": "Wanneer ingeschakeld, kan Roo meerdere bestanden in één verzoek bewerken. Wanneer uitgeschakeld, moet Roo bestanden één voor één bewerken. Het uitschakelen hiervan kan helpen wanneer je werkt met minder capabele modellen of wanneer je meer controle wilt over bestandswijzigingen." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Prompt caching inschakelen", "description": "Indien ingeschakeld, gebruikt Roo dit model met prompt caching om kosten te verlagen." diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index b0fb8efac30..1cd53cf889d 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -31,7 +31,8 @@ "prompts": "Podpowiedzi", "experimental": "Eksperymentalne", "language": "Język", - "about": "O Roo Code" + "about": "O Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Skonfiguruj podpowiedzi wsparcia używane do szybkich działań, takich jak ulepszanie podpowiedzi, wyjaśnianie kodu i rozwiązywanie problemów. Te podpowiedzi pomagają Roo zapewnić lepsze wsparcie dla typowych zadań programistycznych." @@ -613,6 +614,15 @@ "description": "Gdy włączone, Roo może edytować wiele plików w jednym żądaniu. Gdy wyłączone, Roo musi edytować pliki jeden po drugim. Wyłączenie tego może pomóc podczas pracy z mniej zdolnymi modelami lub gdy chcesz mieć większą kontrolę nad modyfikacjami plików." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Wyłącz buforowanie promptów", "description": "Po zaznaczeniu, Roo nie będzie używać buforowania promptów dla tego modelu." diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 4167ade9749..dc516da3db4 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -31,7 +31,8 @@ "prompts": "Prompts", "experimental": "Experimental", "language": "Idioma", - "about": "Sobre" + "about": "Sobre", + "fileEditing": "File Editing" }, "prompts": { "description": "Configure prompts de suporte usados para ações rápidas como melhorar prompts, explicar código e corrigir problemas. Esses prompts ajudam o Roo a fornecer melhor assistência para tarefas comuns de desenvolvimento." @@ -613,6 +614,15 @@ "description": "Quando habilitado, o Roo pode editar múltiplos arquivos em uma única solicitação. Quando desabilitado, o Roo deve editar arquivos um de cada vez. Desabilitar isso pode ajudar ao trabalhar com modelos menos capazes ou quando você quer mais controle sobre modificações de arquivos." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Desativar cache de prompts", "description": "Quando marcado, o Roo não usará o cache de prompts para este modelo." diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index c07bc1d98a5..31909b935cd 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -31,7 +31,8 @@ "prompts": "Промпты", "experimental": "Экспериментальное", "language": "Язык", - "about": "О Roo Code" + "about": "О Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "Настройте промпты поддержки, используемые для быстрых действий, таких как улучшение промптов, объяснение кода и исправление проблем. Эти промпты помогают Roo обеспечить лучшую поддержку для общих задач разработки." @@ -613,6 +614,15 @@ "description": "Когда включено, Roo может редактировать несколько файлов в одном запросе. Когда отключено, Roo должен редактировать файлы по одному. Отключение этой функции может помочь при работе с менее способными моделями или когда вы хотите больше контроля над изменениями файлов." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Отключить кэширование промптов", "description": "Если отмечено, Roo не будет использовать кэширование промптов для этой модели." diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index ae6fad364d9..c9c92b161f0 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -31,7 +31,8 @@ "prompts": "Promptlar", "experimental": "Deneysel", "language": "Dil", - "about": "Roo Code Hakkında" + "about": "Roo Code Hakkında", + "fileEditing": "File Editing" }, "prompts": { "description": "Prompt geliştirme, kod açıklama ve sorun çözme gibi hızlı eylemler için kullanılan destek promptlarını yapılandırın. Bu promptlar, Roo'nun yaygın geliştirme görevleri için daha iyi destek sağlamasına yardımcı olur." @@ -613,6 +614,15 @@ "description": "Etkinleştirildiğinde, Roo tek bir istekte birden fazla dosyayı düzenleyebilir. Devre dışı bırakıldığında, Roo dosyaları tek tek düzenlemek zorundadır. Bunu devre dışı bırakmak, daha az yetenekli modellerle çalışırken veya dosya değişiklikleri üzerinde daha fazla kontrol istediğinde yardımcı olabilir." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Prompt önbelleğini devre dışı bırak", "description": "İşaretlendiğinde, Roo bu model için prompt önbelleğini kullanmayacaktır." diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 6505d7df0e9..5539d930d41 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -31,7 +31,8 @@ "prompts": "Lời nhắc", "experimental": "Thử nghiệm", "language": "Ngôn ngữ", - "about": "Giới thiệu" + "about": "Giới thiệu", + "fileEditing": "File Editing" }, "prompts": { "description": "Cấu hình các lời nhắc hỗ trợ được sử dụng cho các hành động nhanh như cải thiện lời nhắc, giải thích mã và khắc phục sự cố. Những lời nhắc này giúp Roo cung cấp hỗ trợ tốt hơn cho các tác vụ phát triển phổ biến." @@ -613,6 +614,15 @@ "description": "Khi được bật, Roo có thể chỉnh sửa nhiều tệp trong một yêu cầu duy nhất. Khi bị tắt, Roo phải chỉnh sửa từng tệp một. Tắt tính năng này có thể hữu ích khi làm việc với các mô hình kém khả năng hơn hoặc khi bạn muốn kiểm soát nhiều hơn đối với các thay đổi tệp." } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "Tắt bộ nhớ đệm prompt", "description": "Khi được chọn, Roo sẽ không sử dụng bộ nhớ đệm prompt cho mô hình này." diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index bf3eea0294e..d6de60ad54f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -31,7 +31,8 @@ "prompts": "提示词", "experimental": "实验性", "language": "语言", - "about": "关于 Roo Code" + "about": "关于 Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "配置用于快速操作的支持提示词,如增强提示词、解释代码和修复问题。这些提示词帮助 Roo 为常见开发任务提供更好的支持。" @@ -613,6 +614,15 @@ "description": "启用后 Roo 可在单个请求中编辑多个文件。禁用后 Roo 必须逐个编辑文件。禁用此功能有助于使用能力较弱的模型或需要更精确控制文件修改时。" } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "禁用提示词缓存", "description": "选中后,Roo 将不会为此模型使用提示词缓存。" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index e2a896dce48..88be3cb41a9 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -31,7 +31,8 @@ "prompts": "提示詞", "experimental": "實驗性", "language": "語言", - "about": "關於 Roo Code" + "about": "關於 Roo Code", + "fileEditing": "File Editing" }, "prompts": { "description": "設定用於快速操作的支援提示詞,如增強提示詞、解釋程式碼和修復問題。這些提示詞幫助 Roo 為常見開發工作提供更好的支援。" @@ -613,6 +614,15 @@ "description": "啟用後 Roo 可在單個請求中編輯多個檔案。停用後 Roo 必須逐個編輯檔案。停用此功能有助於使用能力較弱的模型或需要更精確控制檔案修改時。" } }, + "fileEditing": { + "description": "Configure how Roo edits files - either through visual diffs or direct file writes", + "fileBasedEditingLabel": "Enable file-based editing mode", + "fileBasedEditingDescription": "When enabled, Roo will write changes directly to files without showing diffs. This is faster but provides less visibility into changes.", + "diffEnabledLabel": "Show diff view", + "diffEnabledDescription": "When enabled, Roo will show a visual diff of changes before applying them. This is disabled when file-based editing is active.", + "writeDelayLabel": "Write delay", + "writeDelayDescription": "Delay in milliseconds after file writes to allow diagnostics to detect potential problems" + }, "promptCaching": { "label": "停用提示詞快取", "description": "勾選後,Roo 將不會為此模型使用提示詞快取。"