@@ -10,24 +10,23 @@ import { FsWriteParams } from './types'
1010
1111export const diffViewUriScheme = 'amazonq-diff'
1212
13+ type StreamingSession = {
14+ filePath : string
15+ tempFilePath : string
16+ originalContent : string
17+ activeDiffEditor : vscode . TextEditor
18+ fadedOverlayController : DecorationController
19+ activeLineController : DecorationController
20+ streamedLines : string [ ]
21+ disposed : boolean
22+ fsWriteParams ?: FsWriteParams
23+ }
24+
1325/**
1426 * Streaming Diff Controller using temporary files for animations
1527 */
1628export class StreamingDiffController implements vscode . Disposable {
17- private activeStreamingSessions = new Map <
18- string ,
19- {
20- filePath : string
21- tempFilePath : string
22- originalContent : string
23- activeDiffEditor : vscode . TextEditor
24- fadedOverlayController : DecorationController
25- activeLineController : DecorationController
26- streamedLines : string [ ]
27- disposed : boolean
28- fsWriteParams ?: FsWriteParams
29- }
30- > ( )
29+ private activeStreamingSessions = new Map < string , StreamingSession > ( )
3130
3231 private fsReplaceSessionsByFile = new Map <
3332 string ,
@@ -257,11 +256,7 @@ export class StreamingDiffController implements vscode.Disposable {
257256 }
258257 }
259258
260- /**
261- * Handle fsReplace diffPair phase - individual diff pair animation (like Cline's SEARCH/REPLACE blocks)
262- * **RACE CONDITION FIX**: Ensures the same temp file is reused for all diff pairs from the same toolUseId
263- */
264- async handleFsReplaceDiffPair ( session : any , partialContent : string , isFinal : boolean ) : Promise < void > {
259+ async handleFsReplaceDiffPair ( session : StreamingSession , partialContent : string , isFinal : boolean ) : Promise < void > {
265260 try {
266261 const diffEditor = session . activeDiffEditor
267262 const document = diffEditor . document
@@ -278,24 +273,22 @@ export class StreamingDiffController implements vscode.Disposable {
278273 return
279274 }
280275 const currentContent = document . getText ( )
281-
282- if ( document . uri . fsPath !== session . tempFilePath ) {
283- try {
284- const correctDocument = await vscode . workspace . openTextDocument (
285- vscode . Uri . file ( session . tempFilePath )
276+ if ( document . uri . fsPath === session . tempFilePath ) {
277+ return
278+ }
279+ try {
280+ const correctDocument = await vscode . workspace . openTextDocument ( vscode . Uri . file ( session . tempFilePath ) )
281+ if ( correctDocument ) {
282+ const correctEditor = vscode . window . visibleTextEditors . find (
283+ ( editor ) => editor . document . uri . fsPath === session . tempFilePath
286284 )
287- if ( correctDocument ) {
288- const correctEditor = vscode . window . visibleTextEditors . find (
289- ( editor ) => editor . document . uri . fsPath === session . tempFilePath
290- )
291- if ( correctEditor ) {
292- session . activeDiffEditor = correctEditor
293- }
285+ if ( correctEditor ) {
286+ session . activeDiffEditor = correctEditor
294287 }
295- } catch ( error ) {
296- getLogger ( ) . error ( `[StreamingDiffController] ❌ Failed to correct document path: ${ error } ` )
297- return
298288 }
289+ } catch ( error ) {
290+ getLogger ( ) . error ( `[StreamingDiffController] ❌ Failed to correct document path: ${ error } ` )
291+ return
299292 }
300293
301294 // Find the location of oldStr in the current content
@@ -324,8 +317,11 @@ export class StreamingDiffController implements vscode.Disposable {
324317 for ( let lineNum = startLineNumber ; lineNum <= newEndLineNumber ; lineNum ++ ) {
325318 session . activeLineController . setActiveLine ( lineNum )
326319 session . fadedOverlayController . updateOverlayAfterLine ( lineNum , document . lineCount )
320+ // Small delay to create smooth line-by-line animation effect for visual feedback
327321 await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) )
328322 }
323+ // Clear active line highlighting after a brief delay to allow user to see the final result
324+ // before moving to the next diff pair or completing the animation
329325 setTimeout ( ( ) => {
330326 session . activeLineController . clear ( )
331327 } , 500 )
@@ -378,9 +374,6 @@ export class StreamingDiffController implements vscode.Disposable {
378374 }
379375 }
380376
381- /**
382- * Scroll editor to line like Cline
383- */
384377 private scrollEditorToLine ( editor : vscode . TextEditor , line : number ) : void {
385378 const scrollLine = line
386379 editor . revealRange ( new vscode . Range ( scrollLine , 0 , scrollLine , 0 ) , vscode . TextEditorRevealType . InCenter )
@@ -436,7 +429,7 @@ export class StreamingDiffController implements vscode.Disposable {
436429 /**
437430 * Handle fsReplace completion signal from parser - triggers immediate cleanup
438431 */
439- private async handleFsReplaceCompletionSignal ( session : any ) : Promise < void > {
432+ private async handleFsReplaceCompletionSignal ( session : StreamingSession ) : Promise < void > {
440433 const filePath = session . filePath
441434 try {
442435 // Clear decorations immediately
@@ -453,6 +446,8 @@ export class StreamingDiffController implements vscode.Disposable {
453446 getLogger ( ) . error ( `[StreamingDiffController] ❌ Failed to save fsReplace temp file: ${ saveError } ` )
454447 }
455448 }
449+ // Delay cleanup to allow final UI updates and user to see completion state
450+ // before removing visual elements and cleaning up resources
456451 setTimeout ( async ( ) => {
457452 try {
458453 await this . cleanupTempFile ( session . tempFilePath )
@@ -480,10 +475,27 @@ export class StreamingDiffController implements vscode.Disposable {
480475 }
481476 }
482477
478+ /**
479+ * Clean up multiple streaming sessions by their tool use IDs
480+ */
481+ private async cleanupSessions ( toolUseIds : Set < string > ) : Promise < void > {
482+ for ( const toolUseId of toolUseIds ) {
483+ const sessionToCleanup = this . activeStreamingSessions . get ( toolUseId )
484+ if ( sessionToCleanup ) {
485+ sessionToCleanup . disposed = true
486+ this . activeStreamingSessions . delete ( toolUseId )
487+ }
488+ }
489+ }
490+
483491 /**
484492 * Handle fsReplace completion - properly track and cleanup when all diff pairs for a file are done
485493 */
486- private async handleFsReplaceCompletion ( session : any , pairIndex : number , totalPairs : number ) : Promise < void > {
494+ private async handleFsReplaceCompletion (
495+ session : StreamingSession ,
496+ pairIndex : number ,
497+ totalPairs : number
498+ ) : Promise < void > {
487499 const filePath = session . filePath
488500 const fsReplaceSession = this . fsReplaceSessionsByFile . get ( filePath )
489501
@@ -501,13 +513,7 @@ export class StreamingDiffController implements vscode.Disposable {
501513 setTimeout ( async ( ) => {
502514 try {
503515 await this . cleanupTempFile ( fsReplaceSession . tempFilePath )
504- for ( const toolUseId of fsReplaceSession . toolUseIds ) {
505- const sessionToCleanup = this . activeStreamingSessions . get ( toolUseId )
506- if ( sessionToCleanup ) {
507- sessionToCleanup . disposed = true
508- this . activeStreamingSessions . delete ( toolUseId )
509- }
510- }
516+ await this . cleanupSessions ( fsReplaceSession . toolUseIds )
511517 this . fsReplaceSessionsByFile . delete ( filePath )
512518 } catch ( error ) {
513519 getLogger ( ) . warn (
@@ -522,18 +528,23 @@ export class StreamingDiffController implements vscode.Disposable {
522528 * Clean up all temporary files for a chat session
523529 */
524530 async cleanupChatSession ( ) : Promise < void > {
525- const tempFilesToCleanup : string [ ] = [ ]
526- for ( const [ , session ] of this . activeStreamingSessions . entries ( ) ) {
531+ const tempFilePaths : string [ ] = [ ]
532+
533+ // Collect from active streaming sessions
534+ for ( const session of this . activeStreamingSessions . values ( ) ) {
527535 if ( session . tempFilePath ) {
528- tempFilesToCleanup . push ( session . tempFilePath )
536+ tempFilePaths . push ( session . tempFilePath )
529537 }
530538 }
531- for ( const [ , fsReplaceSession ] of this . fsReplaceSessionsByFile . entries ( ) ) {
532- if ( fsReplaceSession . tempFilePath ) {
533- tempFilesToCleanup . push ( fsReplaceSession . tempFilePath )
539+
540+ // Collect from fs replace sessions
541+ for ( const session of this . fsReplaceSessionsByFile . values ( ) ) {
542+ if ( session . tempFilePath ) {
543+ tempFilePaths . push ( session . tempFilePath )
534544 }
535545 }
536- for ( const tempFilePath of tempFilesToCleanup ) {
546+
547+ for ( const tempFilePath of tempFilePaths ) {
537548 try {
538549 await this . cleanupTempFile ( tempFilePath )
539550 } catch ( error ) {
0 commit comments