@@ -32,7 +32,8 @@ define(function (require, exports, module) {
3232 const DocumentManager = require ( "document/DocumentManager" ) ,
3333 CommandManager = require ( "command/CommandManager" ) ,
3434 Commands = require ( "command/Commands" ) ,
35- FileSystem = require ( "filesystem/FileSystem" ) ;
35+ FileSystem = require ( "filesystem/FileSystem" ) ,
36+ ProjectManager = require ( "project/ProjectManager" ) ;
3637
3738 // --- Constants ---
3839 const HEARTBEAT_INTERVAL_MS = 60 * 1000 ;
@@ -53,6 +54,9 @@ define(function (require, exports, module) {
5354 const _writtenHashes = new Set ( ) ; // hashes confirmed on disk
5455 let _snapshots = [ ] ; // flat: _snapshots[i] = { filePath: hash|null }
5556 let _pendingBeforeSnap = { } ; // built during current response: filePath → hash|null
57+ const _pendingDeleted = new Set ( ) ; // file paths deleted during current response
58+ const _readFiles = { } ; // filePath → raw content string (files AI has read)
59+ let _isTracking = false ; // true while AI is streaming
5660
5761 // --- Path utility ---
5862
@@ -130,7 +134,7 @@ define(function (require, exports, module) {
130134
131135 function _readContent ( hash ) {
132136 // Check memory buffer first (content may not have flushed yet)
133- if ( _memoryBuffer [ hash ] ) {
137+ if ( _memoryBuffer . hasOwnProperty ( hash ) ) {
134138 return Promise . resolve ( _memoryBuffer [ hash ] ) ;
135139 }
136140 // Read from disk
@@ -146,6 +150,15 @@ define(function (require, exports, module) {
146150 } ) ;
147151 }
148152
153+ function _readFileFromDisk ( vfsPath ) {
154+ return new Promise ( function ( resolve , reject ) {
155+ const file = FileSystem . getFileForPath ( vfsPath ) ;
156+ file . read ( function ( err , data ) {
157+ if ( err ) { reject ( err ) ; } else { resolve ( data ) ; }
158+ } ) ;
159+ } ) ;
160+ }
161+
149162 // --- File operations ---
150163
151164 /**
@@ -180,31 +193,34 @@ define(function (require, exports, module) {
180193 const vfsPath = realToVfsPath ( filePath ) ;
181194 const file = FileSystem . getFileForPath ( vfsPath ) ;
182195
183- const openDoc = DocumentManager . getOpenDocumentForPath ( vfsPath ) ;
184- if ( openDoc ) {
185- if ( openDoc . isDirty ) {
186- openDoc . setText ( "" ) ;
187- }
188- CommandManager . execute ( Commands . FILE_CLOSE , { file : file , _forceClose : true } )
189- . always ( function ( ) {
190- file . unlink ( function ( err ) {
191- if ( err ) {
192- result . reject ( err ) ;
193- } else {
196+ function _unlinkFile ( ) {
197+ file . unlink ( function ( err ) {
198+ if ( err ) {
199+ // File already gone — desired state achieved, treat as success
200+ file . exists ( function ( _existErr , exists ) {
201+ if ( ! exists ) {
194202 result . resolve ( ) ;
203+ } else {
204+ result . reject ( err ) ;
195205 }
196206 } ) ;
197- } ) ;
198- } else {
199- file . unlink ( function ( err ) {
200- if ( err ) {
201- result . reject ( err ) ;
202207 } else {
203208 result . resolve ( ) ;
204209 }
205210 } ) ;
206211 }
207212
213+ const openDoc = DocumentManager . getOpenDocumentForPath ( vfsPath ) ;
214+ if ( openDoc ) {
215+ if ( openDoc . isDirty ) {
216+ openDoc . setText ( "" ) ;
217+ }
218+ CommandManager . execute ( Commands . FILE_CLOSE , { file : file , _forceClose : true } )
219+ . always ( _unlinkFile ) ;
220+ } else {
221+ _unlinkFile ( ) ;
222+ }
223+
208224 return result . promise ( ) ;
209225 }
210226
@@ -224,8 +240,10 @@ define(function (require, exports, module) {
224240 try {
225241 doc . setText ( content ) ;
226242 saveDocToDisk ( doc ) . always ( function ( ) {
227- CommandManager . execute ( Commands . CMD_OPEN , { fullPath : vfsPath } ) ;
228- result . resolve ( ) ;
243+ CommandManager . execute ( Commands . CMD_OPEN , { fullPath : vfsPath } )
244+ . always ( function ( ) {
245+ result . resolve ( ) ;
246+ } ) ;
229247 } ) ;
230248 } catch ( err ) {
231249 result . reject ( err ) ;
@@ -306,6 +324,37 @@ define(function (require, exports, module) {
306324 }
307325 }
308326
327+ /**
328+ * Record a file the AI has read, storing its content hash for potential
329+ * delete/rename tracking. If the file is later deleted, this content
330+ * can be promoted into a snapshot for restore.
331+ * @param {string } filePath - real filesystem path
332+ * @param {string } content - file content at read time
333+ */
334+ function recordFileRead ( filePath , content ) {
335+ _readFiles [ filePath ] = content ;
336+ }
337+
338+ /**
339+ * Record that a file has been deleted during this response.
340+ * If the file hasn't been tracked yet, its previousContent is stored
341+ * and back-filled into existing snapshots.
342+ * @param {string } filePath - real filesystem path
343+ * @param {string } previousContent - content before deletion
344+ */
345+ function recordFileDeletion ( filePath , previousContent ) {
346+ if ( ! _pendingBeforeSnap . hasOwnProperty ( filePath ) ) {
347+ const hash = storeContent ( previousContent ) ;
348+ _pendingBeforeSnap [ filePath ] = hash ;
349+ _snapshots . forEach ( function ( snap ) {
350+ if ( snap [ filePath ] === undefined ) {
351+ snap [ filePath ] = hash ;
352+ }
353+ } ) ;
354+ }
355+ _pendingDeleted . add ( filePath ) ;
356+ }
357+
309358 /**
310359 * Create the initial snapshot (snapshot 0) capturing file state before any
311360 * AI edits. Called once per session on the first edit.
@@ -321,24 +370,44 @@ define(function (require, exports, module) {
321370 * Builds an "after" snapshot from current document content for edited files,
322371 * pushes it, and resets transient tracking variables.
323372 * Flushes in-memory content to disk for long-term storage.
324- * @return {number } the after-snapshot index, or -1 if no edits happened
373+ *
374+ * Priority for each file:
375+ * 1. If in _pendingDeleted → null
376+ * 2. If doc is open → storeContent(openDoc.getText())
377+ * 3. Fallback: read from disk → storeContent(content)
378+ * 4. If disk read fails (file gone) → null
379+ *
380+ * @return {Promise<number> } the after-snapshot index, or -1 if no edits happened
325381 */
326- function finalizeResponse ( ) {
382+ async function finalizeResponse ( ) {
327383 let afterIndex = - 1 ;
328384 if ( Object . keys ( _pendingBeforeSnap ) . length > 0 ) {
329- // Build "after" snapshot = last snapshot + current content of edited files
330385 const afterSnap = Object . assign ( { } , _snapshots [ _snapshots . length - 1 ] ) ;
331- Object . keys ( _pendingBeforeSnap ) . forEach ( function ( fp ) {
386+ const editedPaths = Object . keys ( _pendingBeforeSnap ) ;
387+ for ( let i = 0 ; i < editedPaths . length ; i ++ ) {
388+ const fp = editedPaths [ i ] ;
389+ if ( _pendingDeleted . has ( fp ) ) {
390+ afterSnap [ fp ] = null ;
391+ continue ;
392+ }
332393 const vfsPath = realToVfsPath ( fp ) ;
333394 const openDoc = DocumentManager . getOpenDocumentForPath ( vfsPath ) ;
334395 if ( openDoc ) {
335396 afterSnap [ fp ] = storeContent ( openDoc . getText ( ) ) ;
397+ } else {
398+ try {
399+ const content = await _readFileFromDisk ( vfsPath ) ;
400+ afterSnap [ fp ] = storeContent ( content ) ;
401+ } catch ( e ) {
402+ afterSnap [ fp ] = null ;
403+ }
336404 }
337- } ) ;
405+ }
338406 _snapshots . push ( afterSnap ) ;
339407 afterIndex = _snapshots . length - 1 ;
340408 }
341409 _pendingBeforeSnap = { } ;
410+ _pendingDeleted . clear ( ) ;
342411 _flushToDisk ( ) ;
343412 return afterIndex ;
344413 }
@@ -357,6 +426,74 @@ define(function (require, exports, module) {
357426 onComplete ( errorCount ) ;
358427 }
359428
429+ // --- FS event tracking ---
430+
431+ function _onProjectFileChanged ( _event , entry , addedInProject , removedInProject ) {
432+ if ( ! removedInProject || ! removedInProject . length ) { return ; }
433+ removedInProject . forEach ( function ( removedEntry ) {
434+ if ( ! removedEntry . isFile ) { return ; }
435+ const fp = removedEntry . fullPath ;
436+ // Check if AI has edited this file (already in snapshots)
437+ const isEdited = _pendingBeforeSnap . hasOwnProperty ( fp ) ||
438+ _snapshots . some ( function ( snap ) { return snap . hasOwnProperty ( fp ) ; } ) ;
439+ if ( isEdited ) {
440+ _pendingDeleted . add ( fp ) ;
441+ return ;
442+ }
443+ // Check if AI has read this file (raw content available)
444+ if ( _readFiles . hasOwnProperty ( fp ) ) {
445+ // Promote from read-tracked to snapshot-tracked, then mark deleted
446+ const hash = storeContent ( _readFiles [ fp ] ) ;
447+ _pendingBeforeSnap [ fp ] = hash ;
448+ _snapshots . forEach ( function ( snap ) {
449+ if ( snap [ fp ] === undefined ) {
450+ snap [ fp ] = hash ;
451+ }
452+ } ) ;
453+ _pendingDeleted . add ( fp ) ;
454+ }
455+ } ) ;
456+ }
457+
458+ function _onProjectFileRenamed ( _event , oldPath , newPath ) {
459+ // Update _pendingBeforeSnap
460+ if ( _pendingBeforeSnap . hasOwnProperty ( oldPath ) ) {
461+ _pendingBeforeSnap [ newPath ] = _pendingBeforeSnap [ oldPath ] ;
462+ delete _pendingBeforeSnap [ oldPath ] ;
463+ }
464+ // Update _pendingDeleted
465+ if ( _pendingDeleted . has ( oldPath ) ) {
466+ _pendingDeleted . delete ( oldPath ) ;
467+ _pendingDeleted . add ( newPath ) ;
468+ }
469+ // Update all snapshots
470+ _snapshots . forEach ( function ( snap ) {
471+ if ( snap . hasOwnProperty ( oldPath ) ) {
472+ snap [ newPath ] = snap [ oldPath ] ;
473+ delete snap [ oldPath ] ;
474+ }
475+ } ) ;
476+ // Update _readFiles
477+ if ( _readFiles . hasOwnProperty ( oldPath ) ) {
478+ _readFiles [ newPath ] = _readFiles [ oldPath ] ;
479+ delete _readFiles [ oldPath ] ;
480+ }
481+ }
482+
483+ function startTracking ( ) {
484+ if ( _isTracking ) { return ; }
485+ _isTracking = true ;
486+ ProjectManager . on ( "projectFileChanged" , _onProjectFileChanged ) ;
487+ ProjectManager . on ( "projectFileRenamed" , _onProjectFileRenamed ) ;
488+ }
489+
490+ function stopTracking ( ) {
491+ if ( ! _isTracking ) { return ; }
492+ _isTracking = false ;
493+ ProjectManager . off ( "projectFileChanged" , _onProjectFileChanged ) ;
494+ ProjectManager . off ( "projectFileRenamed" , _onProjectFileRenamed ) ;
495+ }
496+
360497 /**
361498 * @return {number } number of snapshots
362499 */
@@ -373,6 +510,9 @@ define(function (require, exports, module) {
373510 _writtenHashes . clear ( ) ;
374511 _snapshots = [ ] ;
375512 _pendingBeforeSnap = { } ;
513+ _pendingDeleted . clear ( ) ;
514+ Object . keys ( _readFiles ) . forEach ( function ( k ) { delete _readFiles [ k ] ; } ) ;
515+ stopTracking ( ) ;
376516
377517 // Delete and recreate aiSnap directory
378518 if ( _diskReady && _aiSnapDir ) {
@@ -472,9 +612,13 @@ define(function (require, exports, module) {
472612 exports . saveDocToDisk = saveDocToDisk ;
473613 exports . storeContent = storeContent ;
474614 exports . recordFileBeforeEdit = recordFileBeforeEdit ;
615+ exports . recordFileRead = recordFileRead ;
616+ exports . recordFileDeletion = recordFileDeletion ;
475617 exports . createInitialSnapshot = createInitialSnapshot ;
476618 exports . finalizeResponse = finalizeResponse ;
477619 exports . restoreToSnapshot = restoreToSnapshot ;
478620 exports . getSnapshotCount = getSnapshotCount ;
621+ exports . startTracking = startTracking ;
622+ exports . stopTracking = stopTracking ;
479623 exports . reset = reset ;
480624} ) ;
0 commit comments