fix(editor): rendered preview stays stale after discarding changes#2433
fix(editor): rendered preview stays stale after discarding changes#2433george-vii wants to merge 2 commits into
Conversation
Discarding a change (or any working-tree revert the fs-watcher surfaces) reloaded the buffer model's content but left an open rendered preview showing the old content until the tab was remounted. Disk-driven buffer reloads (applyDiskUpdate, reloadFromDisk) update the Monaco model inside a reloadingFromDisk guard, which makes the buffer's change listener bail out and skip bumping bufferVersions. Read-only reactive consumers (the rendered markdown/html/svg preview) subscribe to bufferVersions, so they never re-rendered. Both reload paths now bump bufferVersions after applying the reloaded content. Adds a regression test: a disk reload updates a clean open buffer and bumps bufferVersions.
Greptile SummaryThis PR fixes a stale rendered preview bug in the Monaco-backed editor: after a discard (or any disk-driven buffer reload), the markdown/html/svg preview kept showing the old content because
Confidence Score: 4/5Safe to merge — the fix is narrowly scoped to two reload paths, the logic is correct, and the new regression test validates the primary scenario. The bump logic is correct and well-placed in both reload paths. reloadFromDisk bumps bufferVersions unconditionally regardless of whether content actually changed (unlike applyDiskUpdate which guards on !newMatchesBuffer), and in applyDiskUpdate the version bump and dirty-flag clear are dispatched in two separate MobX transactions, leaving a narrow window where a reactive consumer could observe version-bumped-but-still-dirty state. monaco-model-registry.ts around the reloadFromDisk method warrants a second look for the unconditional version bump and the split runInAction pattern in applyDiskUpdate.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts | Adds explicit bufferVersions bumps in reloadFromDisk and applyDiskUpdate after disk-driven buffer reloads, fixing stale preview rendering. Minor: reloadFromDisk bumps unconditionally (unlike applyDiskUpdate's content-diff guard), and the new bump in applyDiskUpdate uses a separate runInAction from the existing dirty-clear. |
| apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.test.ts | Adds a regression test for the bufferVersions bump via the invalidateModel→applyDiskUpdate path; the reloadFromDisk path is not covered by the new test. |
Sequence Diagram
sequenceDiagram
participant UI as Discard / Branch Switch
participant Reg as MonacoModelRegistry
participant Disk as Disk Model
participant Buf as Buffer Model
participant Preview as Preview Renderer
UI->>Reg: invalidateModel(diskUri)
Reg->>Disk: readFile() new content
Reg->>Reg: applyDiskUpdate(diskUri, entry, newContent)
Reg->>Buf: "applyEdits reloadingFromDisk=true"
Note over Buf: onDidChangeContent fires but bails
Reg->>Reg: reloadingFromDisk.delete(bufferUri)
Reg->>Reg: runInAction bufferVersions++ NEW
Reg->>Reg: runInAction dirtyUris.delete
Reg-->>Preview: MobX notifies
Preview->>Buf: getValue renders new content
UI->>Reg: reloadFromDisk(uri)
Reg->>Disk: getValue
Reg->>Buf: "setValue reloadingFromDisk=true"
Note over Buf: onDidChangeContent fires but bails
Reg->>Reg: reloadingFromDisk.delete(uri)
Reg->>Reg: runInAction dirtyUris.delete + bufferVersions++ NEW
Reg-->>Preview: MobX notifies
Preview->>Buf: getValue renders new content
Comments Outside Diff (3)
-
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts, line 705-719 (link)reloadFromDiskbumps version even when content is unchangedsetValue(disk.model.getValue())is called unconditionally, and thenbufferVersionsis always bumped. If the disk content is already identical to the buffer (e.g. a no-op "Accept Incoming" after an external edit that matches the open buffer), this fires an unnecessary preview re-render. UnlikeapplyDiskUpdate, which only bumps when!newMatchesBuffer,reloadFromDiskhas no such guard. The outcome is correct but slightly inconsistent with the sister function's pattern and could cause spurious re-renders in edge cases.Prompt To Fix With AI
This is a comment left during a code review. Path: apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts Line: 705-719 Comment: **`reloadFromDisk` bumps version even when content is unchanged** `setValue(disk.model.getValue())` is called unconditionally, and then `bufferVersions` is always bumped. If the disk content is already identical to the buffer (e.g. a no-op "Accept Incoming" after an external edit that matches the open buffer), this fires an unnecessary preview re-render. Unlike `applyDiskUpdate`, which only bumps when `!newMatchesBuffer`, `reloadFromDisk` has no such guard. The outcome is correct but slightly inconsistent with the sister function's pattern and could cause spurious re-renders in edge cases. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
-
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts, line 841-863 (link)Two separate
runInActioncalls for related observable writesThe new
bufferVersionsbump and the existingdirtyUris.deleteare dispatched in two separate MobX transactions. Between them, a MobX reaction (e.g. acomputedthat reads both) could observe an intermediate state where the version is already bumped but the dirty flag is not yet cleared. In the main discard path both observables matter to consumers. Merging them into a singlerunInActionwould atomically update both and remove any window for inconsistent intermediate state.Prompt To Fix With AI
This is a comment left during a code review. Path: apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts Line: 841-863 Comment: **Two separate `runInAction` calls for related observable writes** The new `bufferVersions` bump and the existing `dirtyUris.delete` are dispatched in two separate MobX transactions. Between them, a MobX reaction (e.g. a `computed` that reads both) could observe an intermediate state where the version is already bumped but the dirty flag is not yet cleared. In the main discard path both observables matter to consumers. Merging them into a single `runInAction` would atomically update both and remove any window for inconsistent intermediate state. How can I resolve this? If you propose a fix, please make it concise.
-
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.test.ts, line 112-155 (link)Test covers
applyDiskUpdateonly;reloadFromDiskis untestedThe new regression test exercises the
invalidateModel→applyDiskUpdatepath, but the "Accept Incoming" (reloadFromDisk) path — the other place wherebufferVersionswas patched — has no corresponding test. Adding a parallel test that callsreloadFromDiskdirectly would close the coverage gap and guard against the same regression re-appearing in the conflict-dialog flow.Prompt To Fix With AI
This is a comment left during a code review. Path: apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.test.ts Line: 112-155 Comment: **Test covers `applyDiskUpdate` only; `reloadFromDisk` is untested** The new regression test exercises the `invalidateModel` → `applyDiskUpdate` path, but the "Accept Incoming" (`reloadFromDisk`) path — the other place where `bufferVersions` was patched — has no corresponding test. Adding a parallel test that calls `reloadFromDisk` directly would close the coverage gap and guard against the same regression re-appearing in the conflict-dialog flow. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts:705-719
**`reloadFromDisk` bumps version even when content is unchanged**
`setValue(disk.model.getValue())` is called unconditionally, and then `bufferVersions` is always bumped. If the disk content is already identical to the buffer (e.g. a no-op "Accept Incoming" after an external edit that matches the open buffer), this fires an unnecessary preview re-render. Unlike `applyDiskUpdate`, which only bumps when `!newMatchesBuffer`, `reloadFromDisk` has no such guard. The outcome is correct but slightly inconsistent with the sister function's pattern and could cause spurious re-renders in edge cases.
### Issue 2 of 3
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.ts:841-863
**Two separate `runInAction` calls for related observable writes**
The new `bufferVersions` bump and the existing `dirtyUris.delete` are dispatched in two separate MobX transactions. Between them, a MobX reaction (e.g. a `computed` that reads both) could observe an intermediate state where the version is already bumped but the dirty flag is not yet cleared. In the main discard path both observables matter to consumers. Merging them into a single `runInAction` would atomically update both and remove any window for inconsistent intermediate state.
### Issue 3 of 3
apps/emdash-desktop/src/renderer/lib/monaco/monaco-model-registry.test.ts:112-155
**Test covers `applyDiskUpdate` only; `reloadFromDisk` is untested**
The new regression test exercises the `invalidateModel` → `applyDiskUpdate` path, but the "Accept Incoming" (`reloadFromDisk`) path — the other place where `bufferVersions` was patched — has no corresponding test. Adding a parallel test that calls `reloadFromDisk` directly would close the coverage gap and guard against the same regression re-appearing in the conflict-dialog flow.
Reviews (1): Last reviewed commit: "fix(editor): refresh rendered preview af..." | Re-trigger Greptile
|
Addressed the review feedback:
|
Bug
After discarding a change to a file open in the editor's rendered preview (markdown/html/svg), the preview keeps showing the discarded content even though the working tree is reverted and the Changes panel is clean. It only refreshes on a tab remount (close + reopen).
Repro
Root cause
Disk-driven buffer reloads (
applyDiskUpdate,reloadFromDisk) update the Monaco buffer model inside areloadingFromDiskguard. The buffer'sonDidChangeContentlistener intentionally bails while that guard is set (so a disk reload isn't treated as a user edit) — but that also skips bumpingbufferVersions. The rendered preview's only reactive dependency isbufferVersions, so it never re-renders. The Monaco source view updates fine (bound to the model directly), which is why only preview mode is affected and a remount "fixes" it.Fix
Bump
bufferVersionsexplicitly in both reload paths after applying the reloaded content, decoupled from the dirty/autosave logic the listener guards. Covers discard, watcher-surfaced external edits, branch switches, and the conflict-dialog "Accept Incoming" path.Testing
bufferVersions.