Repository navigation
Description
In the playground, if you delete a file and then add or upload a new file with the same name, the editor shows the deleted file's old content. The first keystroke then overwrites the new content with that old text. deleteFile removes the file from the store, but its Monaco model stays alive: EditorPane disposes models only when the selected instrument changes, and it creates a model for every .ts/.js file when an instrument loads. @monaco-editor/react looks for an existing model by path before it uses defaultValue, so the re-created file opens the old model. The store, the preview and Download hold the new content while the editor displays the old content. Because onChange sends the model's whole value, the first keystroke replaces the store's content with the old text plus that keystroke. Replacing a file by upload always goes through this path: Upload Files refuses a name that already exists, so the user has to delete the old file first.
Where
apps/playground/src/store/slices/editor.slice.ts:43-52 (no model is disposed):
deleteFile: (name) => {
set((state) => {
state.files = state.files.filter((file) => file.name !== name);apps/playground/src/components/Editor/EditorPane.tsx:87-90 and :133-137, :162:
return () => {
monaco.editor.getModels().forEach((model) => model.dispose());
};
}, [isMounted, selectedInstrumentId]);
// ...
<MonacoEditor
...
defaultValue={defaultFile.content}
keepCurrentModel={true}
...
path={defaultFile.name}Reproduce
- In the playground, click Add File, create
helpers.ts, and typeexport const a = 1;. - Right-click
helpers.tsin the file sidebar, choose Delete and confirm. - Click Upload Files and upload a
helpers.tsfrom disk containingexport const b = 2;, then click it in the sidebar. - Type one character.
Actual: after step 3 the editor shows export const a = 1;, while Download Archive contains export const b = 2;. After step 4 the store holds export const a = 1; plus the character, and the uploaded content is gone.
Expected: the editor shows the uploaded export const b = 2;, and typing edits that content.
Tests
Monaco has no unit-test environment in this project (see apps/playground/AGENTS.md, Tests), so the regression test is end-to-end: in testing/src/specs/playground.spec.ts, add a file, type content, delete it, add a file with the same name, and assert that the editor is empty. If the fix moves the disposal decision into a pure helper (for example one that diffs filenames before and after a store update), test that helper in apps/playground/src/utils/__tests__/ with it('should report the names of removed files, so their editor models are disposed').
Suggested fix
Dispose the Monaco model (and the matching addExtraLib entry) when a file is deleted. For example, EditorPane can subscribe to files and dispose the model for every name that disappears. Update the deleteFile never disposes a model note in apps/playground/AGENTS.md in the same change.
Activity
joshunrau commentedon Oct 26, 2026
Still reproduces on
main(d86740e). #1660 does not fix it. Its newMonacoEditor.tsxstill reuses the model a path already has, andEditorPane's model effect is unchanged there. Reusing a model by path is fine. The bug is that nothing keeps the models in step withfiles.Same cause, more paths
EditorPane.tsx:66-90reconciles models only on mount, whenselectedInstrument.idchanges, and on unmount. Anything that replacesfilesunder the same id leaves the editor on the old models, and the first keystroke writes them back:resetInstruments(instrument.slice.ts:25-35) reloadsdefaultSelectedInstrument, whose id is acrypto.randomUUID()made once per page load (src/instruments/index.ts:36). On a fresh load, type inindex.ts, then Restore Defaults: the preview reverts and the editor does not.setSelectedInstrumentwith the id that is already selected (instrument.slice.ts:37-51has no same-id check). This happens from the instrument list, or from a share link that matches the selected instrument (IndexPage.tsx:51-54).Extra libs leak as well. The
IDisposablethataddExtraLibreturns is thrown away (EditorPane.tsx:81), and the cleanup disposes only models. So a user file's extra lib outlives deletes and instrument switches. Monaco 0.54's worker treats it as a root file of its own, separate from the model:getScriptFileNamesints.worker.jsreturns the model URIs plus the extra-lib keys. For example, after you load Interactive With Legacy Script, the globalmessagefrom itslegacy.d.ts(#1805) type-checks in every other instrument until reload.Plan
Replace the effect with a hook next to
useEditorErrorSync, driven byuseAppStore.subscribe((store) => store.files, …). Put the diff itself in a pure function,src/utils/editor-models.ts. Compared with the previousfiles:Map<string, IDisposable>.file.content:model.setValue(file.content). Typed edits already match, so this runs only when the store replaces content.On mount, run the diff from
[]. On unmount, run it to[].Editor.tsx:122-128unmounts the pane whenever no tab is open, andfilescan still change while it is gone. A rename (#1739) becomes a remove plus an add.One catch in step 3: calling
setValueon the model the outgoing editor still shows firesonChange.setSelectedFileContentthen writes to whateverselectedFilenameis by that point. Switch between two instruments that both havestyles.csswhile it is open, and the newstyles.csscontent is written into the new instrument's index file. HaveonChangecallupdateFile(defaultFile.name, …)instead.Drop the deleted-files clause from
useEditorErrorSync.ts:9-13andapps/playground/AGENTS.md:139-141.This needs neither #1660 nor #1825. If it lands first, #1660 has to port the new code to
monaco.typescript. It also has to drop the sentence in its new AGENTS.md paragraph that says onlyEditorPanedisposes models, all of them, on an instrument switch or on unmount.Tests
src/utils/__tests__/editor-models.test.ts: runs in node against a fakemonaco, with no new dependency. Cover dispose on delete, create on upload,setValueon Restore Defaults, and no call on a typed edit.testing/src/specs/playground.spec.ts: the issue's delete-and-recreate case, plus Restore Defaults after typing inindex.ts. Keepindex.tsopen in the first test, because closing the last tab unmounts the pane and disposes every model, which hides the bug. Add File and Upload Files have no accessible name yet (Playground icon-only buttons have no accessible names, and editor tabs cannot be closed from the keyboard #1763). Until they do, reach them the way fix: resolve 26 low-difficulty issues, one commit each #1825's spec reaches icon buttons,button:has(svg.lucide-file-plus)andlucide-file-up, and type through.monaco-editor .view-lines. fix(playground): search instruments by label #1817, fix: resolve 26 low-difficulty issues, one commit each #1825 and build: upgrade dependencies held back by breaking changes #1660 all edit this spec.Triage note written with Claude Code. Code references point at
main@d86740e.