set a trap for the editor buffer corruption

Not a fix — I could not find the cause by reading, and shipping a guess dressed as
one is worse than saying so.

What reading DID settle, and it removes two of the three candidates:

- The end-of-turn refresh cannot be it. `reloadFile` sets `originalContent` and
  clears `isDirty`, so no refresh can produce the dirty dot in the screenshot.
- Nothing in the app types into the page: no `execCommand('insertText')`, no
  synthetic keyboard events, no clipboard writes anywhere near the editor.

That leaves Monaco's own change event, which means the model was edited the way a
paste or a programmatic `executeEdits` edits it. Neither is identifiable from a
screenshot taken afterwards, which is why this needs to be caught in the act.

`reportSuspiciousEdit` logs any insertion over 200 characters arriving while the
editor does NOT have focus, with an Error constructed at that moment so the stack
names the caller. A person cannot type 200 characters into an editor they are not
focused on; whatever does will be in that trace.

It prevents nothing and changes no behaviour. Delete it once the cause is known.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 11:15:33 +00:00
co-authored by Claude Opus 5
parent 23f2ecb950
commit fc8d02e8e1
2 changed files with 50 additions and 1 deletions
@@ -28,6 +28,41 @@ type CodeEditorViewProps = {
panelId?: string;
};
/**
* Shout when the buffer changes in a way a person could not have typed.
*
* Background-job output has twice appeared spliced into an open file — the model mutated, the tab marked
* dirty, the file on disk untouched. Two explanations are already ruled out: the end-of-turn refresh
* cannot do it (`reloadFile` clears `isDirty`), and nothing in the app types into the page. That leaves
* Monaco's own change event, which means it looks like a paste or a programmatic edit — and neither can
* be identified from a screenshot after the fact.
*
* So this does not prevent anything. It records the one thing missing: what arrived, how big it was, and
* whether the editor even had focus at the time. A large insertion into an UNFOCUSED editor is not a
* user, and the stack trace attached to it names whatever called it.
*
* Delete this once the cause is known. It is a trap, not a feature.
*/
const SUSPICIOUS_INSERT = 200;
function reportSuspiciousEdit(
editor: MonacoEditor.IStandaloneCodeEditor | null,
file: { path: string; content: string },
next: string,
): void {
const grew = next.length - file.content.length;
if (grew < SUSPICIOUS_INSERT) return;
if (editor?.hasTextFocus()) return;
// eslint-disable-next-line no-console
console.warn(
`[editor] ${grew} characters appeared in ${file.path} while the editor was NOT focused. ` +
`This is the background-job-output bug (TODO.md → Known bugs). First 120 chars:\n` +
next.slice(0, 120),
new Error('stack at the moment of the edit'),
);
}
export const CodeEditorView = ({
className,
theme = 'vs-dark',
@@ -294,7 +329,10 @@ export const CodeEditorView = ({
theme={theme}
language={getLanguage(activeFile.name)}
value={activeFile.content}
onChange={(value) => setContent(activeFile.path, value ?? '')}
onChange={(value) => {
reportSuspiciousEdit(editorRef.current, activeFile, value ?? '');
setContent(activeFile.path, value ?? '');
}}
beforeMount={configureMonacoTypescript}
onMount={handleEditorMount}
options={{