The "two Monaco editors sharing a model" elimination asked which OTHER
component mounts Monaco and never asked whether the code editor collides with
itself. It does — the panel is addable to a dashboard twice, and useEditorState
keeps files in a plain useState, so two panels hold two independent buffers for
one globally-keyed model.
Verified against @monaco-editor/react@4.7.0's built source rather than inferred.
Model lookup is getModel(uri) || createModel(value), so the second mount adopts
the first panel's buffer and ignores its own freshly-read value. The value-sync
effect then overwrites the shared model with executeEdits. And the flag that
suppresses onChange during that write is per-instance, so one panel's
programmatic write fires the OTHER panel's onChange — which CodeEditor answers
with setContent(path, value). The other panel's buffer becomes this one's
content and goes dirty with no keystroke: dirty tab, content nobody typed, disk
untouched, fixed by reopening. Exactly the reported shape.
Unmount also disposes the shared model (keepCurrentModel defaults false), and
key={activeFile.path} means that fires on every tab switch, not only on panel
close — so one panel can dispose a model another is still displaying.
Not yet a closed case: nothing in the app writes job output into
useEditorState, so this produces the right shape but does not explain where the
log text comes from. No fix shipped — the two candidates (per-panel model URIs
vs sharing the state to match the shared model) differ in whether Monaco's
TypeScript resolution keeps working, and that is the owner's call. Trap stays
armed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
83 lines
5.7 KiB
Markdown
83 lines
5.7 KiB
Markdown
---
|
|
id: known-bugs/background-job-logs-appeared-inside-an-open
|
|
title: 'Background job logs appeared INSIDE an open file in the code editor'
|
|
theme: known-bugs
|
|
status: open
|
|
created: 2026-08-16
|
|
date_source: stated
|
|
order: 1
|
|
---
|
|
|
|
**Background job logs appeared INSIDE an open file in the code editor.** Reported 2026-08-16 by the
|
|
owner, on the production host, running as a member with a `bun dev` background job streaming. The
|
|
editor had a file open; the runner's log output was rendered into that file's buffer. **It was never
|
|
saved** — the file on disk was intact, and the editor was not marked dirty as far as the report goes.
|
|
So this is a DISPLAY-layer mixing, not a write path: the bytes reached a buffer that only exists in
|
|
the browser.
|
|
|
|
Not reproduced, not diagnosed, and deliberately not guessed at further than this. What is worth
|
|
recording is where the two streams could meet, because both were added in the same week and they are
|
|
the only two things writing text into a panel:
|
|
|
|
- `useTaskDetail` polls `GET /chat/tasks/:id` every 2s while a job runs and returns `{kind:'log', text}`.
|
|
`CodeSurface` renders it — and `CodeSurface` is shared with the editor's own surfaces.
|
|
- The editor keys open files by PATH (`useEditorState`), while the task panel keys by TASK ID, and
|
|
several panel-scoped stores in between are keyed by `panelId` through `useGlobal` /
|
|
`usePanelMemory`. A key collision between those namespaces would produce exactly this: one panel's
|
|
text arriving in another's buffer, with no server involvement and nothing persisted.
|
|
|
|
**Screenshot 2026-08-16 answers the first question and kills two theories.** The tab carries a DIRTY
|
|
dot, and the log lines have Monaco line numbers continuing the file's own sequence (…180, then eight
|
|
lines of dotenv/POSTGRES_URL output, then 189 `</Link>`). So the MODEL was mutated — this is the
|
|
editor's content state, not its render, and the earlier "the file was okay" means it was simply never
|
|
saved. Note the splice is mid-file, not a replacement: JSX text and log text share line 180
|
|
(`Forgot?--env-file=.env --watch src/server.tsx`).
|
|
|
|
Ruled out by that: `CodeSurface` renders with shiki, not Monaco, so the two cannot share an editor
|
|
instance. And the app's only other Monaco mount (`FileViewer/renderers/EditorRenderer.tsx`) passes no
|
|
`path`, so its model URI is `inmemory://model/N` and cannot collide with the editor's `file:///…`.
|
|
|
|
So the pollution arrives as TEXT through the editor's own content path — `openFile`, `setContent`, or
|
|
the end-of-turn refresh that re-reads every clean file. That last one is the prime suspect: it is the
|
|
only thing that writes a buffer without a keystroke, it runs exactly when a background job is
|
|
producing output, and it lands its result by path.
|
|
|
|
**Narrowed 2026-08-16, and a trap is now set.** `reloadFile` sets `originalContent` and clears
|
|
`isDirty`, so the end-of-turn refresh CANNOT produce the dirty dot in the screenshot — the refresh
|
|
path is out. Nothing in the app injects text into the page either: no `execCommand('insertText')`,
|
|
no synthetic keyboard events, no clipboard writes near the editor. That leaves Monaco's own change
|
|
event, which means it looks like a paste or a programmatic `executeEdits`.
|
|
|
|
**`docs/editor-buffer-corruption.md` is the procedure — read that when someone reports this.** A
|
|
trap is set: `reportSuspiciousEdit` in CodeEditor.tsx records any insertion over 200 characters
|
|
arriving while the editor is NOT focused, with the stack at that moment, into
|
|
`localStorage['officer:editor-buffer-trap']` (last five). It persists rather than only logging
|
|
because this arrives as a report hours later, when the console is long gone — the question becomes
|
|
"paste this key" instead of "what did you see?". Delete all of it once the cause is known.
|
|
|
|
One thing to rule out first, because it is mine and it is recent: `2b70a570` gave `<Editor>` a real
|
|
`path`, so models are now keyed by file URI globally rather than being per-mount `inmemory://`. Two
|
|
panels open on the same file now share one model. That should be an improvement, but it is a change
|
|
in exactly the layer that turned out to be involved.
|
|
|
|
**2026-08-25 — that suspicion was right, and the "ruled out" row above was wrong.** The elimination asked
|
|
which OTHER component mounts Monaco and never asked whether the code editor collides with **itself**. It
|
|
does: the panel is addable to a dashboard twice, and `useEditorState` keeps `files` in a plain `useState`,
|
|
so two panels hold two independent buffers for one globally-keyed model. Verified in
|
|
`@monaco-editor/react@4.7.0`'s source: model lookup is `getModel(uri) || createModel(value)` so the second
|
|
mount's value is ignored, the value-sync effect then overwrites the shared model with `executeEdits`, and
|
|
**the flag that suppresses `onChange` during that write is per-instance** — so one panel's programmatic
|
|
write fires the OTHER panel's `onChange`. `CodeEditor` responds with `setContent(path, value)`: the other
|
|
panel's buffer becomes this one's content and goes dirty with no keystroke. Unmount also disposes the
|
|
shared model (`keepCurrentModel` defaults false), and `key={activeFile.path}` means that happens on every
|
|
tab switch, not just on panel close.
|
|
|
|
That reproduces the symptom's exact shape — dirty tab, content nobody typed, disk clean, fixed by
|
|
reopening. It does **not** explain how job output becomes one of those buffers, and nothing in the app
|
|
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`.
|
|
|
|
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.
|