Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/tools/ApplyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import { BaseTool, ToolCallbacks } from "./BaseTool"
interface ApplyDiffParams {
path: string
diff: string
ref?: import("../../shared/tools").ContentRef
multi_ref?: import("../../shared/tools").ContentRef[]
transform?: import("../../shared/tools").ContentRefParams["transform"]
}

export class ApplyDiffTool extends BaseTool<"apply_diff"> {
Expand Down Expand Up @@ -173,7 +176,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
return
}

// Save directly without showing diff view or opening the file
task.diffViewProvider.editType = "modify"
task.diffViewProvider.originalContent = originalContent
await task.diffViewProvider.saveDirectly(
Expand All @@ -182,6 +184,7 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
// Original behavior with diff view
Expand Down
12 changes: 9 additions & 3 deletions src/core/tools/ApplyPatchTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
return
}

// Save the changes
if (isPreventFocusDisruptionEnabled) {
await task.diffViewProvider.saveDirectly(relPath, newContent, true, diagnosticsEnabled, writeDelayMs)
} else {
Expand Down Expand Up @@ -407,14 +406,14 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
return
}

// Save new content to the new path
if (isPreventFocusDisruptionEnabled) {
await task.diffViewProvider.saveDirectly(
change.movePath,
newContent,
false,
diagnosticsEnabled,
writeDelayMs,
isMovePathWriteProtected,
)
} else {
// Write to new path and delete old file
Expand All @@ -434,7 +433,14 @@ export class ApplyPatchTool extends BaseTool<"apply_patch"> {
} else {
// Save changes to the same file
if (isPreventFocusDisruptionEnabled) {
await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
await task.diffViewProvider.saveDirectly(
relPath,
newContent,
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
await task.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
}
Expand Down
7 changes: 5 additions & 2 deletions src/core/tools/EditFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,15 +434,18 @@ export class EditFileTool extends BaseTool<"edit_file"> {
return
}

// Save the changes
if (isPreventFocusDisruptionEnabled) {
// Direct file write without diff view or opening the file
// Background editing: always pass openFile=false to prevent focus disruption;
// file is written via fs.writeFile and VSCode dirty state is resolved via
// openTextDocument + doc.save()
await task.diffViewProvider.saveDirectly(
relPath,
newContent,
isNewFile,
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
// Call saveChanges to update the DiffViewProvider properties
Expand Down
10 changes: 8 additions & 2 deletions src/core/tools/EditTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,16 @@ export class EditTool extends BaseTool<"edit"> {
return
}

// Save the changes
if (isPreventFocusDisruptionEnabled) {
// Direct file write without diff view or opening the file
await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
await task.diffViewProvider.saveDirectly(
relPath,
newContent,
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
// Call saveChanges to update the DiffViewProvider properties
await task.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
Expand Down
10 changes: 8 additions & 2 deletions src/core/tools/SearchReplaceTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,16 @@ export class SearchReplaceTool extends BaseTool<"search_replace"> {
return
}

// Save the changes
if (isPreventFocusDisruptionEnabled) {
// Direct file write without diff view or opening the file
await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
await task.diffViewProvider.saveDirectly(
relPath,
newContent,
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
// Call saveChanges to update the DiffViewProvider properties
await task.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
Expand Down
10 changes: 9 additions & 1 deletion src/core/tools/WriteToFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,15 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
return
}

await task.diffViewProvider.saveDirectly(relPath, newContent, false, diagnosticsEnabled, writeDelayMs)
// Direct file write without opening diff view (background editing)
await task.diffViewProvider.saveDirectly(
relPath,
newContent,
false,
diagnosticsEnabled,
writeDelayMs,
isWriteProtected,
)
} else {
if (!task.diffViewProvider.isEditing) {
const partialMessage = JSON.stringify(sharedMessageProps)
Expand Down
89 changes: 79 additions & 10 deletions src/integrations/editor/DiffViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,38 +645,87 @@ export class DiffViewProvider {
openFile: boolean = true,
diagnosticsEnabled: boolean = true,
writeDelayMs: number = DEFAULT_WRITE_DELAY_MS,
isWriteProtected: boolean = false,
): Promise<{
newProblemsMessage: string | undefined
userEdits: string | undefined
finalContent: string | undefined
}> {
const absolutePath = path.resolve(this.cwd, relPath)

// Protected files must always show diff view for manual review,
// even when background editing is enabled. This prevents accidental
// modification of sensitive configuration files.
if (isWriteProtected && !openFile) {
openFile = true
}

// When auto-approval is disabled, force showing the file so the user
// can review changes. Background editing only makes sense when writes
// are auto-approved (#8736).
if (!openFile) {
const task = this.taskRef.deref()
const provider = task?.providerRef.deref()
if (provider) {
const state = await provider.getState()
if (!state?.autoApprovalEnabled) {
openFile = true
}
}
}

// Get diagnostics before editing the file
this.preDiagnostics = vscode.languages.getDiagnostics()

// Write the content directly to the file
// Write the content directly to the file using Node's fs.
// Node's fs.writeFile does NOT notify VSCode's file watcher, which is
// intentional — it prevents open editor tabs from showing "unsaved changes"
// prompts when the user tries to close them after background editing.
await createDirectoriesForFile(absolutePath)
await fs.writeFile(absolutePath, content, "utf-8")

// Verify the content was written correctly to disk with exponential backoff retry
const fileUri = vscode.Uri.file(absolutePath)
const MAX_WRITE_RETRIES = 3
let writeVerified = false
for (let attempt = 0; attempt < MAX_WRITE_RETRIES; attempt++) {
const verifyContent = await fs.readFile(absolutePath, "utf-8")
if (verifyContent === content) {
writeVerified = true
break
}
if (attempt < MAX_WRITE_RETRIES - 1) {
// Exponential backoff: 100ms, 200ms
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 100))
await fs.writeFile(absolutePath, content, "utf-8")
}
}
if (!writeVerified) {
throw new Error(`Failed to save content to ${relPath} after ${MAX_WRITE_RETRIES} attempts`)
}

// Open the document to ensure diagnostics are loaded
// When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open in memory
// When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open
// in memory and immediately save to mark it as "clean" in VSCode — this
// prevents the "unsaved changes" prompt when closing the tab.
if (openFile) {
// Show the document in the editor
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
// Show the document in the editor without stealing focus
await vscode.window.showTextDocument(fileUri, {
preview: false,
preserveFocus: true,
})
} else {
// Just open the document in memory to trigger diagnostics without showing it
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(absolutePath))
// Open the document in memory to trigger diagnostics without showing it
const doc = await vscode.workspace.openTextDocument(fileUri)

// Save the document to ensure VSCode recognizes it as saved and triggers diagnostics
// Save the document to ensure VSCode recognizes it as saved and
// triggers diagnostics. Without this, VSCode would show "unsaved
// changes" when the user tries to close the file.
if (doc.isDirty) {
await doc.save()
}

// Force a small delay to ensure diagnostics are triggered
// Small delay to allow diagnostics to be triggered
await new Promise((resolve) => setTimeout(resolve, 100))
}

Expand Down Expand Up @@ -712,15 +761,35 @@ export class DiffViewProvider {
newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : ""
}

// Read back the final content to detect any user modifications
// that may have occurred via external editors or file watchers
let detectedUserEdits: string | undefined
try {
const finalDoc = await vscode.workspace.openTextDocument(vscode.Uri.file(absolutePath))
const finalDocContent = finalDoc.getText()
const normalizedExpected = content.replace(/\r\n|\n/g, "\n")
const normalizedActual = finalDocContent.replace(/\r\n|\n/g, "\n")

if (normalizedActual !== normalizedExpected) {
detectedUserEdits = formatResponse.createPrettyPatch(
relPath.toPosix(),
normalizedExpected,
normalizedActual,
)
}
} catch {
// If we can't read back the document, proceed without user edit detection
}

// Store the results for formatFileWriteResponse
this.newProblemsMessage = newProblemsMessage
this.userEdits = undefined
this.userEdits = detectedUserEdits
this.relPath = relPath
this.newContent = content

return {
newProblemsMessage,
userEdits: undefined,
userEdits: detectedUserEdits,
finalContent: content,
}
}
Expand Down
Loading
Loading