feat: 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.
miguel-heygen
left a comment
There was a problem hiding this comment.
Requesting changes for two correctness issues I found on the current head. The previous producer/source-metadata and authored-translate blockers look fixed in the main preview/render paths, but these two paths can still produce incorrect user-visible behavior.
| } | ||
| if (!doc) return; | ||
|
|
||
| const element = findElementForSelection(doc, selection, selection.sourceFile); |
There was a problem hiding this comment.
Several new refresh/overlay paths call findElementForSelection with selection.sourceFile as the fallback active composition. In master view that makes root elements with no data-composition-file look like they belong to the nested source, so duplicate ids/classes can resolve to the root element after selection refresh or overlay lookup. I reproduced this on head 3eda023b with root #card plus nested scenes/nested.html #card: resolving the nested selection with selection.sourceFile returned the root card. These callers need the real active composition path, or null for master view, instead of the target source file.
| return `calc(${original} + ${rotationValue})`; | ||
| }; | ||
|
|
||
| const applyPathOffset = (element: HTMLElement, edit: Record<string, unknown>) => { |
There was a problem hiding this comment.
The thumbnail manifest applier has its own path offset logic and overwrites translate with only the Studio offset variables. The preview/render helpers now preserve authored/computed translate values, so thumbnails can show a different position from Studio preview and final render for manually moved elements that already had translate. The same helper also has a narrower source resolver at lines 100-107, so composition-file hosts without data-composition-id are handled differently from the shared manual edit helpers.
miguel-heygen
left a comment
There was a problem hiding this comment.
The two requested-change blockers are addressed on current head b2c6d64.
What I rechecked:
- Re-resolution now uses the real active composition path in App and DomEditOverlay, and the added duplicate-id master-view regression passes.
- Thumbnail manual edit application now preserves authored translate and resolves source-file hosts consistently with preview/render.
- Local targeted suites passed, Studio typecheck passed, and I browser-smoked a root/nested duplicate #card fixture with authored translate plus manifest offsets. The preview applied root and nested edits to the correct source elements, and the nested thumbnail route returned the cropped nested card without picking the root duplicate.
CI note: perf/preview checks were green when checked; several long regression shards were still pending.
b07b888 to
8b8dcf5
Compare
This stack of pull requests is managed by Graphite. Learn more about stacking. |
* fix: stabilize studio preview and runtime sync * fix: pass selector through timeline thumbnails * feat: add studio timeline editing * fix: disambiguate timeline edit targets * fix: stop timeline auto-scroll in fit mode * feat: use percentage-based timeline zoom * fix: sync timeline playhead on zoom changes * fix: reset timeline scroll when returning to fit * feat(studio): add manual DOM editing inspector * docs: update studio manual dom editing guide * feat(studio): add image asset picker for fills * feat(studio): add inline image uploads for fills * fix(studio): use real file input for image fill uploads * fix(studio): restore toast plumbing after rebase * fix(studio): explain in-app upload limitation * fix(studio): reuse asset-tab upload pattern in fills * feat(studio): refine manual design inspector * fix(studio): polish manual design inspector * fix(studio): keep color picker in viewport * fix(studio): clarify color picker selection * docs: update manual DOM editing guide * fix(studio): keep gradient color picker open * fix(studio): scope text color to text layers * fix(studio): add agent fallback for immovable layers * fix(studio): address manual editing review feedback * fix(studio): make local font selection reliable
Studio manual editing and timeline editing mutate project files directly, but those edits had no reliable undo/redo path. Before releasing manual editing, users need a way to recover from visual property changes, source-editor saves, timeline moves/resizes/deletes, and timeline asset drops. The history also needs to survive a page refresh. A refresh should not erase the only way back from a bad manual edit. - Adds a persistent per-project edit-history model for file snapshots. - Stores undo/redo stacks in IndexedDB so history survives Studio refreshes. - Records source editor saves, manual DOM edits, and timeline mutations. - Adds toolbar undo/redo buttons with standard keyboard shortcuts: `Cmd/Ctrl+Z`, `Cmd/Ctrl+Shift+Z`, and `Ctrl+Y`. - Validates current file hashes before applying undo/redo so external file changes do not silently overwrite newer content. - Keeps history available in memory if IndexedDB persistence fails during a session. - Adds focused unit coverage for the pure history model, storage adapter, controller/hook behavior, and project-file save helper. Studio previously treated every editor mutation as an immediate file write. Manual DOM editing, timeline updates, and source-editor saves each had separate write paths, so there was no common transaction boundary where Studio could capture the file contents before and after an edit. Undo/redo needed to sit above those write paths as a file-level transaction system: capture changed files before saving, write the new contents, persist the history entry by project, then apply undo/redo only when the current file content still matches the expected snapshot. - `bun --filter @hyperframes/studio test src/utils/editHistory.test.ts src/utils/editHistoryStorage.test.ts src/hooks/usePersistentEditHistory.test.ts src/utils/studioFileHistory.test.ts` -> 4 files pass, 15 tests pass - `bun --filter @hyperframes/studio test` -> 26 files pass, 289 tests pass - `bun --filter @hyperframes/studio typecheck` - `bunx oxlint packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/src/hooks/usePersistentEditHistory.ts packages/studio/src/hooks/usePersistentEditHistory.test.ts packages/studio/src/utils/editHistory.ts packages/studio/src/utils/editHistory.test.ts packages/studio/src/utils/editHistoryStorage.ts packages/studio/src/utils/editHistoryStorage.test.ts packages/studio/src/utils/studioFileHistory.ts packages/studio/src/utils/studioFileHistory.test.ts` -> 0 warnings, 0 errors - `bunx oxfmt --check packages/studio/src/App.tsx packages/studio/src/icons/SystemIcons.tsx packages/studio/src/hooks/usePersistentEditHistory.ts packages/studio/src/hooks/usePersistentEditHistory.test.ts packages/studio/src/utils/editHistory.ts packages/studio/src/utils/editHistory.test.ts packages/studio/src/utils/editHistoryStorage.ts packages/studio/src/utils/editHistoryStorage.test.ts packages/studio/src/utils/studioFileHistory.ts packages/studio/src/utils/studioFileHistory.test.ts` - `git diff --check` - `bun run --filter @hyperframes/core build:hyperframes-runtime` before commit hook, because the clean worktree needed the ignored runtime-inline artifact for typecheck - Lefthook pre-commit -> lint, format, typecheck pass - Lefthook commit-msg -> commitlint pass - Started Studio locally at `http://127.0.0.1:5190/#project/undo-redo-sample`. - Used `agent-browser` to select a preview element in the Inspector and change `#hero-card` from `left: 220px` to `left: 260px`. - Refreshed Studio and verified Undo stayed enabled. - Clicked Undo and verified the project file returned to `left: 220px`; clicked Redo and verified the inline `left: 260px` returned. - Used `agent-browser` to drag the `side-card` timeline clip, refreshed Studio, then verified Undo restored the previous timeline attributes and Redo reapplied the timeline move. - Recorded the tested undo/redo flow with `agent-browser`: `qa-artifacts/studio-undo-redo-2026-04-28/studio-undo-redo-flow.webm`. - Local screenshots and recordings are kept under `qa-artifacts/studio-undo-redo-2026-04-28/` and are intentionally not committed. - The scratch Studio project used for browser proof is local-only under `packages/studio/data/projects/undo-redo-sample/` and is intentionally not committed. - The PR intentionally excludes the earlier PRD/TDD planning notes under `docs/superpowers/`; those remain local-only per request.
ec63fd7 to
446c5e2
Compare
e0c88dd to
ce1de64
Compare
ce1de64 to
fd62a9d
Compare
This reverts commit d0abe90.

Summary
Studio manual geometry edits now persist as a project-local manifest instead of being baked into composition source on each gesture.
The manifest lives at:
It is the source of truth for manual drag, resize, rotation, inspector geometry edits, group moves, and selected-layer reset.
Architecture
path-offset,box-size,rotation), a source-scoped target, and the edit values.sourceFile,id,selector, andselectorIndex, so duplicate selectors in nested compositions resolve against the owning source file.translate, resize writes stable dimensions/flex sizing, and rotation uses CSSrotateover the authored base.User Impact
Users can move, resize, rotate, group-move, and reset supported layers from the canvas or inspector, then refresh, capture thumbnails/screenshots, play animated compositions, and render videos without manual edits drifting away from the edited state.
Main Files
packages/studio/src/components/editor/manualEdits.tspackages/studio/src/components/editor/DomEditOverlay.tsxpackages/studio/src/components/editor/PropertyPanel.tsxpackages/studio/src/App.tsxpackages/core/src/studio-api/helpers/manualEditsRenderScript.tspackages/studio/vite.config.tspackages/cli/src/server/studioServer.tspackages/core/src/compiler/htmlBundler.tspackages/producer/src/services/htmlCompiler.tspackages/core/src/studio-api/routes/thumbnail.tspackages/producer/src/services/fileServer.tspackages/producer/src/services/renderOrchestrator.tsTest Plan