Skip to content

Re-creating a deleted playground file with the same name shows the deleted content, and the first keystroke overwrites the new file #1741

Open

Description

@joshunrau

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

  1. In the playground, click Add File, create helpers.ts, and type export const a = 1;.
  2. Right-click helpers.ts in the file sidebar, choose Delete and confirm.
  3. Click Upload Files and upload a helpers.ts from disk containing export const b = 2;, then click it in the sidebar.
  4. 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

  1. added
    BugType: existing behavior is wrong
    Difficulty: LowIsolated change, about 2 hours or less
    Area: Playgroundapps/playground (in-browser instrument editor)
    on Oct 7, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still reproduces on main (d86740e). #1660 does not fix it. Its new MonacoEditor.tsx still reuses the model a path already has, and EditorPane's model effect is unchanged there. Reusing a model by path is fine. The bug is that nothing keeps the models in step with files.

    Same cause, more paths

    EditorPane.tsx:66-90 reconciles models only on mount, when selectedInstrument.id changes, and on unmount. Anything that replaces files under the same id leaves the editor on the old models, and the first keystroke writes them back:

    • Restore Defaults on the default template. resetInstruments (instrument.slice.ts:25-35) reloads defaultSelectedInstrument, whose id is a crypto.randomUUID() made once per page load (src/instruments/index.ts:36). On a fresh load, type in index.ts, then Restore Defaults: the preview reverts and the editor does not.
    • setSelectedInstrument with the id that is already selected (instrument.slice.ts:37-51 has 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 IDisposable that addExtraLib returns 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: getScriptFileNames in ts.worker.js returns the model URIs plus the extra-lib keys. For example, after you load Interactive With Legacy Script, the global message from its legacy.d.ts (#1805) type-checks in every other instrument until reload.

    Plan

    Replace the effect with a hook next to useEditorErrorSync, driven by useAppStore.subscribe((store) => store.files, …). Put the diff itself in a pure function, src/utils/editor-models.ts. Compared with the previous files:

    1. Removed name: dispose its model and its extra-lib handle. Do this in every language, because the editor creates CSS, HTML and JSON models lazily by path. Keep the handles in a Map<string, IDisposable>.
    2. Added TS/JS name: create its model, plus the extra lib for TS. Uploaded files that were never opened then get markers, so the upload gate sees their errors too (see Add functionality to lint all files in playground #931).
    3. Kept name whose file object changed and whose model value differs from 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-128 unmounts the pane whenever no tab is open, and files can still change while it is gone. A rename (#1739) becomes a remove plus an add.

    One catch in step 3: calling setValue on the model the outgoing editor still shows fires onChange. setSelectedFileContent then writes to whatever selectedFilename is by that point. Switch between two instruments that both have styles.css while it is open, and the new styles.css content is written into the new instrument's index file. Have onChange call updateFile(defaultFile.name, …) instead.

    Drop the deleted-files clause from useEditorErrorSync.ts:9-13 and apps/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 only EditorPane disposes models, all of them, on an instrument switch or on unmount.

    Tests

    Triage note written with Claude Code. Code references point at main @ d86740e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: Playgroundapps/playground (in-browser instrument editor)BugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions