Skip to content

feat: Persist Studio manual edits via manifest#593

Merged
vanceingalls merged 33 commits intomainfrom
feat/studio-manual-edit-manifest
May 4, 2026
Merged

feat: Persist Studio manual edits via manifest#593
vanceingalls merged 33 commits intomainfrom
feat/studio-manual-edit-manifest

Conversation

@vanceingalls
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls commented May 1, 2026

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:

.hyperframes/studio-manual-edits.json

It is the source of truth for manual drag, resize, rotation, inspector geometry edits, group moves, and selected-layer reset.

Architecture

  • Manifest-backed edits: each edit stores a kind (path-offset, box-size, rotation), a source-scoped target, and the edit values.
  • Source-scoped resolution: targets include sourceFile, id, selector, and selectorIndex, so duplicate selectors in nested compositions resolve against the owning source file.
  • Additive CSS layer: move uses CSS translate, resize writes stable dimensions/flex sizing, and rotation uses CSS rotate over the authored base.
  • Shared replay runtime: Studio preview, thumbnails, frame capture, producer renders, and CLI Studio renders/thumbnails all use the same core manual-edit render script.
  • Animation-safe replay: Studio reapplies the manual layer after load, refresh, timeline seeks, player operations, playback frames, thumbnail seeks, and render seeks instead of rewriting GSAP timelines.
  • History and handoff: the manifest is a normal project file, so undo/redo and agent edits can preserve, modify, or remove manual visual edits explicitly.

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.ts
  • packages/studio/src/components/editor/DomEditOverlay.tsx
  • packages/studio/src/components/editor/PropertyPanel.tsx
  • packages/studio/src/App.tsx
  • packages/core/src/studio-api/helpers/manualEditsRenderScript.ts
  • packages/studio/vite.config.ts
  • packages/cli/src/server/studioServer.ts
  • packages/core/src/compiler/htmlBundler.ts
  • packages/producer/src/services/htmlCompiler.ts
  • packages/core/src/studio-api/routes/thumbnail.ts
  • packages/producer/src/services/fileServer.ts
  • packages/producer/src/services/renderOrchestrator.ts

Test Plan

volta run --node 22.20.0 bun run build
volta run --node 22.20.0 bun run --filter @hyperframes/core test -- src/studio-api/helpers/manualEditsRenderScript.test.ts
volta run --node 22.20.0 bun run --filter @hyperframes/core typecheck
volta run --node 22.20.0 bun run --filter @hyperframes/studio typecheck
volta run --node 22.20.0 bun run --filter @hyperframes/cli typecheck
volta run --node 22.20.0 bunx oxlint <changed files>
volta run --node 22.20.0 bunx oxfmt --check <changed files>
git diff --check

@vanceingalls vanceingalls force-pushed the feat/studio-manual-edit-manifest branch from 7bdc9b9 to 117f6ac Compare May 1, 2026 19:22
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

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.

  1. [P1] Preserve source metadata for rendered manual edits. Manual edits made inside a sub-composition are stored with target.sourceFile set to that composition path, but producer compilation inlines the sub-composition and removes data-composition-src without preserving an equivalent data-composition-file marker. The render-time manifest runtime only resolves nested targets through data-composition-file / data-composition-src and otherwise falls back to index.html, so those manifest edits cannot match in the compiled render DOM. I reproduced this with a compiled-style DOM: a manifest rotation targeting sourceFile="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 around packages/producer/src/services/htmlCompiler.ts where data-composition-src is removed during inline compilation, and the resolver is in packages/studio/src/components/editor/manualEditsRenderScript.ts.

  2. [P2] Restore authored translate when clearing offsets. Path offsets overwrite the inline translate longhand, but clearStudioPathOffset only removes translate instead of restoring any authored inline translate value. Undoing or clearing a manual offset on an element that already had style="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 with translate: 10px 20px, then applying an empty manifest; translate became empty. See packages/studio/src/components/editor/manualEdits.ts around clearStudioPathOffset.

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.

@vanceingalls vanceingalls requested a review from miguel-heygen May 2, 2026 18:09
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

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.

Comment thread packages/studio/src/App.tsx Outdated
}
if (!doc) return;

const element = findElementForSelection(doc, selection, selection.sourceFile);
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.

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.

Comment thread packages/studio/vite.config.ts Outdated
return `calc(${original} + ${rotationValue})`;
};

const applyPathOffset = (element: HTMLElement, edit: Record<string, unknown>) => {
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.

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.

@vanceingalls vanceingalls requested a review from miguel-heygen May 2, 2026 18:39
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen left a comment

Choose a reason for hiding this comment

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

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.

@vanceingalls vanceingalls changed the base branch from next to graphite-base/593 May 3, 2026 20:21
@vanceingalls vanceingalls changed the base branch from graphite-base/593 to main May 3, 2026 20:21
Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

Comment thread packages/studio/src/components/editor/domEditing.ts Fixed
miguel-heygen and others added 18 commits May 3, 2026 19:29
* 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.
@vanceingalls vanceingalls force-pushed the feat/studio-manual-edit-manifest branch from ec63fd7 to 446c5e2 Compare May 4, 2026 02:33
@vanceingalls vanceingalls changed the title Persist Studio manual edits via manifest feat: Persist Studio manual edits via manifest May 4, 2026
@vanceingalls vanceingalls force-pushed the feat/studio-manual-edit-manifest branch from e0c88dd to ce1de64 Compare May 4, 2026 03:08
@vanceingalls vanceingalls force-pushed the feat/studio-manual-edit-manifest branch from ce1de64 to fd62a9d Compare May 4, 2026 03:13
@vanceingalls vanceingalls merged commit d0abe90 into main May 4, 2026
43 checks passed
@vanceingalls vanceingalls deleted the feat/studio-manual-edit-manifest branch May 4, 2026 06:06
miguel-heygen added a commit that referenced this pull request May 4, 2026
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.

3 participants