Skip to content

fix(studio): undo right after an edit undoes that edit, even before its save lands - #4807

Merged
miguel-heygen merged 8 commits into
mainfrom
studio/undo-waits-for-committed-edit
Oct 1, 2026
Merged

miguel-heygen merged 8 commits into
mainfrom
studio/undo-waits-for-committed-edit

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

What

Cmd+Z pressed right after an edit, before the edit's save has landed, now undoes that edit. Before, it could undo nothing or the edit before it, and the edit's save then landed after the undo and stayed. A resize or move undone that way left the element in a third state (size reverted, position not), and a nudge burst was never undone at all.

Why

Undo first waits for pending edits (waitForPendingDomEditSaves), but it only saw saves already queued in the DOM edit save queue or registered as pending edits. A canvas or panel edit counts from the moment its commit is called, and many commits spend a while before their first write: a resize parses the element's animations before it writes, and a nudge burst does not call its commit until 400 ms after the last arrow key. An undo in that window found nothing pending, stepped the history, and the save landed afterwards.

How

  • Every action that saves an edit, as the DOM edit context hands it to the canvas and the panels, registers its whole promise as a pending edit when it is called. Undo's drain already waits for pending edits, so it now waits for the edit on screen and then undoes it.
  • The context's action list says, for each action, whether it saves an edit. The context's type is built from that list, so a new action can't reach the canvas until someone marks it.
  • A nudge burst counts as a pending edit from its first arrow key until its save lands. So undo's instant paint stays off for the whole 400 ms wait for more keys, and can't show the edit before the burst undone. Undo's drain commits a burst still inside that wait and waits for its save.
  • A failed edit rolls itself back and shows its error; it never entered the history. Undo waits for every pending edit and queued save to settle, landed or failed, and never stops on a failure: one Cmd+Z after a failed edit undoes the last edit that did save, as in other editors. Other callers of the save drain (render, export) still stop on a failed save.
  • A burst always ends its pending edit, even when its commit throws, so a broken commit can't hold every later undo and export.
  • One tracker, trackedStudioEdit, counts an edit as pending from its call: the context's saving actions, timeline edits, the Delete and z-order hotkeys, and every GSAP script commit (so keyframe resets and deletes from the keyboard count too). The drain reports what happened, failures and conflicts alike, because the outside-change check needs them; only undo waits failures out. A conflict no longer cuts the drain short: it waits for the edits still running, then reports the conflict.

Measurement

Per-frame probe in the built Studio on the edit accuracy bench's gsap=none fixtures, px placement, no rotation, root, 100% zoom. Undo is pressed on the frame after release (or after the last arrow key), then the file and the screen are read 8 s later.

case main: file restored branch: file restored
resize no yes
nudge no yes
crop yes yes

The edit accuracy bench's sequence cases (gsap=none, 6 each), 3 jobs, all builds on base 3d8325e. "File checks" is every check except smoothness: tracking, drop, reload, undo.

Sequence main this PR this PR with #4857
nudge one key, undo on the next frame 0/6 6/6 6/6
move, nudge three keys, drag back 6/6 5/6 6/6
rotate, nudge 6/6 3/6 6/6

On main the nudge-then-undo cases fail drop, reload and undo: the nudge is never undone. This PR alone breaks some of the other two rows. The cause is #4857's bug: each save's temp file reaches Studio as an outside change, and its drain now commits a nudge burst early, splitting it into two saves. With #4857 all three rows pass; #4857 is merged and this PR is on top of it.

A real outside change that arrives mid-burst still commits the burst: the drain exists so a local edit lands before Studio accepts someone else's file, and holding the burst would let the outside reload wipe it or let it save over the newer file. Only the false outside changes were the bug, and #4857 removes them. Smoothness fails in most cases on every build on this machine, main included.

In CI the edit accuracy gate now passes the six nudge-then-undo cases, and the last commit banks them in baseline.json from that run.

Test plan

  • DomEditContext.test.tsx: an edit committed through the context actions, whose save has not written anything yet, holds undo's drain until it settles; an edit that fails does not fail the drain. The first fails when the action is not marked as saving, the second when the registration keeps the rejection.
  • useDomEditNudge.test.tsx: a burst counts as a pending edit from its first key until its save lands; undo's drain commits a burst still inside its wait and waits for its save. The first fails without the burst's registration, the second without the flush listener (the test runs on fake timers, so the burst's own 400 ms timer can't commit it). A commit that throws still ends the burst's pending edit; fails when that isn't in a finally.
  • useEditHistoryActions.paint.test.tsx, over the real history engine:
    • move, nudge, then undo inside the nudge's wait: the move never shows undone, and the undo ends with the nudge undone in the file and on screen. Fails without the burst's registration.
    • an undo pressed while a save fails in the real save queue undoes the edit before it, file and box alike. Fails when undo's wait stops on the failure.
    • an undo pressed while a tracked timeline edit fails still undoes the edit before it. Fails when undo stops on a failed edit.
  • useAppHotkeys.previewForwarding.test.tsx: Cmd+Z settles pending edits before it steps the history. Fails when the hotkeys skip the settle.
  • studioPendingEdits.test.ts: a drain that meets a conflict still waits for the edits that started while it ran (fails with the old early return); trackedStudioEdit counts a call until it settles and reports its failure.
  • useGsapScriptCommits.test.tsx: a GSAP script commit counts as pending from its call until it lands. Fails without the tracker.
  • Studio typecheck, oxlint, oxfmt; the full Studio suite.

Not covered by a test: App.tsx itself is not mounted in tests, so its wiring (the settle into the hotkeys, the tracked Delete and z-order) is checked by the typecheck and by name only.

Before

Undo pressed on the frame after a resize, with the save slowed to 1.5 s so the race is certain: the box ends in neither the original nor the resized state, and the file keeps the resize.

Before: early undo

After

The same undo waits for the resize's save and undoes it: the box is back where it started and the file matches.

After: early undo

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 852 passing here, 846 on the base branch

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

Newly passing (6)

  • seqnudgeundo-none-pct-r0-nested-z100
  • seqnudgeundo-none-px-r0-root-z100
  • seqnudgeundo-none-px-r0-nested-z100
  • seqnudgeundo-none-pct-r0-root-z100
  • seqnudgeundo-none-center-r0-root-z100
  • seqnudgeundo-none-center-r0-nested-z100

Quarantined, measured but not gated (2)

@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch 2 times, most recently from 57498a4 to ba5d8f9 Compare October 1, 2026 11:27
@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch 3 times, most recently from e7a3399 to 9bfedc8 Compare October 1, 2026 16:34
@miguel-heygen
miguel-heygen changed the base branch from main to fix/watcher-ignores-atomic-temp October 1, 2026 16:34
@miguel-heygen
miguel-heygen force-pushed the fix/watcher-ignores-atomic-temp branch from 47885aa to d989d5e Compare October 1, 2026 16:53
@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch from 9bfedc8 to 1bafb01 Compare October 1, 2026 16:58
This was referenced Oct 1, 2026
@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch from 4a4d750 to b16fca2 Compare October 1, 2026 18:41
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