Skip to content

fix(studio): a host can claim its writes under the key Studio's edit history uses - #5064

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/studio-history-claim-key
Oct 5, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/studio-history-claim-key

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

usePersistentEditHistory now also returns claimKey(key): the exact history key its recordEdit claims under for a given coalesceKey. recordEdit builds its own key through it, so the hook stays the one place that knows the format.

Why

Since #4937 every coalescing key is scoped to the page (<page id>:<key>), so a reload or a second tab never joins another page's step. A host that writes files itself (an import, a file replaced from disk) and claims them in the same project history, meaning to join the edit Studio saves next under the same key, had no way to learn the scoped key. Its claim stayed unscoped, so the host's write and Studio's save became two entries: one Cmd+Z took back the save and left the imported file behind.

With claimKey, the host claims under claimKey(key) and passes key to recordEdit, and both land in one entry. A host that calls recordEdit itself can already list its new files in created; claimKey is for a host whose backend writes and claims files without access to the hook.

No visible change

A hook gains one returned function; nothing on screen changes. A host that adopts claimKey gets one undo where it had two.

How I tested it

  • New test in usePersistentEditHistory.test.ts, over the real history routes and engine: a host claims a new file under claimKey(key), Studio saves under key, and one undo restores the composition and removes the file.
    • Fails on main (claimKey does not exist).
    • Fails when the host's key differs from the one recordEdit claims under (I changed claimKey to return the bare key while recordEdit kept the scoped one): the file survives the undo.
    • Passes 3 runs in a row with the change; the file's other 23 tests pass, including "a key one page holds does not join another page's edit under the same key".
  • tsc --noEmit for packages/studio, oxlint and oxfmt on both files.

Small on purpose: one returned function and its test.

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 5, 2026 10:26
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 2040 (base branch 2040), smooth 1624 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 (0)

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at 519b5aa3.

What it does: claimKey(key) returns ${pageKeyScope}:${key}, the same string recordEdit now builds through it. A host that claims its own write under claimKey(key) and then calls recordEdit with key joins one held claim, so one undo takes back both.

Checked

  • Tests: usePersistentEditHistory.test.ts passes 24/24, three runs in a row.
  • Mutation checks:
    • With claimKey returning the bare key while recordEdit keeps the scoped one, the new test fails.
    • With both unscoped, "a key one page holds does not join another page's edit under the same key" fails. So the page scope from #4937 is still enforced through claimKey.
  • Can a host's claim swallow a real outside edit and lose its undo? No. Joining a held claim (heldFor in projectHistory.ts) requires the same key, nothing pending elsewhere, and no held file changed since. I probed both cases over the real engine:
    • An outside write to another file between the host's claim and Studio's save: the result is three entries (host claim, "Changed outside the app", Studio's save), and three undos take them back in order.
    • The claimed clip.mp4 replaced outside the app before Studio's save: the first undo reverts index.html only, the second puts back the original clip ("media"), and the outside replacement stays its own step.
    • In both cases the join is refused rather than the outside change being misattributed. This PR changes only the key string, not the claim or join rules.
  • Reachable by a host: usePersistentEditHistory is exported from packages/studio/src/index.ts, and hosts compose it themselves (hostMount.test.tsx).
  • CI: 58 pass, 17 skipped, 0 failing at this head.

Reuse and simplification: this is the minimal shape. It returns the hook's own scope, so the key format stays in one place. Hosts that call recordEdit themselves already have created, so this only covers a backend that claims on its own.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 72d4cc3 Oct 5, 2026
85 checks passed
@miguel-heygen
miguel-heygen deleted the fix/studio-history-claim-key branch October 5, 2026 11:01
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.

2 participants