Skip to content

Renaming an open file in the playground leaves its tab on the old name and silently discards every later edit to it #1739

Open

Description

@joshunrau

Renaming an open file in the playground leaves its tab and selection on the old name, and every later edit to it is thrown away. renameFile only changes name on the entry in files. openFilenames and selectedFilename keep the old name, so the tab still shows it and Monaco keeps editing the old path. Each keystroke then reaches setSelectedFileContent, which calls updateFile(selectedFilename, { content }) with the old name. updateFile finds no file by that name, logs Cannot update file '<old>': file does not exist to the console and returns. Monaco keeps showing the typed text, but nothing reaches the store, so the preview stops updating and Save, Download and Share all use the content from before the rename. Renaming the index file also leaves indexFilename stale.

Where

apps/playground/src/store/slices/editor.slice.ts:56-58 and apps/playground/src/store/slices/editor.slice.ts:81-90:

renameFile: (currentName, updatedName) => {
  get().updateFile(currentName, { name: updatedName });
},
// ...
updateFile: (name, update) => {
  set((state) => {
    const index = state.files.findIndex((file) => file.name === name);
    if (index === -1) {
      console.error(`Cannot update file '${name}': file does not exist`);
      return;
    }

Reproduce

  1. In the playground, click Add File, name it helpers.ts and press Enter. It opens in a tab and is selected.
  2. Right-click helpers.ts in the file sidebar, choose Rename, change it to utils.ts and press Enter.
  3. Type export const answer = 42; in the editor.
  4. Click Download Archive and open the zip.

Actual: the tab still reads helpers.ts, the console logs Cannot update file 'helpers.ts': file does not exist once per keystroke, and utils.ts in the zip is empty.
Expected: the tab and selection follow the rename to utils.ts, and the zip's utils.ts contains the typed line.

Tests

apps/playground/src/store/slices/__tests__/editor.slice.test.ts: it('should move an open file's tab and selection to its new name, so edits after a rename reach the renamed file').

E2E in testing/src/specs/playground.spec.ts: add a file, rename it, type into it, and assert that the tab shows the new name and that the share link's decoded files contain the typed content.

Suggested fix

Make renameFile one set that renames the file, replaces the old name in openFilenames, moves selectedFilename when it was the old name, and recomputes indexFilename. Refuse a name that already exists with feedback the user can see, rather than only console.error.

Activity

  1. added
    BugType: existing behavior is wrong
    Priority: HighSignificant or frequently hit user impact; target the next release or two
    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). #1825 reworks closeFile and deleteFile in editor.slice.ts but leaves renameFile as it is.

    Gaps in the suggested fix

    • Recomputing indexFilename throws for a non-index name. resolveIndexFilename returns null only for an empty list (apps/playground/src/utils/file.ts:8-10, :42-44). Otherwise resolveIndexInput throws (packages/instrument-bundler/src/resolve.ts:16-18). So with the proposed recompute, renaming index.ts to main.ts throws inside the immer producer and the rename is dropped. The same throw already breaks addFile and addFiles (editor.slice.ts:16, :31) once the index file is deleted, which deleteFile allows. After that, adding or uploading files fails unless one of them is an index file. Make the lookup return null when no file is an index candidate. Put it in resolve.ts, with resolveIndexInput throwing on the null, so the extension priority stays in one place. That package is published and coverage-gated, so the new export needs its own tests there. Use the same lookup in deleteFile, which leaves indexFilename stale today. This can be its own commit, landed first.
    • Pressing Enter on an unchanged name calls renameFile(name, name) (EditorFileButton.tsx:68-72). That hits the "already exists" branch (editor.slice.ts:87-89), so with visible feedback it would warn on a no-op. Treat it as a no-op.
    • A refused rename leaves the rejected name in the sidebar. displayFilename resets only when the filename prop changes (EditorFileButton.tsx:46-48). The click-outside handler (:33-38) retries the rename on every click while the two differ, so a visible error would fire on every click. Reset displayFilename on refusal. Have renameFile return the reason, so the button can show a translated notification (useNotificationsStore, as DeleteInstrumentDialog.tsx does) without repeating the check.
    • Refuse a name without a known extension in renameFile, as Add File does in the UI (Editor.tsx:109-115). Today the update.name && guard (editor.slice.ts:87) skips the duplicate check for '', so a file can be renamed to an empty name.

    Monaco half

    The store fix alone fixes the reported case. Moving selectedFilename changes the editor's key (EditorPane.tsx:61, :138), and the model at the new path is created from store content. The old model is never disposed, though, and getOrCreateModel returns an existing model as-is, both in @monaco-editor/react 4.7.0 and in #1660's MonacoEditor.tsx. So rename helpers.ts to utils.ts, type, and rename it back: the editor reopens the stale helpers.ts model, and the next keystroke replaces the store's content with that stale text plus the keystroke. This is #1741's cause. #1741's fix covers it only if it disposes the model of every name that leaves files, not only on deleteFile.

    Order and 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: HighSignificant or frequently hit user impact; target the next release or two

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions