Skip to content

chore: merge next into main#746

Closed
miguel-heygen wants to merge 94 commits into
mainfrom
chore/merge-next-to-main
Closed

chore: merge next into main#746
miguel-heygen wants to merge 94 commits into
mainfrom
chore/merge-next-to-main

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

Summary

Merges the next branch into main with all conflicts resolved, bringing 91 commits of Studio improvements, runtime enhancements, and producer refactoring.

Conflicts resolved (4 files)

File Resolution
core/runtime/init.ts Kept next's !tl null guard + main's mediaPreloader.preloadAroundTime() call
producer/renderOrchestrator.ts Took main's refactored stage architecture, added next's renderBodyScripts / additionalBodyScripts support
studio/CompositionsTab.tsx Took main's hover-activated iframe preview + static thumbnail fallback
studio/Player.tsx Kept next's stricter isPreviewMediaElement check (avoids false loading from error/no-source states)

Highlights

Version note

next is at 0.6.0-alpha.13, main at 0.5.7. Version bump to 0.6.0 should happen as a follow-up commit after merge.

Test plan

  • CI passes on the merge commit
  • Studio loads and renders compositions correctly
  • npx hyperframes render produces valid output

🤖 Generated with Claude Code

miguel-heygen and others added 30 commits May 10, 2026 18:35
* 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.
Studio frame capture could fail for projects mounted outside the repo when the project id came from an encoded hash route. A project like `Notion Showcase` loaded as `#project/Notion%20Showcase`, but the capture URL encoded that already-encoded value again, producing `/api/projects/Notion%2520Showcase/...` and a 404.

While validating the fix by seeking through the preview, capture also diverged from the visible player for nested compositions because the thumbnail route sought raw timelines instead of the same player seek path used by Studio preview.

- Decodes project ids when reading Studio `#project/...` routes and centralizes project hash/API path construction.
- Keeps API URLs encoded exactly once, including project names with spaces, literal `%`, reserved characters, and unicode.
- Updates Studio thumbnail capture to prefer `window.__player.seek(t)` and only fall back to raw timeline seeking for standalone pages.
- Preserves explicit `t=0` thumbnail requests instead of falling back to `0.5` seconds.
- Adds preview-regression CI coverage for Studio routing, frame capture URL construction, thumbnail seeking, and core thumbnail seek parsing.

Studio treated the hash route segment as the canonical project id even when the browser had already percent-encoded it. `buildFrameCaptureUrl` then encoded that string again, so a decoded project directory name and the capture API path no longer matched.

The preview/capture mismatch was a separate seek-path issue: the visible Studio preview seeks through the HyperFrames player, which maps global time into nested composition time. The capture route bypassed that layer and paused all registered timelines at the same global time.

The zero-second capture case came from parsing `t` with a truthiness fallback, so `parseFloat("0") || 0.5` became `0.5`.

- `bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.ts`
- `bun run --cwd packages/core test -- src/studio-api/routes/thumbnail.test.ts`
- `bunx oxfmt --check .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bunx oxlint .github/workflows/preview-regression.yml packages/studio/vite.thumbnail.ts packages/studio/vite.thumbnail.test.ts packages/studio/vite.config.ts packages/studio/src/utils/projectRouting.ts packages/studio/src/utils/projectRouting.test.ts packages/studio/src/utils/frameCapture.ts packages/studio/src/App.tsx packages/core/src/studio-api/routes/thumbnail.ts packages/core/src/studio-api/routes/thumbnail.test.ts`
- `bun run --cwd packages/studio typecheck`
- `bun run --cwd packages/core build:hyperframes-runtime`
- `bun run --cwd packages/core typecheck`
- `git diff --check`

Pre-commit also reran lint, format, and typecheck successfully for the committed files.

Using `agent-browser`, I mounted `/Users/miguel07code/Downloads/Notion Showcase` into Studio's project data and opened:

```text
http://127.0.0.1:5197/#project/Notion%20Showcase
```

Before the fix, Capture requested `/api/projects/Notion%2520Showcase/thumbnail/index.html?...` and Studio showed `Capture failed`.

After the fix, I sought the preview to `0s`, `2s`, `10s`, and `18s`, captured each frame, and compared the visible preview crop against the capture output. The capture URLs all used `Notion%20Showcase`, not `Notion%2520Showcase`, and no failure toast appeared.

Mean pixel diffs for preview vs capture were:

- `0s`: `0.0`
- `2s`: `0.8641`
- `10s`: `0.3496`
- `18s`: `0.2309`

The small non-zero diffs are raster/antialias-level differences after resizing the capture to the preview crop dimensions.

- Browser screenshots, comparison sheets, network logs, and the `agent-browser` recording are local-only under `qa-artifacts/capture-button/` and are not committed.
- The local Notion Showcase project mount is an ignored symlink under `packages/studio/data/projects/` and is not committed.
- Thumbnail cache versions were bumped so stale captures generated with the old seek behavior are not reused.
miguel-heygen and others added 17 commits May 11, 2026 10:37
- Move Text section to the top of the panel (before Layout)
- Remove Selection Colors section
- Rename "Blending" to "Transparency"
- Fix stroke Width/Style height mismatch by making SelectField
  use inline label layout matching MetricField
React registers onWheel passively, so preventDefault had no effect
on the parent scroll container. Replace with a native wheel listener
(passive: false) that blocks both default scroll and propagation.
…rtcuts

feat(studio): consolidate keyboard shortcuts into single handler + fixes alpha issues
fix(studio): clean next alpha inspector artifacts
…loop (#722)

Three bugs that compound in Studio preview:

1. **Double audio on pause/resume**: syncRuntimeMedia played audio through
   the HTML <audio> element while WebAudioTransport simultaneously played
   the same source through AudioBufferSourceNode. Fixed by passing
   webAudio.isActive() as outputMuted so HTML elements stay muted when
   Web Audio owns playback. Also removed the priorMuted restore in
   stopAll() which raced with the next play cycle.

2. **Manifest polling loop**: applyStudioManualEditsToPreview and
   applyStudioMotionToPreview unconditionally fetched from disk on every
   call, even without forceFromDisk. The runtime posts state messages
   every frame via postMessage, triggering React re-renders that re-invoked
   these functions ~60x/second. Fixed by returning early when no disk read
   is requested, and using refs instead of callbacks in useEffect deps.

3. **Parent proxy double-play**: the player web component created parent-frame
   audio proxies even when the runtime bridge was available, causing two
   audio sources on autoplay-blocked promotion. Fixed by skipping proxy
   creation when _hasRuntimeBridge returns true, and synchronously muting
   iframe media on promotion to close the async race window.

Also fixes pre-existing ResolutionPreset type missing square variants.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ching

Inspector picks invisible elements when an ancestor has GSAP-set opacity: 0
because CSS opacity is not inherited — getComputedStyle on the child still
returns 1. Walk the ancestor chain in the picker, domEditing, and overlay
visibility checks to catch this.

Also:
- Containers with all-invisible children are no longer selectable
- Selection/hover overlay hides during playback and while loading
- Undo/redo no longer double-refreshes (echo suppression for all file writes)
- Undo/redo reloads iframe in-place instead of recreating the Player,
  preserving shader transition cache
- Preview routes return ETag + Cache-Control headers; composition HTML uses
  project signature for conditional 304, binary assets use mtime+size
- Loading overlay deferred 400ms so cached loads never flash it
Remove the eye icon (inspector) and image icon (thumbnail toggle) from timeline
clips. The timeline layer inspector feature and all supporting code is removed.

Enable manual dragging in the preview by default. Add scrub-to-drag on X/Y/W/H
fields in the design panel. Hide the Radius section when the element has no
visible background. Fix pre-existing ResolutionPreset type for square presets.
- Line height and letter-spacing: convert from free-text to select with presets
- Font style: remove oblique (browser falls back to italic), keep normal/italic
- Font weight: detect available weights via document.fonts.check(), add labels
- Font source: local fonts matching Google catalog tagged as Google
- Font list: balanced per-source caps prevent any source from being cut off
- Sort order: Google fonts rank before Local so curated fonts appear first

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-caching

fix(studio): inspector visibility, undo/redo blinking, and preview caching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ad regression (#743)

- Add rotation (R) field to geometry row (X, Y, W, H, R) in property panel.
  Goes through manifest via handleDomRotationCommit, resettable with Reset Edits.
- Auto-promote display:inline elements to inline-block when dragged so
  translate works on inline spans.
- Fix regression from polling fix: iframe load now passes readFromDiskFirst
  to load manifest from disk, so Reset Edits finds existing entries.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Conflicts resolved in 4 files:

- core/runtime/init.ts: kept next's !tl guard + main's mediaPreloader call
- producer/renderOrchestrator.ts: took main's refactored stages, added next's
  renderBodyScripts/additionalBodyScripts support
- studio/CompositionsTab.tsx: took main's hover-activated preview + thumbnail
  fallback over next's always-on iframe
- studio/Player.tsx: kept next's stricter isPreviewMediaElement check over
  main's simpler preload===auto check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mintlify
Copy link
Copy Markdown

mintlify Bot commented May 12, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview May 12, 2026, 4:47 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

// Keep this heuristic conservative: if user source already loads GSAP, Studio does not add another copy.
return (
/<script\b[^>]*src=["'][^"']*gsap/i.test(html) ||
/\/\*\s*inlined:.*gsap/i.test(html) ||
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

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

Verdict: Request changes — this PR doesn't build.

The merge resolution covers the four files called out in the body, but it misses a wider semantic conflict that broke the producer build. CI is hard-red on Build, Typecheck, Test, and CLI smoke (required). The other conflict resolutions look reasonable.

James (jrusso1020) already approved; CodeQL flagged one ReDoS in preview.ts. The findings below are additive.

Blockers

  • blocker — missed merge conflict: rational-FPS rollback orphans new producer stages. packages/core/src/index.ts (and core.types.ts) drop Fps, FpsParseResult, parseFps, parseFpsWithDefault, fpsToNumber, fpsToFfmpegArg (the next branch reverted rational FPS back to plain number). But main introduced producer-stage modules that still import those symbols:

    • packages/producer/src/services/render/stages/probeStage.ts:37fpsToNumber
    • packages/producer/src/services/render/stages/extractVideosStage.ts:46fpsToNumber
    • packages/producer/src/services/render/stages/captureHdrStage.ts:68fpsToFfmpegArg, fpsToNumber
    • packages/producer/src/services/renderOrchestrator.ts:49fpsToNumber, type Fps

    esbuild fails with No matching export in "../core/src/index.ts" for import "fpsToNumber" (×5 in CI logs). The body lists 4 conflict files, but conceptually this is a fifth: the source-branch PRs (#725–737) that decomposed executeRenderJob into stages were written against the rational-FPS API on main, and the resolution didn't propagate next's rollback into the new stage files. Either keep the FPS exports in core or convert the new stages to plain number like the rest of next's consumers (cli/src/commands/render.ts, engine/src/services/frameCapture.ts).

    Why it matters: this isn't recoverable as a follow-up commit on main post-merge — anyone who pulls main between merge and the fix has a non-building monorepo. bun run build, the CLI smoke, and the player perf jobs all fail at the same gate.

  • blocker — required CI is failing. CLI smoke (required), Build, Typecheck, Test, Perf: * (load, fps, scrub, drift, parity), player-perf, multiple regression-shards, Windows render — all FAILURE on the latest commit (8436533). The Build/Typecheck/CLI-smoke failures all trace back to the FPS issue above; the perf/regression suite is harder to attribute without local repro but Test failing matters for a release merge. These need to be green before this can ship.

Important

  • important — version bump claim contradicts the diff. Body says: "Version bump to 0.6.0 should happen as a follow-up commit after merge." But the diff already bumps every package from 0.5.7 to 0.6.0-alpha.13 (inherited from next). Merging this as-is lands the alpha tag on main. Decide one way or the other before merge — either retag to 0.6.0 (stable) in this PR, or update the body so reviewers know main will publish as 0.6.0-alpha.13. The Slack message described this as "release merge for manual editing", which reads as a stable release, not an alpha cut.

  • important — rollback plan / forward-fix path. This is a 91-commit, +21,951/-2,041, 151-file merge. If anything regresses post-deploy, a clean revert of a single commit is the only viable rollback (no piecemeal). Worth confirming the merge will be a true merge commit (not squash) so git revert <merge-sha> -m 1 is on the table. Worth a one-liner in the body.

Nits

  • nit — STUDIO_MANUAL_EDITING_ENABLED defaults true. packages/studio/src/components/editor/manualEditingAvailability.ts:32 — for a feature being shipped as "manual editing alpha", a default-on flag is aggressive. MOTION_PANEL correctly defaults to false. If manual editing is alpha-quality, mirror the pattern. (Not blocking — this is the SDK, and Vite env wiring is the consumer's responsibility — but the asymmetry with the motion panel is worth a sanity check.)

  • nit — CodeQL ReDoS already flagged. packages/core/src/studio-api/routes/preview.ts:71/\/\*\s*inlined:.*gsap/i. The .* after inlined: on input the user can influence (HTML payload) is the polynomial-regex pattern. Anchoring with a length cap or rewriting to a non-greedy bounded match would silence it cleanly. Already raised by github-advanced-security — flagging here so it doesn't get lost behind the merge noise.

Praise

  • The three non-FPS conflict resolutions read well and the rationale in the body matches the diff: init.ts keeps the !tl null guard + mediaPreloader.preloadAroundTime() (defensible — neither side broke the other); Player.tsx's stricter isPreviewMediaElement + networkState/error checks are a meaningful improvement; CompositionsTab.tsx hover-iframe is cleanly bolted on.
  • App.tsx 4297 → 567 is a real architectural win. The new module boundaries under components/editor/ look sensible.
  • Feature-flag layer (manualEditingAvailability.ts) has the right shape — env-var driven, layered (inspector gates timeline-layer-inspector and preview-selection), with truthy/falsy normalization.

— Vai

miguel-heygen and others added 2 commits May 12, 2026 16:56
The auto-merge took next's core.types.ts which lacked the Fps interface,
fpsToNumber, fpsToFfmpegArg, parseFps, and parseFpsWithDefault that main
added for the producer stage refactor. Re-added the Fps block and its
exports to fix the build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Main's producer refactor changed fps from number to the Fps rational
interface throughout. The previous merge only restored the types but
left several producer files with the old number-based signatures.
Took the full set from main and re-applied next's renderBodyScripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

Closing — will revisit the next→main merge later.

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.

4 participants