Persist Studio manual edits via manifest#593
Conversation
7bdc9b9 to
117f6ac
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
I would not merge this as-is. The manifest direction looks good, but I found two correctness issues that can make Studio preview diverge from render or lose authored inline state.
-
[P1] Preserve source metadata for rendered manual edits. Manual edits made inside a sub-composition are stored with
target.sourceFileset to that composition path, but producer compilation inlines the sub-composition and removesdata-composition-srcwithout preserving an equivalentdata-composition-filemarker. The render-time manifest runtime only resolves nested targets throughdata-composition-file/data-composition-srcand otherwise falls back toindex.html, so those manifest edits cannot match in the compiled render DOM. I reproduced this with a compiled-style DOM: a manifest rotation targetingsourceFile="compositions/scene.html"left the nested card unrotated. This means Studio preview and final render can diverge for drilled-in manual edits. The affected render path is aroundpackages/producer/src/services/htmlCompiler.tswheredata-composition-srcis removed during inline compilation, and the resolver is inpackages/studio/src/components/editor/manualEditsRenderScript.ts. -
[P2] Restore authored translate when clearing offsets. Path offsets overwrite the inline
translatelonghand, butclearStudioPathOffsetonly removestranslateinstead of restoring any authored inline translate value. Undoing or clearing a manual offset on an element that already hadstyle="translate: ..."leaves the Studio preview without the original translate until a full reload. I reproduced this by applying a manual path offset to an element withtranslate: 10px 20px, then applying an empty manifest;translatebecame empty. Seepackages/studio/src/components/editor/manualEdits.tsaroundclearStudioPathOffset.
Verification I ran locally on head 117f6acc: Studio focused tests passed, core thumbnail test passed, producer file server test passed. I also ran the two small reproduction snippets above; both reproduced the issues.
Summary
This PR changes Studio canvas geometry editing from direct source/timeline mutation to a manifest-backed edit layer.
Manual drag, resize, and rotation gestures now persist to
.hyperframes/studio-manual-edits.json. Studio then reapplies that manifest in the places where users expect the edited layout to show up: live preview, undo/redo, thumbnails, frame capture, and producer renders.The goal is to make visual canvas edits work mechanically without requiring the composition author or the model to pre-arrange every editable element with absolute positioning or GSAP variables.
Problem
The previous Studio manual-edit path was fragile for animated compositions:
This PR makes the manual visual edit itself explicit and project-local.
Approach
Manual edit manifest
Studio now stores manual canvas geometry edits in:
The manifest stores stable targets plus edit records:
{ "version": 1, "edits": [ { "kind": "path-offset", "target": { "sourceFile": "index.html", "selector": "#card", "id": "card" }, "x": 32, "y": 18 }, { "kind": "box-size", "target": { "sourceFile": "scenes/intro.html", "selector": ".title", "selectorIndex": 0 }, "width": 420, "height": 96 }, { "kind": "rotation", "target": { "sourceFile": "scenes/intro.html", "selector": ".badge", "selectorIndex": 1 }, "angle": -8.5 } ] }The target includes
sourceFile,id,selector, andselectorIndexso Studio can keep edits attached to the intended element across preview, thumbnail, and render contexts.CSS application model
Manifest edits are applied as a visual layer on the target element:
translatelonghand backed by--hf-studio-offset-xand--hf-studio-offset-y.width,height,min-*,max-*,box-sizing, and flex sizing overrides where needed.rotatelonghand backed by--hf-studio-rotation.This avoids rewriting GSAP timelines or requiring elements to be absolutely positioned. GSAP can still render its authored animation state, and Studio reapplies the manifest after seek/render passes.
Source-scoped target resolution
Manifest target lookup is now source-file scoped instead of global document scoped.
This matters because bundled previews can contain repeated IDs, classes, or selectors across scenes and nested compositions. Resolution now checks the owning
[data-composition-id]root and itsdata-composition-file/data-composition-srcmetadata before applying an edit.Covered cases include:
idvalues across source files.Preview and render reapplication
Studio preview now installs seek hooks so manifest edits are reapplied after timeline seeking:
window.__hf.seekwindow.__player.seekwindow.__player.renderSeekwindow.__timeline.seekwindow.__timelines[*].seekProducer renders also inject a small body runtime that applies the manifest and wraps render seek functions. The render wrapper uses a finite retry loop rather than an unbounded interval.
This addresses the class of bugs where an element appears correctly after dragging, but then jumps back or loses rotation/offset after a seek, scrub, capture, or render pass.
Undo/redo integration
Because the manual edit manifest is a normal project file, Studio's persistent edit history can record drag, resize, and rotation changes as file-level transactions.
Undo/redo no longer depends on click-away/blur timing for the manifest write path. Applying undo/redo for
.hyperframes/studio-manual-edits.jsonupdates the preview without a full source reload when possible.Removed old manual geometry path
This removes the old source-patching/manual-movable path for canvas geometry:
left/top/ transform changes during the gesture.User-facing behavior
After this PR, users should be able to:
Agent handoff model
The agent-facing handoff is now explicit:
.hyperframes/studio-manual-edits.json.This keeps user-made visual edits from being hidden in transient DOM state and reduces the chance that later agent edits accidentally collide with the user's Studio adjustments.
Main files changed
packages/studio/src/components/editor/manualEdits.tspackages/studio/src/components/editor/DomEditOverlay.tsxpackages/studio/src/App.tsxpackages/studio/src/components/editor/manualEditsRenderScript.tspackages/studio/vite.config.tspackages/core/src/studio-api/routes/thumbnail.tspackages/producer/src/services/fileServer.tsTest coverage added/updated
This PR adds or updates tests for:
Verification
Local checks run on the final restacked branch:
bun --bun run --filter @hyperframes/studio test \ src/components/editor/domEditing.test.ts \ src/components/editor/manualEdits.test.ts \ src/components/editor/DomEditOverlay.test.ts \ src/components/editor/manualEditsRenderScript.test.ts \ src/hooks/usePersistentEditHistory.test.ts \ src/player/hooks/useTimelinePlayer.test.ts \ src/utils/clipboard.test.tsResult: 7 files passed, 76 tests passed.
bun --bun run --filter @hyperframes/core test src/studio-api/routes/thumbnail.test.tsResult: 1 file passed, 6 tests passed.
bun test packages/producer/src/services/fileServer.test.tsResult: 23 tests passed.
Result: all passed.
Result: 0 warnings, 0 errors.
Result: all matched files use the correct format.
Result: passed.
Manual render smoke test from the branch:
manual-editing-four-scenes_2026-05-01_12-02-16.mp4.Notes for reviewers
testuses its regression harness, so the focused producer file server test was run directly withbun testbecause that test file importsbun:test.