Skip to content

fix(studio): a refused edit names the file and why, on every edit path - #7749

Open
miguel-heygen wants to merge 2 commits into
mainfrom
dwinedit/refused-edit-words
Open

miguel-heygen wants to merge 2 commits into
mainfrom
dwinedit/refused-edit-words

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

What

When the server refuses an edit because the file is read-only or another program has it open, Studio now says so in words that name the file:

  • "index.html can't be changed: it is read-only or protected. Check its permissions, then try again."
  • "index.html is open in another program. Close it there, then try again."

Before, deleting a clip from a read-only file showed "Failed to delete 1 clip from index.html", although the server had answered why (403 {"why":"permission"}).

Why

The server answers a refused write 403 why: "permission" or 423 why: "busy" on every edit route (#7731). Studio only turned that into words on the paths that call createStudioSaveHttpError with the file's path: the save and the reads. The timeline delete, element removal, group and wrap edits, element patches, GSAP edits and their rollback, and composition inserts each wrote their own generic message, or called createStudioSaveHttpError without the path, which it needs to name the file.

Related work

Follows #7731 (the server's 403/423 answers).

How

  • studioSaveDiagnostics.ts keeps the only copy of the words. A new heldFileWordsOf(response, path) returns them for a refused answer, reading a copy of the body so the caller can still read it, and null for anything else.
  • Each edit call site asks it first and keeps its own message for every other failure: timeline delete, group commits, element patch, GSAP mutation and GSAP rollback, freeze frame, and the caption save (caption-overrides.json), whose banner now says why. Element removal and composition insert already used createStudioSaveHttpError and now pass the file's path.
  • Not changed: edits that can write several files, whose refusal does not say which file was held, so they keep their current words: the patch batch (patch-element-batches), the razor cut (split-batch), undo and redo, and block install. Creating, renaming and deleting files in the Files panel never replace an existing file's contents, so a read-only file does not refuse them. Checked by listing every POST, PUT, PATCH and DELETE in packages/studio/src.

Before

A clip deleted from a read-only index.html (the repo's timeline-trim-snap fixture, the file made read-only after it loaded):

Before: Failed to delete 1 clip from index.html

After

The same delete:

After: index.html can't be changed: it is read-only or protected

Test plan

  • Unit tests added/updated:
    • useTimelineDeleteOps.test.tsx: a delete the server refuses with 403 or 423 shows the words that name the file;
    • useDomEditCommitsHelpers.test.ts: a patch refused as read-only shows the same words;
    • useFreezeFrame.test.ts: a freeze-frame refused as read-only names the file;
    • useCaptionSync.test.ts (new): a refused caption save shows the read-only words, and any other refusal keeps "Caption changes couldn't be saved";
    • studioSaveDiagnostics.test.ts: heldFileWordsOf reads a copy and leaves the body for the caller, and is null for other answers.
  • fails without the fix: on main's code, the two delete cases showed "Failed to delete 1 clip from index.html", the patch case showed "Couldn't save edit: permission denied", and the helper did not exist.
  • With the change: the touched tests pass 3 runs in a row (65 tests); the full studio suite passes (757 files, 8,644 tests); tsc --noEmit, oxfmt and oxlint are clean on the changed files.
  • Manual testing performed: the captures above, from Studio's dev server in headless Chrome, main and this branch; the clip stays on the timeline in both.
  • Comments follow CONTRIBUTING.md "Comments".

@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 81 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@heygengenesis heygengenesis Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head f2ce4470

  • blocker: two single-file edit paths still drop the file name, though the PR says it covers every edit path and lists only the multi-file patch batch as left out.

    • packages/studio/src/hooks/useFreezeFrame.ts:78: a freeze-frame on a read-only file gets the 403 why: "permission" answer, because the /projects/:id/* held-file middleware (files.ts:2810) is registered before registerFreezeFrameRoutes. The toast still reads permission denied, or Freeze frame failed (423), with no file. input.path is in scope, so (await heldFileWordsOf(response, input.path)) ?? … before the json() read is enough.
    • packages/studio/src/captions/hooks/useCaptionSync.ts:106: the caption-overrides.json PUT throws HTTP 403 and shows Caption changes couldn't be saved. That is the same refusal this PR fixes elsewhere.

    Please fix both and add a test for the freeze-frame case, or name them under "Not changed" with a reason, as the PR does for the patch batch (the razor cut, split-batch, is multi-file too and could sit there with it).

Scope

Read the whole diff and listed every mutating studioApiFetch in packages/studio/src to check coverage. The razor cut (split-batch) is multi-file, like the patch batch, so the same exemption fits it. The 3 touched test files pass locally (36 tests). The fails-without-the-fix line is present.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Fixed in b34649f.

  • Freeze frame (useFreezeFrame.ts): asks heldFileWordsOf(response, input.path) before reading the body. New test in useFreezeFrame.test.ts: a 403 why: "permission" on scenes/intro.html throws "intro.html can't be changed: it is read-only or protected…". On main it threw "permission denied".
  • Caption save (useCaptionSync.ts): the banner now shows the held-file words for caption-overrides.json, and keeps "Caption changes couldn't be saved" for any other failure. New useCaptionSync.test.ts covers both. On main the read-only case showed the generic banner.
  • "Not changed" now names every multi-file edit with the reason: the patch batch, the razor cut (split-batch), undo and redo, and block install. I listed every POST, PUT, PATCH and DELETE in packages/studio/src to check coverage. The Files panel's create, rename and delete never replace an existing file's contents.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · f2ce447 → b34649f

Receipt

Freeze frame and the caption save now name the refused file. The PR now lists the remaining multi-file edits with reasons. With useFreezeFrame.ts and useCaptionSync.ts reverted to f2ce4470, the two new tests fail (2 of 6). At the head, the touched tests pass 42/42. All CI on b34649fb is green.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant