one buffer per file, shared by every panel showing it

The code editor held file contents per panel while Monaco keyed the model by
file URI globally, so two panels on one file had two buffers and one model.
@monaco-editor/react resolves a model as getModel(uri) || createModel(value),
so the second panel adopted the first one's model and its own value was
ignored; the value-sync effect then overwrote the shared model with
executeEdits, and the flag suppressing onChange around that write is
per-instance — so the OTHER panel's onChange fired, and the editor answers that
with setContent. One panel's buffer silently became the other's and the tab
went dirty with no keystroke.

useEditorState now keys buffers by path in one shared store and keeps only the
tab set and active path per panel. openFile discards the content it is handed
when the file is already open elsewhere, mirroring what Monaco does with the
model — that is what removes the divergence, because there is then no value for
the sync effect to reconcile. Buffers are refcounted by holders and dropped
when the last panel closes, so closing a tab cannot discard another panel's
unsaved work; closing the last one still discards, as before.

keepCurrentModel is the other half and was needed under any fix: the default
disposes the shared model on unmount, and key={activeFile.path} makes that
every tab switch rather than only a panel close.

Writes go through queryClient.setQueryData rather than useGlobal's setter,
which applies a functional update to the data captured in render — invisible
with one writer, wrong with one per panel.

Nine tests, and the editor had none before. The known-bugs card stays OPEN: this
removes a mechanism that produces the reported shape, not the mystery of where
the log text came from. The trap stays armed, and it can now fire legitimately
when two panels share a file, so the stack matters more than the count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
This commit is contained in:
2026-08-25 07:45:32 +00:00
co-authored by Claude Opus 5
parent 27a8c29fe4
commit a3fced25c4
5 changed files with 352 additions and 31 deletions
@@ -77,6 +77,20 @@ reopening. It does **not** explain how job output becomes one of those buffers,
writes job output into `useEditorState`. So the trap stays armed; it is still the only thing that can name
where the text came from. Full write-up in `docs/editor-buffer-corruption.md`.
**Mechanism removed 2026-08-25.** `useEditorState` now holds one buffer per file, shared by every panel
that has it open, and keeps only the tab set and active path per panel — the state now matches the model
Monaco was already sharing. `openFile` deliberately discards the content it is handed when the file is
open elsewhere, which is what removes the divergence: with both panels reading one buffer there is no
`value` for the sync effect to reconcile, so the `executeEdits` never runs and the cross-panel `onChange`
never fires. A buffer is refcounted by `holders` and dropped when the last panel closes it, so closing a
tab in one panel cannot discard another's unsaved work. `<Editor keepCurrentModel>` stops the shared model
being disposed on every tab switch. Nine tests in `useEditorState.test.ts`.
**Left open on purpose.** This removes a mechanism that produces the reported SHAPE; it does not explain
where the log text came from, and that question is the actual bug. The trap stays armed and
`docs/editor-buffer-corruption.md` stays. One new wrinkle recorded there: two panels on one file can now
fire the trap legitimately on a large paste, so the `stack` matters more than the count.
Worth noting because it is the reason to look at all: an editor that can show content the file does
not contain is a correctness problem even when it never writes — the next person's instinct will be to
save the file to "fix" it.