What happens
Open a memory note in the Memory panel, or a file through a terminal link, and start editing. If anything writes that file while you type — a session, a formatter, a git checkout — your unsaved edits are replaced by what is on disk, without a prompt and without a trace.
public/viewer-panel.js:102-106:
this._onFileChanged = (changedPath) => {
if (changedPath === this._watchedPath && !this._saving) {
this._reloadFromDisk();
}
};
!this._saving is the whole guard. It exists to stop the panel reacting to its own write (_save() sets the flag and clears it 500 ms later). Nothing asks whether the buffer holds work that is not on disk. _reloadFromDisk then dispatches a whole-document replace into the editor — the undo history survives, but nothing tells you to use it, and a panel scrolled away from the cursor gives no visible sign that the content changed.
This reaches the Memory panel and the 'file' tab of the file panel, which are the two ViewerPanel callers that can save. The .work-files panel and the Diagnostics trace viewer are also reloaded, but they have no save path at all, so a buffer there is already unrecoverable by a different route.
The panel next door does the opposite
The Changes panel watches files too, and treats a dirty buffer as something to protect: it reloads a clean buffer, warns on a dirty one, and refuses a save whose file moved underneath it. public/file-panel.js carries five pointers to the rule, and .ai/contexts/changes-view.md states it as "a dirty buffer is never overwritten, and never lied to".
Both behaviours are in the same renderer, on the same kind of file, a few hundred lines apart.
A second defect in the same watch
main.js:1003:
const watcher = fs.watch(resolved, (eventType) => {
if (eventType !== 'change') return;
A rename is discarded. That is how an atomic replacement lands — git checkout, sed -i, and the save path of most editors write a temporary file and rename it over the target. After one of those the watch is armed on an inode nothing writes to any more: the panel goes deaf and shows stale content indefinitely, with no indication.
git-changes-watch.js:46 handles exactly this case by re-arming on rename. The two registries answer the same question and only one of them is right.
What to change
- A reload must not replace a buffer whose content differs from what was last read from disk. Reload a clean buffer; on a dirty one, say the file changed and let the editor decide — the Changes panel's notice is the shape to follow rather than a new one.
- Re-arm on
rename, so an atomic replacement does not silently end the watch.
Not in scope
Merging the two watcher registries, or the wider question of one editor surface for memory files and changed files. This is the data-loss half, and it is worth fixing whether or not that happens. #301 covers the registries' descriptor accounting.
Acceptance
- A dirty buffer survives an external write to its file, and the user is told.
- A clean buffer still reloads.
- A file replaced by rename still reports its next change.
- Each of the three is pinned by a test that goes red when the guard is removed.
What happens
Open a memory note in the Memory panel, or a file through a terminal link, and start editing. If anything writes that file while you type — a session, a formatter, a
git checkout— your unsaved edits are replaced by what is on disk, without a prompt and without a trace.public/viewer-panel.js:102-106:!this._savingis the whole guard. It exists to stop the panel reacting to its own write (_save()sets the flag and clears it 500 ms later). Nothing asks whether the buffer holds work that is not on disk._reloadFromDiskthen dispatches a whole-document replace into the editor — the undo history survives, but nothing tells you to use it, and a panel scrolled away from the cursor gives no visible sign that the content changed.This reaches the Memory panel and the
'file'tab of the file panel, which are the twoViewerPanelcallers that can save. The.work-filespanel and the Diagnostics trace viewer are also reloaded, but they have no save path at all, so a buffer there is already unrecoverable by a different route.The panel next door does the opposite
The Changes panel watches files too, and treats a dirty buffer as something to protect: it reloads a clean buffer, warns on a dirty one, and refuses a save whose file moved underneath it.
public/file-panel.jscarries five pointers to the rule, and.ai/contexts/changes-view.mdstates it as "a dirty buffer is never overwritten, and never lied to".Both behaviours are in the same renderer, on the same kind of file, a few hundred lines apart.
A second defect in the same watch
main.js:1003:A
renameis discarded. That is how an atomic replacement lands —git checkout,sed -i, and the save path of most editors write a temporary file and rename it over the target. After one of those the watch is armed on an inode nothing writes to any more: the panel goes deaf and shows stale content indefinitely, with no indication.git-changes-watch.js:46handles exactly this case by re-arming onrename. The two registries answer the same question and only one of them is right.What to change
rename, so an atomic replacement does not silently end the watch.Not in scope
Merging the two watcher registries, or the wider question of one editor surface for memory files and changed files. This is the data-loss half, and it is worth fixing whether or not that happens. #301 covers the registries' descriptor accounting.
Acceptance