Commit Graph
5238 Commits
Author SHA1 Message Date
Miguel Ángel 2bee99f030 fix(studio): undo no longer snaps back a box you started dragging again (#5039)
* fix(studio): undo no longer snaps back a box you started dragging again

* fix(studio): undo reloads the preview after the drag ends, and the bench checks it

* test(studio): leave the edit bench sequence for the resize-then-drag fix

* fix(studio): the undo reload after a drag also refreshes the restored scenes
2026-10-05 02:02:08 +00:00
Miguel Ángel cd82ce044d perf(studio): drag and resize any number of selected clips, saved in one write per file (#4937)
* feat(studio): hand-move and hand-resize any number of selected clips

* test(studio): bench dragging and resizing every clip, frame cost and drop time

* test(studio): give the bench browser launch the run timeout

* test(studio): bench request timeline offsets

* fix(studio): joining or leaving a multi-clip drag restyles a clip instead of remounting it

* fix(studio): restore the linked-selection import and mark the drag bench complex

* test(studio): find a clip's track row through the per-clip wrapper

* test(studio): resolve track rows through the per-clip wrapper in the remaining cases

* perf(studio): write a group edit's GSAP changes as one request per file

* test(studio): cover the batched GSAP write and keep the owned-chain cases

* test(studio): bench profile of the drag's pointer moves

* perf(studio): sync a group edit's root duration once per file, not once per clip

* feat(studio): a timeline move registers a still-saving revert so Cmd+Z right after it need not wait

* test(studio): the reverted move stays reverted when its save lands

* test(studio): bench prints the heaviest call chain on request

* perf(studio): preflight a many-clip selection against one index of the live tweens

* test(studio): share one timeline across the indexed lookups

* test(studio): bench failure output skips resource 404 noise

* fix(studio): the live-tween index skips timeline entries that are not objects

* fix(studio): the live-tween index skips a null timeline entry

* test(studio): bench reports the first drag frame apart from the steady ones

* test(studio): bench lists inclusive time per function on request

* test(studio): bench reports when the last write of an action landed

* test(studio): bench reports medians of save, frame and first-frame times

* refactor(studio): move the live-tween index out of gsapRuntimeKeyframes to keep it under the line cap

* fix(studio): wire the live-tween index imports and drop an unused test binding

* style(studio): format the files CI flagged

* fix(studio): keep the bench and the scale helper out of fallow's findings

* refactor(studio): one exact tween-index lookup, finish a batch file once, reuse the duration sync

* fix(studio): the tween index lists a tween once per key

* fix(studio): restore the batch-finish and named-parameter helpers

* fix(studio): undo reverts an edit still saving before painting an older predicted step

* style(studio): format the batch duration sync

* chore(studio): keep the new comments within the block and share limits

* test(studio): settle the pending edit even when the undo-order test fails

* perf(studio-server): retime every clip in a GSAP batch with one parse and one hold sync

* test(studio-server): a batch of clip retimes writes what single requests in turn write

* style: format the retime fold and its tests

* perf(parsers): clip matchers in one retime share a DOM query cache

* perf(parsers): look up every clip id in one DOM walk when retiming

* perf(studio): re-resolving the same selected node keeps its source check

* perf(studio): a drop re-renders only the selection that actually changed

* test(studio): report main-thread task time of a bulk drop

* perf(studio): a group drop updates the timeline store once, not once per clip

* fix(studio): re-reading index.html keeps sub-composition keyframes

* refactor(studio): one owner for clip-retime no-op rules and selection options

* fix(studio): reuse a source check only for the same target, and re-check on a new cache version

* fix(studio): undo confirms a reverted save only once its history claim counted

* refactor: split a retime's application out of the one-parse fold; share test fixtures

* perf(studio): drop the per-clip wrapper node so timeline scrolling stays within budget

* refactor(studio): name the drag passenger style after the passenger it styles

* fix(studio): undo keeps a move undone when its history claim landed before the key

* fix(studio): undo a timeline move and its tween shift in one step

* refactor(studio): one owner of a timeline gesture's undo window

* refactor(studio): move single-clip move and resize into their own hook

* fix(studio): a reload or second tab never joins another page's undo step

* fix(studio): a delete and its ripple stay one undo step however late the ripple lands

* fix(studio-server): date a held undo step by its last write, not by the edit that ends it
2026-10-05 01:14:24 +00:00
Miguel Ángel ceaf90bf31 Merge pull request #5040 from heygen-com/release/v0.8.127
chore: release v0.8.127
v0.8.127
2026-10-04 18:04:45 -07:00
Miguel Angel Simon Sierra da1ac75718 chore: release v0.8.127 2026-10-04 17:25:23 -07:00
Miguel Ángel cd0d04e4e9 fix(studio): dragging a keyframe's path node keeps the other keyframes where they were (#4946) 2026-10-04 23:54:27 +00:00
Miguel Ángel e703c39cdd test(core): the HTML scanner's linear-work checks count work instead of timing it (#5037) 2026-10-04 23:34:11 +00:00
Miguel Ángel dc3665ab61 Merge pull request #5038 from heygen-com/release/v0.8.126
chore: release v0.8.126
v0.8.126
2026-10-04 16:14:22 -07:00
Miguel Ángel 1bdb5edd93 perf(studio): long clips mount only the picture tiles in view (#4993)
* perf(studio): long clips mount only the picture tiles in view

A clip's picture strip mounted one tile per frame width, so a 10-minute
clip at full zoom mounted 18,000 tiles. The strip-size hook now also
reports the strip's on-screen span (refreshed on scroll, resize and clip
moves), and the video, image and composition strips mount only the tiles
inside it.

* refactor(studio): read the strip span outside the state updater

* fix(studio): a dragged long clip keeps its picture across the screen

The strip refreshed its span only on scroll, resize or a style change on
its own clip, but a drag moves the ghost through a wrapper, so a long
clip's ghost went blank past about 600 px of drag. Each strip now marks
its unmounted ends and re-measures when either nears the screen,
whatever moved it, and commits before paint. The tile row is one shared
ThumbnailTiles for the video, image and composition strips.

* fix(studio): warn of a tile gap inside the timeline scroller too

* fix(studio): clip strips re-measure together, one layout per frame

With row virtualization off the timeline mounts every clip, and each
strip had its own scroll listener and frame that read layout and then
committed. A scroll jump changed every strip's span, so a 1,000-clip
timeline laid out about once per clip per frame (the viewport gate's
1,000-row arm went to ~3 s frames). All strips now share one scroll
listener, observers and frame: every position is read first, then every
update commits in one batch.

* perf(studio): a scroll re-renders only the clip strips crossing the screen edge

A strip's span is clamped to its own measured width, and each strip keeps
its current size outside React so an unchanged size never calls setState.
A clip wholly on or wholly off screen then does no work on a scroll; only
strips straddling the edge re-render.

* perf(studio): a scroll re-measures only clip strips near the screen

The shared frame read every mounted strip's position, so a timeline of
1,000 unvirtualized clips forced about 1,000 style-and-layout passes per
scroll frame. An IntersectionObserver now tracks which strips are within
256 px of the screen; a scroll re-measures only those, and a strip is
measured as it comes near. A clip wholly off screen mounts no tile.

* test(studio): share the strip test setup through the root harness

* test(studio): a strip is measured as it comes near the screen

* fix(studio): a scroll jump measures the clips it brings on screen in the same frame

Each frame moves every strip's last measured box by its scroller's offset
change and reads only the boxes that land within 256 px of the screen. A
jump therefore measures exactly the strips it brings into view, with no
blank frame, at the cost of a short scroll; strips far away get their span
from the moved box without a layout read.

* perf(studio): a strip off either side of the screen keeps one empty span

* perf(studio): a clip strip far above or below the screen mounts no tiles

* perf(studio): a scroll frame leaves off-screen clip strips untouched

* perf(studio): short clips keep every tile and do no work on a scroll

A strip up to 4,096 px wide keeps all its tiles, as before this change,
and a scroll frame skips it; only long clips mount the tiles in view.
Short clips gain nothing from virtualizing and paid a re-render each
time they crossed the screen's edge.

* fix(studio): a scroll is never applied twice to a clip strip's position

Each strip remembers the scroll offset its box was read at, so a strip read
between a scroll and that scroll's frame is not shifted again. The empty-span
guards are removed: a near strip always starts inside itself and a far one
already gets nothing.

* fix(studio): a clip on screen keeps its pictures after a move and a scroll

A strip showing tiles is re-read on every scroll frame instead of trusting
its last box shifted by the scroll, which a move without a scroll (rows
removed above it, a dock resize) leaves stale. Tests now pin the vertical
shift, the 256 px band, the 4,096 px short-clip limit and the tile padding.
2026-10-04 22:54:13 +00:00
Miguel Angel Simon Sierra a9787836e8 chore: release v0.8.126 2026-10-04 15:36:19 -07:00
Miguel Ángel f70dce70ed test(studio-server): pin how an older history log takes in the media ledger (#5021)
* test(studio-server): pin how an older history log takes in the media ledger

Four guards in the take-in had no test that failed without them: the start already naming the ledger, a change that made it, setting the keepsLedger mark, and saving it.

* test(studio-server): put the undoNewest note back on undoNewest
2026-10-04 22:17:26 +00:00
Miguel Ángel 00553e685c test(studio): the edit bench checks the keyframe rule outside a range and for new property groups (#5011)
* test(studio): the edit bench checks the keyframe rule outside a range and for new property groups

Adds playheads before a range that starts late and after every range to the
keyframed grid, and fails the keyframe rule when an edit makes the timeline
animate a property group (position, size, scale, rotation) on the target that
nothing animated before.

* test(studio): trim the keyframe-rule bench comments to the comment ratchet

* test(studio): split the bench's property-group reader into small steps

* test(studio): bank new keyframe guard cases and match any script tag casing

* test(studio): skip the same non-animated keys as the parser when grouping

* test(studio): bank the keyframe guard cases from this head's gate run
2026-10-04 22:14:32 +00:00
Miguel Ángel 16871a1796 fix: preview and bundled scripts run where and when the author put them (#5014)
* fix(core): bundled local scripts keep their own place, defer and async

The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.

* fix: preview scripts run after the runtime when the bundler fails, and inlined files are escaped

- When the bundler returns nothing or throws, the preview places the runtime
  where the bundled page does (end of head) instead of after the composition's
  scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
  and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.

* fix: deferred local scripts run late without data: URLs, and authored runtime reads survive

- Local defer/async scripts are inlined in their own tag with the attribute
  kept; the font-gate runner (and its old-runtime fallback) runs an inline
  classic defer script after the classic scripts, in document order, so a page
  policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
  names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
  one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
  unicode regex.

* fix: a deferred inline script waits for the deferred CDN script before it to load

* fix: an authored script that mentions the runtime marker no longer stops the runtime being added

* fix: only inlined deferred files run late, head ones included, and renders skip a CDN runtime link

* fix: deferred head files run as before, and one selector owns the deferred-file rule

* fix: a local file's legacy <!-- comment no longer breaks its merged run, and escapes keep case

* fix: a local file inlined on its own keeps working with a legacy <!-- comment

* test(core): a local non-JavaScript file keeps its text as written
2026-10-04 22:11:55 +00:00
Miguel Ángel b45818f69d fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own (#4957)
* fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own

useMotionPathData found the selected layer with `instanceof` against the preview
frame's HTMLElement. In the Studio preview the body's nodes often carry the editor
window's prototypes (see core's runtime/domRealm.ts), so the check failed, the path
never got its anchor and drew nothing. Core's realm-free isHtmlElement owns that
question; both motion-path call sites now use it.

* fix(studio): every preview node check asks what a node is, not which window built it

Studio checked preview nodes against their document window's own constructors in
seek reapply, studio motion, timeline z-order commits, the manual offset drag and
the z-order menu. On loads where body nodes carry the editor window's prototypes
those checks fail silently, as the motion path did. They now use core's realm-free
predicates, and a source test fails on a new window-bound check.

* fix(studio): the webmcp preview helper asks what a node is, and both realm tests stub the frame

* fix(studio): the last preview node checks ask what a node is, and the guard sees any constructor read

The selector walk read ancestor ids only from editor-realm nodes and the audio group
observer never saw a group the preview added. Both use core's predicates now. The guard
flags any DOM interface read off an object, the spellings it missed included, and the
window gates the old constructor needed are gone.

* test(studio): cover the main seek filter across windows and split member reads

* fix(studio): a press on the motion path's line inside the selected layer moves the layer

* fix(studio): a press inside the selected layer moves it, and path nodes sit where the layer is

* refactor(studio): one helper places a path point, for keyframes and the start

* fix(studio): a handle over a path node takes its press; nodes sit right on centred layers

* fix(studio): reading a layer's path anchor leaves its CSS translate alone

* chore(studio): keep the motion path home file under the comment share

* fix(studio): a new motion path ends where you click on a CSS-centred layer

* fix(studio): a path made on a transform-centred layer ends where you click

* style(studio): keep a motion path test comment within 120 columns
2026-10-04 22:08:29 +00:00
Miguel Ángel 107d4d980c Merge pull request #5031 from heygen-com/release/v0.8.125
chore: release v0.8.125
v0.8.125
2026-10-04 13:55:17 -07:00
Miguel Angel Simon Sierra b6c89b5583 chore: release v0.8.125 2026-10-04 13:35:42 -07:00
Miguel Ángel 49333a882d fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves (#5022)
* fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves

* test(studio): pin the painted-back draw and redraw rules

* test(studio): the resize intercept also gets the draw that keeps an undone edit undone

* fix(studio): a scale resize reads its drop point on the box as dropped, even after undo

* fix(studio): a group move and a resize settle keep an undone edit undone

* fix(studio): undo paints back a GSAP move saved as a left/top offset

* fix(studio): undo still steps a GSAP edit whose first write saved before a later one failed

* fix(studio): the resize pixel check measures a box undo painted back as the edit drew it

* fix(studio): a GSAP move undone mid-save keeps its drag base, so its save adds the drag once

* refactor(studio): trim the scale-resize finalize comments to what the code cannot say
2026-10-04 20:13:06 +00:00
Miguel Ángel 99c389bce9 fix(core): a cropped element's edge stays exact while GSAP tweens it (#5010)
* fix(core): a cropped element's edge stays exact while GSAP tweens it

* fix(core): configure GSAP while the runtime script runs, before compositions set elements

* test(studio): bank the six crop-scale cases that now pass

* test(producer): regenerate the png-sequence and mov-prores goldens for 2D GSAP transforms

* test(core): a non-configurable window.gsap is configured without a crash

* test(studio): re-bank the edit accuracy baseline from this branch's current head
2026-10-04 20:12:45 +00:00
miga-heygenandMiguel Ángel 0c3e244269 docs(skills): fix wrong facts and broken paths from the doctrine audit (#5026)
Section 2 of #5025, each checked against current main:
- studio: sub-compositions.md lives in /hyperframes-core
- music-to-video: bgm.md path is audio/references/bgm.md in /media-use
- remotion-to-hyperframes: when in doubt, return to /hyperframes
- registry: the search-miss report passes the tier that answered
- talking-head-recut: layout files carry bounds only, the storyboard records
  card.zone; the field is contentHints; style varies only inside the chosen
  group
- embedded-captions: name the real transcription fallback
- motion-graphics: drop the nonexistent resolve.mjs block, list maps
- animation, creative, figma, slideshow: resolve bundled paths from the
  installed skill, not the repo root

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-04 12:27:35 -07:00
miga-heygenandMiguel Ángel 173103dfde docs(skills): resolve cross-skill contradictions from the doctrine audit (#5028)
* docs(skills): resolve cross-skill contradictions from the doctrine audit

Section 1 of #5025, each direction taken from the code or the shared
contract:
- async timeline build is supported (runtime rebinds an async-registered
  timeline; lint only flags registering before the build finishes), so
  animation and keyframes now say register after the build completes
- data-track-index is a Studio row, display only; layering is z-index
- the final gate is `check`, which reruns lint
- music-to-video keeps render approval as the one autonomous question,
  per the brief contract
- faceless-explainer and product-launch-video pre-install registry blocks
  before dispatch, like pr-to-video
- talking-head-recut layout files cover landscape and portrait; 4:5 is
  derived
- embedded-captions Cinematic authors cinematic.json; make-cinematic.cjs
  derives plan.json
- audio carve has no static mode; remove the stale static and dynamic text

Left open: the figma usage beacon and the skills-update consent rule.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* chore(skills): refresh manifest; treat cinematic.json and safe-zones.json as project files

Both are written into the user's project by the embedded-captions workflow,
so the comment-citation check should not look for them in the repo.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-04 12:14:30 -07:00
Miguel Ángel a4a7cbeac8 Merge pull request #5023 from heygen-com/release/v0.8.124
chore: release v0.8.124
v0.8.124
2026-10-04 10:24:41 -07:00
Miguel Angel Simon Sierra 706118aa1a chore: release v0.8.124 2026-10-04 10:06:35 -07:00
Miguel Ángel a2f4160899 fix(studio-server): undoing a change takes the media ledger back with the film (#5016)
* fix(studio-server): undoing a change takes the media ledger back with the film

* fix(studio-server): a log 0.8.123 wrote takes the media ledger in, so the first Undo undoes the edit

* fix(studio-server): only a log from before the ledger fix takes it in, once

* fix(studio-server): an older log takes in the media ledger only as the sweep would see it

A ledger behind a link was read straight into the baseline, then filed as a
deletion by the first sweep. The take-in now asks the same scan the sweep uses.
The cutout test also pins that other .media files and other manifest.jsonl
names stay out of history.
2026-10-04 16:33:56 +00:00
WaterrrForever 2b82e39cd6 feat(studio): make edit with framey stand out in the header (#5019)
Wenbo's feedback: the button read like the ghost controls beside it.
It now wears an accent tint and border, a larger Framey and a permanent
New badge, and pulses three times as Studio opens (none under reduced
motion).
2026-10-04 15:57:34 +00:00
Miguel Ángel dcc721062b fix(cli): a project .env cannot set the feedback email in another letter case (#5018)
* fix(cli): a project .env cannot set the feedback email in another letter case

* fix(cli): capture loads a project .env through the same rules as the CLI
2026-10-04 15:54:34 +00:00
Miguel Ángel 2497402b27 feat(media-use): clone, list and delete HeyGen voices, and accept a host OAuth token (#4992)
* feat(media-use): clone, list and delete HeyGen voices, and accept a host OAuth token

heygen-voice.mjs clones a voice from an mp3 or wav file, lists cloned voices by name prefix, and deletes one, printing HeyGen's own error message on any refusal. HEYGEN_ACCESS_TOKEN is now the first credential source and is sent as an OAuth Bearer token.

* fix(media-use): heygen-tts prints HeyGen's own refusal instead of a generic line

* chore(media-use): regenerate the skills manifest for the voice scripts

* fix(media-use): voice list stops on a repeated page, delete accepts an empty reply, plain refusals

* test(media-use): a voice list that pages forever fails at once instead of hanging

* docs(media-use): keep heygen-voice's header to three lines, usage lives in tts.md
2026-10-04 15:07:30 +00:00
Miguel Ángel ed5c1000b1 fix(studio): a video's sound reads on its clip like an audio row (#5015)
* fix(studio): a video's sound strip reads like an audio row, at half the clip

* fix(studio): the peak badge on a video's sound strip stays whole and readable

* fix(studio): the strip's peak mark stays above the waveform
2026-10-04 14:56:22 +00:00
Miguel Ángel 48bfe61803 fix(studio): a snap the saved clip would miss no longer moves it (#5006)
* fix(studio): a snap the saved clip would miss no longer moves it

* test(studio): walk that a deep-zoom move lands where it shows, with no jump on release

* test(studio): the release walk waits for the saved start, not the drag's own

* test(studio): find the drag landing slot by its test id
2026-10-04 14:25:37 +00:00
Miguel Ángel 5cee25efcb fix(studio-server): motion paths animate when gsap has a query string or loads deferred (#5007)
* fix(studio-server): motion paths animate when gsap has a query string or loads deferred

Studio puts MotionPathPlugin right after the composition gsap tag. A gsap URL with a query string or hash was not recognised, so the plugin went to the head and ran before gsap; a deferred gsap kept the plugin undeferred, with the same result. The tag is now found with a query or hash, and the plugin copies its type and defer, read with the HTML parser the server already uses.

* fix(studio-server): finding the gsap tag stays fast on any preview HTML

The gsap tag pattern from the previous commit could backtrack polynomially on long runs of gsap-like src text. The preview now walks script tags with index searches, decides gsap core from the URL path, reads attributes case-insensitively, and skips tags inside comments and templates.

* refactor(studio-server): the gsap tag scan is small single-purpose steps with no whitespace regexes

Splits the scan so each step has one job (walk tags, read one script tag, find its closing tag, skip a comment or template), and skips whitespace by character instead of with \s* patterns, which code scanning reads as polynomial.

* fix(studio-server): the gsap tag search reuses the core HTML scanner, so style blocks and odd comments cannot hide gsap

The hand-written walk missed raw-text bodies, empty comments and a comment opener inside an attribute value; any of those before the gsap tag sent the plugin to the head again. findStartTags already skips all of them and is what lazyPreviewImages pairs with the parsed document; the preview now does the same.
2026-10-04 14:25:09 +00:00
Miguel Ángel 4a5e38eed6 fix(studio): an audio clip's name pill draws over its waveform (#5013)
* fix(studio): an audio clip's name pill draws over its waveform

* fix(studio): the out-of-sync badge stays over the audio pill; one owner for the waveform layer

* test(studio): the filmstrip tests reset the timeline in afterEach
2026-10-04 14:05:38 +00:00
Miguel Ángel 399f49104e fix(studio): a first animation in a full-document sub-composition plays (#5008)
* fix(studio): a first animation in a full-document sub-composition plays

* fix(studio): keep the comment share of the two touched files at their base

* fix(studio): a first animation also plays when <html> carries the composition id

* fix(studio): a root whose id sits on <html> keeps its first animation in <body>

* fix(studio): a composition id on the template tag keeps the first animation inside it
2026-10-04 14:05:18 +00:00
WaterrrForever 69a2169bde feat(skills): open Studio before every final render, and pitch the desktop app as the chat way to edit (#4952)
* feat(skills): tell the user about HyperFrames Studio at the final preview and after delivery

/hyperframes gains § 6: mention the preview header's Edit with Framey button when the final
Studio preview opens, and after the delivery render pass on the render's own desktop-app line
in plain words — offer to run `hyperframes open` when the app is installed, else give the
download link. Say nothing when the render prints no such line (a draft, a batch row, a run
inside the app). Builds on the CLI lines added in #4951.

* feat(skills): open studio before every final render, pitch the app as the chat way to edit

Users who only ask for a video never saw Studio: autonomous runs asked "preview
first, or render?" and a "you decide" answer rendered. The final look now opens the
Studio preview in every mode, then asks "render now, or what changes?" — opening it
asks nothing, so it also holds when the user said not to ask.

Section 6 says what is true: the Studio preview already edits and saves; the desktop
app adds editing by chat with Framey. The agent names it "the HyperFrames Studio
desktop app" so it is not mistaken for the preview. general-video, the review loop
and the brief contract carry the same final look.

* fix(skills): call the app the hyperframes desktop app, never studio

* fix(skills): pitch edit with framey only when preview shows it, keep direct renders direct

* fix(skills): mention the button only after the line that ships with it
2026-10-04 12:55:56 +00:00
WaterrrForever 6a27a34f21 feat(cli,studio): hand off on macOS now, and open the app on Windows and Linux (#4999)
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio

Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.

- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
  follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
  handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
  The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
  so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
  then Canary) and points to the download when neither is installed. When run by Claude Code
  or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
  so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
  `hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
  run inside the app (HYPERFRAMES_DESKTOP_PROJECT).

* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate

Review of #4951 at 58934a34.

- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
  `handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
  `handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
  answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
  Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
  server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
  directory, and tells a missing app from one macOS could not open (`open-failed`). A project
  that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
  and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.

* feat(cli,studio): hand off on macos now, and open the app on windows and linux

The released Mac app (b254) takes a handed-over folder and its conversation, so
HANDOFF_READY turns on for macOS. Windows and Linux stay on the download until the
desktop change that reads a folder from the command line ships.

- `hyperframes open` finds the app off macOS: the per-user install on Windows, and
  on Linux the AppImage the app records in launcher.json; it starts it with the folder
- download links follow the machine: the DMG on macOS, the AppImage on Linux, and
  nothing on Windows, which has no public download yet
- the Studio button shows wherever there is a download or an installed app; the card
  no longer says "for macOS"
- every render prints the app line except batch rows: music-to-video delivers a draft
- the success line names the app that took the folder (Canary included)

* fix(studio): the edit-with-framey button asks its route through studioApiFetch

* fix(cli,studio): name the app the hyperframes desktop app, apart from the studio preview

* fix(cli,studio): fewer comment lines, so no file's comment share rises
2026-10-04 12:54:17 +00:00
Miguel Ángel f87dc5fa0d Merge pull request #5012 from heygen-com/release/v0.8.123
chore: release v0.8.123
v0.8.123
2026-10-04 05:18:12 -07:00
Miguel Angel Simon Sierra 638e6b7b33 chore: release v0.8.123 2026-10-04 05:06:49 -07:00
Miguel Ángel cad3db47d2 fix(studio-server): history never files a hidden file as a change (#4966)
* fix(studio-server): history never files a hidden file as a change

* fix(studio-server): an older log never makes history write a hidden file

* fix(studio-server): an older log's hidden-file entry no longer stalls Undo

* fix(studio-server): an older log's emptied Undo still keeps its target undone

* fix(studio-server): an older log keeps every entry, and undoing an emptied one still lands

* refactor(studio-server): history's hidden-path rule says itself in names, not comments
2026-10-04 11:44:50 +00:00
Miguel Ángel 805cf60b03 fix(core): the preview redraw leaves animations after the playhead as the seek drew them (#4997)
* fix(core): the preview redraw leaves animations after the playhead as the seek drew them

The redraw that re-renders a timeline at its playhead steps silently to just
below it and back. That backward step drew every reversed child that starts
later at its end (a reversed set showed 0.8 where the seek showed 0.2), and the
landing never revisited it. Children starting after the playhead now sit out
both steps. The redraw also puts back the zero-duration state of every
zero-duration child, not only those with callbacks, so two sets on the playhead
keep their authored order on the next seek and a call in an otherwise empty
nested timeline fires as often as without the redraw.

* refactor(core): collect children after the playhead without a per-child array

* fix(core): a scene's opening set disappears when you step back off the scene's start

The redraw's silent round trip left a nested timeline's start-crossing mark
(_zTime) on the wrong side, so the next backward seek skipped the scene and its
opening set stayed drawn. The redraw now restores that mark for every nested
timeline as well as every zero-duration child.

* refactor(core): name the redraw's playhead tolerance instead of commenting it

* fix(core): a pause inside a reversed repeating scene fires once after a redraw

The redraw renders a nested timeline the seek had left inactive, which marks it
active. With its start-crossing mark restored but not its active flag, the next
seek re-ran the scene's pause. The redraw now restores the active flag too.

* fix(core): a held redraw no longer cuts a paused scene short in renders

Holding later children at time scale 0 let GSAP recompute a nested timeline's
duration during the redraw's round trip without them, so a paused
sub-composition kept a shortened length and froze for the rest of the render
(style-5-prod's typed intro). After restoring the time scales, every ancestor
of a held child is marked dirty and the duration is settled again.

* fix(core): the redraw puts back the scene lengths it measured, not re-measures them

Re-settling after the hold recomputed the root's length from whatever was paused
at that moment, which the redraw should not change. The cached lengths of the
held children's ancestors are now snapshotted after the entry settle and
restored in the finally.
2026-10-04 11:17:41 +00:00
WaterrrForever bd0dc8c3ae feat(studio,cli): add an Edit with Framey button and hyperframes open for HyperFrames Studio (#4951)
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio

Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.

- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
  follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
  handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
  The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
  so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
  then Canary) and points to the download when neither is installed. When run by Claude Code
  or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
  so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
  `hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
  run inside the app (HYPERFRAMES_DESKTOP_PROJECT).

* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate

Review of #4951 at 58934a34.

- One gate: HANDOFF_READY lives only in the CLI. `GET /api/open-in-desktop` now says
  `handoff`, and Studio's button reads it, so the gate cannot be half-flipped.
- While gated nothing opens and nothing is written: `openInDesktop` returns
  `handoff-unavailable` (so `hyperframes open` prints the download line and the POST route
  answers with it), and no `.hyperframes/agent-handoff.json` can sit waiting for a later release.
- The POST answers only Studio's own same-origin request (loopback Host, matching Origin,
  Sec-Fetch-Site same-origin); a cross-site or rebound-host POST gets 403. Routes moved to
  server/desktopRoutes.ts with their own tests.
- `hyperframes open`: exits 1 when nothing opened, answers in JSON under --json for a bad
  directory, and tells a missing app from one macOS could not open (`open-failed`). A project
  that cannot be written still opens, with nothing handed over.
- Tests drive the live path through `ready`, are path-portable for Windows, and cover the draft
  and batch-row hint skips (`wantsDesktopHint`, `batchRowRenderOptions`).
- The load smoke mocks the route.

* fix(studio): the edit-with-framey button asks its route through studioApiFetch
2026-10-04 10:52:23 +00:00
Miguel Ángel 6037d22844 Merge pull request #5009 from heygen-com/release/v0.8.122
chore: release v0.8.122
v0.8.122
2026-10-04 04:03:15 -07:00
Miguel Ángel 7faf2cc6c3 fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node (#4941)
* fix(studio): a move on a keyframe of an eased tween changes that keyframe in place

GSAP eases a keyframe run as a whole with keyframes.ease || ease. The playhead writer
now edits an existing keyframe under that ease and writes the ease back as the
tween's own; adding a keyframe in eased form stays refused, since the ease moves
where its time lands.

* fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node

The motion path drew an axis the tween does not animate at 0, though GSAP folds the
layer's CSS translate into it, so each node sat beside the layer and its grab ring took
presses aimed at the layer. The path now uses GSAP's live position, the same base the
layer drag reads. A press on the node GSAP renders the layer at, or inside the layer's
box but off another node's drawn dot, starts the layer's own move and write.

* test(studio): stub elementsFromPoint, which happy-dom does not have

* refactor(studio): name the press check for what it answers, and format

* chore(studio): cut the helper comments to the package comment share

* fix(studio): a node press goes to the layer only when the layer takes it

The hand-off to the layer's box falls back to the node drag when the box starts no
gesture or takes no pointer, and a selected node's larger dot stays the node's.

* chore(studio): name the keyframe run ease instead of commenting it

* refactor(studio): share the keyframe-at-percentage check in the playhead writer

* fix(parsers): a keyframed tween keeps GSAP's ease, never a default or a quoted variable

GSAP never gives a tween with keyframes its timeline's default ease, so the parser
no longer does. A keyframes-level ease or easeEach the file names by variable is kept
as that variable, and every ease the writer emits goes through valueToCode, so
ease: E is written back as E instead of the string "__raw:E".

* fix(studio): edit an eased keyframe run only at its ends, and a refused press is not a node drag

GSAP renders a run eased as a whole at ease(t), so only its 0% and 100% keyframes sit
at their clock time; the writer refuses the others as before. A layer press the box
answers with its blocked notice counts as handled, so the node does not drag after it.

* fix(studio): a layer press refused at setup leaves the box live, and variable eases stay pinned

startGesture paused the box's position updates and playback before the drag member
setup could refuse; a press handed off from a path node then left the box frozen, since
its release lands outside the box. It now pauses only once the setup succeeds. Tests pin
that adding, converting and unrolling write an ease the file names by variable as that variable.

* fix(studio): pause playback before drag setup and pin variable eases on convert and unroll

* fix(studio): pause playback before a nudge snapshots timelines, and pin the order

* test(studio): a failed nudge pause test cleans up, and a nudge with nothing to move leaves playback alone
2026-10-04 10:44:48 +00:00
Miguel Angel Simon Sierra b7baa5fde1 chore: release v0.8.122 2026-10-04 03:39:06 -07:00
Miguel Ángel 981bcf4553 fix(media-use): say to fix an unreadable credentials path, not to log in again (#4998)
* fix(media-use): say to fix an unreadable credentials path, not to log in again

* fix(media-use): provider=heygen gives the same fix hint as the auth headers

* test(media-use): restore env in the pickProvider test with a plain if
2026-10-04 10:25:47 +00:00
Miguel Ángel 44b5836250 fix(core): composition scripts start after web fonts are ready, so renders repeat (#4980)
* fix(core): composition scripts start after web fonts are ready

A scene script that measures text (offsetHeight, getBoundingClientRect) ran while
the page parsed, often before its web font loaded, so it laid the scene out with
fallback metrics. Renders of the same film differed run to run, and preview could
differ from export.

The compilers now give every body script a type the browser does not run
(text/hf-after-fonts, or +module), through one helper in scriptRuns.ts called
at the end of the bundler, the producer compile and the sub-composition preview.
The Studio motion script is emitted the same way so it still runs after them.

The runtime entry owns the wait: on DOMContentLoaded it forces a layout, waits for
document.fonts.ready or FONT_WAIT_TIMEOUT_MS (5 s), then re-creates each script
in its own place in parser order (classic in order, awaiting src loads; then
module and defer scripts in insertion order), boots as before, and calls the
DOMContentLoaded and load listeners the scripts added. On timeout the scripts
run anyway and runtime_font_wait_timeout names the families still loading.
Scene swaps and the composition loader use the same wait.

* fix(core): a page whose runtime predates the font gate still runs its scripts

A bundle can be served with a runtime loaded from HYPERFRAME_RUNTIME_URL. If that
runtime is older than the font gate, nothing would run the deferred composition
scripts and the film would be blank.

Whenever the marking helper defers scripts, it also puts one small inline script
first in the head. At DOMContentLoaded it runs the deferred scripts in parser
order itself, without waiting for fonts and with a console warning, unless a
runtime has claimed them. The gate's runtime claims them when it loads, before
DOMContentLoaded, so the scripts run exactly once either way.

* fix(studio-server): motion paths animate in Studio preview when gsap loads in the body

The MotionPathPlugin tag Studio inserts after a body gsap script now carries
that script's type, so the runtime runs it right after gsap once fonts are
ready, instead of before gsap exists.

* fix(cli): layout, motion-shot and validate wait for the composition scripts to run

The three commands waited on the timeline registry, which exists as soon as the
runtime file loads, or on a flat sleep. They now wait for the runtime's
render-ready flag through one shared helper, keeping each command's timeout and
proceeding as before when it passes.

* fix(core): a script that wraps addEventListener keeps its wrapper after its scripts run

While composition scripts run, the runtime patches addEventListener to hold
load events that already passed. Putting the original back now happens only if
the patch is still in place, so a library wrapper installed meanwhile survives.

* docs: comments on script timing match scripts that wait for web fonts

The early stub's flush and the player's iframe-load handler described page
scripts as done by DOMContentLoaded and the runtime as ready before load.
Composition scripts now wait for web fonts, so both can come later. Comment-only.

* test(core): pin script order after an external script and the scene swap's font wait

An inline composition script that follows an external one sees its global, and
a scene swap runs the new scene's script only once web fonts are ready.

* docs(producer): the early stub comment names what drains late font-gated work

* fix(studio): motion paths add without a flash when gsap loads after web fonts

Studio preloads MotionPathPlugin on iframe load. A body gsap now runs after web fonts, often after that load, so the preload retries once the runtime reports ready.

* test(cli): pin the runtime wait before each command first samples the page

* fix(studio): the motion path preload stops waiting once the preview runtime has booted

A composition with no global gsap posts ready before the preload runs; the retry listener then never detached and kept each retired preview document alive. It now waits only while the runtime has not booted, and the tests pin the cleanup and the ready filter.

* fix(studio): a preview frame that never boots is not kept alive by the motion path preload

Only the latest preview waits for the ready message of its runtime; a newer load drops the older wait, so a crashed or missing composition frame can be collected.

* style: comments added by the font gate move into names
2026-10-04 10:24:42 +00:00
Miguel Ángel 7f3464921c test(studio): the edit bench render serves GSAP from the repo, never the CDN (#4996)
* test(studio): the edit bench render serves GSAP from the repo, never the CDN

* test(studio): the edit bench render opens no second page that fetches GSAP

* test(studio): the render test checks the CDN pattern matches the fixture's GSAP URL

* test(studio): match the CDN pattern the way Chrome does

* test(studio): the render test checks the whole interception pattern
2026-10-04 09:40:33 +00:00
Miguel Ángel 351d389c0b test(core): pin three composition-length rules that no test covered (#5000)
* test(core): pin the authored floor's direct children, the one-frame media skip and lottie sources

* test(core): pin the loaded-Lottie-library half of the pending rule
2026-10-04 09:19:43 +00:00
Miguel Ángel 7859c0f003 fix(studio): trimming keeps the playhead still; clips snap to the ruler grid (#4986)
* fix(studio): trimming keeps the playhead still; clips snap to the ruler grid

* fix(studio): an edge frame never becomes the playhead time; grid snaps where clips save

* ci(studio): run the trim and grid snap walk with the other Studio browser checks

* fix(studio): snap guides only where the clip saves; play keeps an explicit rewind

* fix(studio): a snap guide shows only where the saved clip lands; preview frame gets its own store

* fix(studio): judge a moved clip's guide by the duration the move keeps

* chore(studio): trim comments the names already say
2026-10-04 08:54:17 +00:00
Miguel Ángel b04081b376 fix(studio): the host can name Split's key on the toolbar and bind the clip menu's link keys (#4994)
* fix(studio): the host can name Split's key on the toolbar and bind the clip menu's link keys

* refactor(studio): early return for typed keys; export test checks identity
2026-10-04 08:46:31 +00:00
Miguel Ángel 18a922f433 fix(media-use): a slow download no longer leaves an empty asset that undo brings back (#4989)
* fix(media-use): a slow download no longer leaves an empty asset that undo brings back

* docs(media-use): tell the person an OAuth sign-in makes voice and avatar video free

* docs(media-use): the OAuth free usage line names the monthly allowance

* docs(media-use): the OAuth line says a free allowance
2026-10-04 08:40:46 +00:00
Miguel Ángel a8ccda23f3 Merge pull request #5001 from heygen-com/release/v0.8.121
chore: release v0.8.121
v0.8.121
2026-10-04 01:45:33 -07:00
Miguel Angel Simon Sierra a6fc2592e2 chore: release v0.8.121 2026-10-04 01:30:55 -07:00
Miguel Ángel 34ff5ca1de refactor(core): one pure owner for composition length and root size (#4990)
* test(core): pin how the runtime resolves a composition's length and size

* refactor(core): move composition length and root size rules into a pure module

* test(core): pin the one-frame timeline floor against a derived length

* fix(core): build and declare the composition-length subpath, reject a non-finite declared length
2026-10-04 08:16:20 +00:00