@@ -645,38 +645,87 @@ export class DiffViewProvider {
645645 openFile : boolean = true ,
646646 diagnosticsEnabled : boolean = true ,
647647 writeDelayMs : number = DEFAULT_WRITE_DELAY_MS ,
648+ isWriteProtected : boolean = false ,
648649 ) : Promise < {
649650 newProblemsMessage : string | undefined
650651 userEdits : string | undefined
651652 finalContent : string | undefined
652653 } > {
653654 const absolutePath = path . resolve ( this . cwd , relPath )
654655
656+ // Protected files must always show diff view for manual review,
657+ // even when background editing is enabled. This prevents accidental
658+ // modification of sensitive configuration files.
659+ if ( isWriteProtected && ! openFile ) {
660+ openFile = true
661+ }
662+
663+ // When auto-approval is disabled, force showing the file so the user
664+ // can review changes. Background editing only makes sense when writes
665+ // are auto-approved (#8736).
666+ if ( ! openFile ) {
667+ const task = this . taskRef . deref ( )
668+ const provider = task ?. providerRef . deref ( )
669+ if ( provider ) {
670+ const state = await provider . getState ( )
671+ if ( ! state ?. autoApprovalEnabled ) {
672+ openFile = true
673+ }
674+ }
675+ }
676+
655677 // Get diagnostics before editing the file
656678 this . preDiagnostics = vscode . languages . getDiagnostics ( )
657679
658- // Write the content directly to the file
680+ // Write the content directly to the file using Node's fs.
681+ // Node's fs.writeFile does NOT notify VSCode's file watcher, which is
682+ // intentional — it prevents open editor tabs from showing "unsaved changes"
683+ // prompts when the user tries to close them after background editing.
659684 await createDirectoriesForFile ( absolutePath )
660685 await fs . writeFile ( absolutePath , content , "utf-8" )
661686
687+ // Verify the content was written correctly to disk with exponential backoff retry
688+ const fileUri = vscode . Uri . file ( absolutePath )
689+ const MAX_WRITE_RETRIES = 3
690+ let writeVerified = false
691+ for ( let attempt = 0 ; attempt < MAX_WRITE_RETRIES ; attempt ++ ) {
692+ const verifyContent = await fs . readFile ( absolutePath , "utf-8" )
693+ if ( verifyContent === content ) {
694+ writeVerified = true
695+ break
696+ }
697+ if ( attempt < MAX_WRITE_RETRIES - 1 ) {
698+ // Exponential backoff: 100ms, 200ms
699+ await new Promise ( ( resolve ) => setTimeout ( resolve , Math . pow ( 2 , attempt ) * 100 ) )
700+ await fs . writeFile ( absolutePath , content , "utf-8" )
701+ }
702+ }
703+ if ( ! writeVerified ) {
704+ throw new Error ( `Failed to save content to ${ relPath } after ${ MAX_WRITE_RETRIES } attempts` )
705+ }
706+
662707 // Open the document to ensure diagnostics are loaded
663- // When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open in memory
708+ // When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open
709+ // in memory and immediately save to mark it as "clean" in VSCode — this
710+ // prevents the "unsaved changes" prompt when closing the tab.
664711 if ( openFile ) {
665- // Show the document in the editor
666- await vscode . window . showTextDocument ( vscode . Uri . file ( absolutePath ) , {
712+ // Show the document in the editor without stealing focus
713+ await vscode . window . showTextDocument ( fileUri , {
667714 preview : false ,
668715 preserveFocus : true ,
669716 } )
670717 } else {
671- // Just open the document in memory to trigger diagnostics without showing it
672- const doc = await vscode . workspace . openTextDocument ( vscode . Uri . file ( absolutePath ) )
718+ // Open the document in memory to trigger diagnostics without showing it
719+ const doc = await vscode . workspace . openTextDocument ( fileUri )
673720
674- // Save the document to ensure VSCode recognizes it as saved and triggers diagnostics
721+ // Save the document to ensure VSCode recognizes it as saved and
722+ // triggers diagnostics. Without this, VSCode would show "unsaved
723+ // changes" when the user tries to close the file.
675724 if ( doc . isDirty ) {
676725 await doc . save ( )
677726 }
678727
679- // Force a small delay to ensure diagnostics are triggered
728+ // Small delay to allow diagnostics to be triggered
680729 await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
681730 }
682731
@@ -712,15 +761,35 @@ export class DiffViewProvider {
712761 newProblems . length > 0 ? `\n\nNew problems detected after saving the file:\n${ newProblems } ` : ""
713762 }
714763
764+ // Read back the final content to detect any user modifications
765+ // that may have occurred via external editors or file watchers
766+ let detectedUserEdits : string | undefined
767+ try {
768+ const finalDoc = await vscode . workspace . openTextDocument ( vscode . Uri . file ( absolutePath ) )
769+ const finalDocContent = finalDoc . getText ( )
770+ const normalizedExpected = content . replace ( / \r \n | \n / g, "\n" )
771+ const normalizedActual = finalDocContent . replace ( / \r \n | \n / g, "\n" )
772+
773+ if ( normalizedActual !== normalizedExpected ) {
774+ detectedUserEdits = formatResponse . createPrettyPatch (
775+ relPath . toPosix ( ) ,
776+ normalizedExpected ,
777+ normalizedActual ,
778+ )
779+ }
780+ } catch {
781+ // If we can't read back the document, proceed without user edit detection
782+ }
783+
715784 // Store the results for formatFileWriteResponse
716785 this . newProblemsMessage = newProblemsMessage
717- this . userEdits = undefined
786+ this . userEdits = detectedUserEdits
718787 this . relPath = relPath
719788 this . newContent = content
720789
721790 return {
722791 newProblemsMessage,
723- userEdits : undefined ,
792+ userEdits : detectedUserEdits ,
724793 finalContent : content ,
725794 }
726795 }
0 commit comments