fix(studio): fold GSAP timing rewrites into the recorded history entry

A timeline move/resize recorded the timing patch, then a server GSAP rewrite mutated the
same file afterward, leaving the recorded after stale so an undo hit a hash conflict. The
GSAP mutation now snapshots the touched files and records a follow-up edit under the same
coalesceKey, with a per-entry coalesceMs override large enough to survive the GSAP round
trip, so undo restores the original in one step. Applies to single-clip and group edits.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-09 17:28:37 -04:00
parent 9cf575c6f9
commit 076c656d6e
5 changed files with 245 additions and 53 deletions
@@ -209,6 +209,67 @@ describe("edit history", () => {
expect(state.undo[0].files["index.html"].after).toBe("c");
});
it("folds a slow GSAP follow-up into the timing edit via a per-entry coalesceMs override", () => {
const timing = buildEditHistoryEntry({
projectId: "project-1",
label: "Resize timeline clip",
kind: "timeline",
coalesceKey: "timeline-resize:clip",
files: { "index.html": { before: "orig", after: "timing" } },
now: 0,
id: "timing",
});
// The server GSAP rewrite lands ~2s later, past the 300ms default window, but the
// follow-up carries a large coalesceMs so undo still collapses to a single step.
const gsap = buildEditHistoryEntry({
projectId: "project-1",
label: "Resize timeline clip",
kind: "timeline",
coalesceKey: "timeline-resize:clip",
coalesceMs: 10_000,
files: { "index.html": { before: "timing", after: "timing+gsap" } },
now: 2000,
id: "gsap",
});
const state = pushEditHistoryEntry(
pushEditHistoryEntry(createEmptyEditHistory(), timing),
gsap,
);
expect(state.undo).toHaveLength(1);
expect(state.undo[0].files["index.html"].before).toBe("orig");
expect(state.undo[0].files["index.html"].after).toBe("timing+gsap");
});
it("does not merge a slow follow-up without the coalesceMs override", () => {
const timing = buildEditHistoryEntry({
projectId: "project-1",
label: "Resize timeline clip",
kind: "timeline",
coalesceKey: "timeline-resize:clip",
files: { "index.html": { before: "orig", after: "timing" } },
now: 0,
id: "timing",
});
const late = buildEditHistoryEntry({
projectId: "project-1",
label: "Resize timeline clip",
kind: "timeline",
coalesceKey: "timeline-resize:clip",
files: { "index.html": { before: "timing", after: "timing+gsap" } },
now: 2000,
id: "late",
});
const state = pushEditHistoryEntry(
pushEditHistoryEntry(createEmptyEditHistory(), timing),
late,
);
expect(state.undo).toHaveLength(2);
});
it("coalesces entries with the same coalesceKey within the window (prop: format)", () => {
const first = buildEditHistoryEntry({
projectId: "project-1",