Skip to content

(viewer): an external write silently replaces unsaved edits, and a rename ends the watch #307

Description

@jbr-sekoia

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions