Commit Graph
2615 Commits
Author SHA1 Message Date
Vance Ingalls 56dcc4e47a fix(cli): accept 1–3-frame short WebMs + add direct pixel-probe tests
Address Miguel's R1 blocker + Rames/Miga's testability nit:

- The `-frames:v 3` decode samples AT MOST 3 frames; a legitimate 1- or
  2-frame WebM (256 or 512 bytes) was returned as `undefined` (probe
  failure), silently skipping the advisory even when every available
  pixel was opaque. Accept any positive whole-frame byte count ≤ 768
  (multiples of 256), distinguishing successful short-EOF from partial/
  malformed decode.
- Export `sampledAlphaIsFullyOpaque` and add 11 direct tests covering:
  3/2/1-frame opaque decodes → true; transparent pixel at pos 0 or
  final byte → false (guards the alpha-byte stride); non-frame-multiple
  / over-3-frame / zero byte counts → undefined; execFileSync throw →
  undefined; findFFmpeg missing → undefined; and one args-shape guard
  pinning the load-bearing `-c:v libvpx-vp9` before `-i` (without which
  the default decoder silently discards VP9 alpha and the whole check
  would false-positive on genuinely-transparent WebMs).
- Update advisory wording from "3 sampled decoded frames" to "every
  sampled decoded pixel" so the message is honest for short WebMs.

18/18 tests pass locally under `vitest run`.
2026-07-13 22:03:11 +00:00
Vance Ingalls b8562c91d5 fix(cli): detect WebM alpha lost when the ALPHA_MODE tag is written without side data
Extends webmAlphaCheck.ts (from #2044) with a pixel-level decode probe.
After the tag check passes, decodes 3 sampled frames via
`ffmpeg -c:v libvpx-vp9 -pix_fmt rgba -f rawvideo` at 8x8 and emits a
distinct advisory if every alpha byte reads 255.

#2044 detects the "tag absent" failure mode (ffprobe shows no
`alpha_mode` in stream tags). It doesn't catch a stricter case reported
on CLI 0.7.56 / Windows 11: ALPHA_MODE=1 present but BlockAdditional
alpha side data empty. Under current logic webmAlphaAdvisory sees
`alphaMode: true` and stays silent, so the render ships as opaque
without any signal.

The -metadata:s:v:0 alpha_mode=1 push is a muxer directive that some
ffmpeg builds write unconditionally, independent of whether libvpx-vp9
emitted the alpha plane. Tag presence is necessary but not sufficient
evidence of preserved alpha.

Advisory text names both possibilities (opaque composition OR silent
alpha drop) plus the concrete workaround (png-sequence + prores repack).
Fast path (no tag or missing tag) is unchanged. Probe adds ~1s per
WebM render only when the tag says alpha.
2026-07-13 21:38:12 +00:00
Vance Ingalls 9b71bc2103 Merge pull request #2361 from heygen-com/via/flake-fix-manager-progress-notice
test(cli): stabilize flaky install-lock wait-notice test with fake timers
2026-07-13 14:37:40 -07:00
Vance Ingalls e47af77c5a test(cli): stabilize flaky install-lock wait-notice test with fake timers
Adopt the fake-timer pattern the sibling "recovers when a crashed reclaimer
leaves both lock directories" test in the same file already uses. Without
fake timers, if the dynamic `import("./manager.js")` beat between
`installFsMocks({ initialMtimeMs: Date.now() })` and the `withInstallLock`
call exceeds `staleMs` (50 ms on a busy shared runner with `vi.resetModules()`
per beforeEach), the new immediate-stale short-circuit added in #2328 fires
on iteration 1, breaks out before `waitedMs` reaches `waitNoticeMs=20`, and
no "Waiting for another hyperframes process" warn ever emits — the assertion
at `manager.test.ts:428` (`expected false to be true`) then fails.

Under fake timers, `Date.now()` is frozen at the mtime seed, so the lock
stays non-stale across the dynamic import and the wait-notice branch
observes real polling; `vi.advanceTimersByTimeAsync(staleMs + pollMs * 5)`
then drives the loop past both the wait-notice threshold and the stale
deadline so the reclaim + acquisition still resolves.

Test-only change; no production-code diff. Verified 27/27 in
`packages/cli/src/browser/manager.test.ts` under `vitest run`.
2026-07-13 21:30:44 +00:00
Xuanru LiandClaude Fable 5 3e3b37d37f fix(cli): occlusion-probe false positives — pointer-events blindness, low-alpha gradients, not-yet-entered text (#2357)
* fix(cli): three occlusion-probe false-positive sources in text_occluded

- pointer-events:none text is invisible to elementFromPoint, so the probe
  always hit whatever paints beneath and misread visible text as buried;
  restore hit-testing on the element for the duration of the probe
- a backgroundImage counted as opaque regardless of alpha, so a 4%-alpha
  grid/scrim gradient qualified as an occluder; gradients now occlude only
  when their colours reach alpha > 0.6 (url() images unchanged)
- a visible container whose every text-bearing descendant is still at
  opacity 0 (entrance not started) was probed anyway; skip when no text
  ink is on screen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): address review — document-wide hit-testing restore, gradient compositing, whitespace ink

- restore hit-testing for ALL pointer-events:none elements during the text
  audit pass (not just the probed text): an occluder that itself carries
  pointer-events:none is invisible to elementFromPoint, which made truly
  buried text read as clean once the text alone became hittable
- hasVisibleTextInk ignores whitespace-only text nodes (indented markup
  defeated the gate) and uses a 0.05 floor so mid-fade text keeps its
  persistence occurrences
- hasOpaqueBackground composites gradient alpha with background-color
  (two 0.5-alpha layers paint at ~0.75); gradientMaxAlpha returns opaque
  for any colour function it cannot score (oklch/lab/...); percentage
  alpha values now parse as fractions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): walk the elementsFromPoint stack in occluderAt

A transparent layer that becomes hittable (pointer-events restored) must
not mask an opaque occluder painting beneath it — single-point
elementFromPoint returned the transparent top and dropped two genuinely
buried cases in the census acceptance run; the stack walk keeps 10/10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): composite stacked background-image layers when judging occluder opacity

Two 0.5-alpha gradient layers paint at 0.75 combined; taking the max
color-stop alpha across the whole declaration under-counted them and
suppressed real text_occluded findings. Split layers at top-level commas
(paren-aware), score each, composite as 1-prod(1-a_i). Also pins the
0.05 text-ink floor with a boundary test (review feedback on #2357).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): keep walking the occlusion stack past pair-specific exemptions

sharedPreserve3d and isCrossSceneTransitionOverlap excuse one hit, not
the whole probe; returning null let a transparent decorative layer in
the text's 3D context mask a real occluder below it (review feedback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:27:39 -07:00
Miguel Ángel 3df59fc0a4 fix(engine): support current FFmpeg filter scripts (#2324) 2026-07-13 16:52:15 -04:00
Miguel Ángel 81a6edcb67 fix(browser): recover abandoned install locks (#2328) 2026-07-13 16:52:04 -04:00
Miguel Ángel 5ff4ba13f4 fix(render): retry explicit parallel capture timeouts (#2331) 2026-07-13 16:51:58 -04:00
James Russo e523a29f59 fix(player): own connection and media resources (#2152)
## What

Make Player connection resources symmetric and scope slideshow media ownership.

## Why

Reconnect could retain destroyed handles, while slideshow operations scanned and mutated unrelated document media.

## How

Null and recreate connection-owned resources idempotently and introduce an OwnedMediaRegistry with abortable cleanup.

## Test plan

- [x] Player reconnect, media-scope, slideshow, and owned-media registry tests
- [x] Stack-wide lint, format, build, typecheck, and relevant integration gates
2026-07-13 16:15:06 -04:00
Miguel Ángel 648e4e8c54 fix(cli): declare Apache-2.0 license (#2351) 2026-07-13 14:56:51 -04:00
James 16942c0c12 fix(player): own connection and media resources 2026-07-13 14:34:54 -04:00
James Russo 242cb3234e Merge pull request #2151 from heygen-com/07-10-fix_core_preserve_runtime_transport_contract
fix(core): preserve runtime transport contract
2026-07-13 14:34:20 -04:00
James cc3ca2f9f7 fix(core): preserve runtime transport contract 2026-07-13 14:08:52 -04:00
James Russo a364f69568 fix(player): version runtime protocol (#2150)
## What

Version the Player/runtime protocol and pin injected core runtime code to the Player release.

## Why

An unversioned CDN runtime and untyped messages allowed silent host/runtime drift and ambiguous frame timing.

## How

Define RuntimeProtocolV1 with capabilities and rational fps, generate the versioned runtime URL, validate major versions, and retain a tested legacy fallback.

## Test plan

- [x] Player, Core, and Studio protocol tests; Player build-time runtime-pin verification
- [x] Stack-wide lint, format, build, typecheck, and relevant integration gates
2026-07-13 14:08:17 -04:00
James dcefdd98ca fix(player): version runtime protocol 2026-07-13 13:28:11 -04:00
James Russo 95fa14b2c2 test(repo): verify packed consumers (#2149)
## What

Verify publish artifacts as clean consumers instead of only scanning tarball manifests.

## Why

A package can contain all declared files and still fail when installed or imported by Node, TypeScript, or a browser bundler.

## How

Pack workspaces, install them into isolated fixtures, validate relative imports, and execute representative consumer entry points.

## Test plan

- [x] packed-manifest unit tests; clean packed install and consumer smoke
- [x] Stack-wide lint, format, build, typecheck, and relevant integration gates
2026-07-13 13:27:38 -04:00
James 979e9d0902 test(repo): verify packed consumers 2026-07-13 11:35:37 -04:00
Miguel Ángel 758a6d2152 test(ci): stabilize Windows suites after Studio cutover (#2320)
* test(cli): isolate browser check bundling

* test(studio): align timeline merge regressions
2026-07-13 03:24:02 -04:00
Miguel Ángel 94c2e0f6eb chore: release v0.7.56 v0.7.56 2026-07-13 07:04:42 +00:00
Miguel Ángelandukimsanov df29fa7a5e feat(studio): revamps Studio + improves code quality (#2291)
* feat(studio): glue API coexistence layer for the NLE swap

What: extends 21 glue files so the OLD timeline/canvas engine and the NEW
NLE components type-check side by side: playerStore (multi-select setters,
zoom pin, snap toggle, non-reactive scale scratch), drag-state types gain
optional NLE fields, timelineLayout/timelineAssetDrop/timelineEditingHelpers/
timelineEditing/timelineElementHelpers/studioHelpers/assetHelpers gain the
NLE exports, DomEditOverlay + gestures + AssetContextMenu + Timeline props
gain optional callbacks/params, contexts gain *Optional hooks, and
TimelineEditCallbacks.onMoveElements becomes a bivariant method accepting
both engines' change shapes. patchDocumentRootDuration's test rides along.

Why: this is the keystone that dissolves the old "welded glue" problem —
every symbol the NLE components need is ADDED next to what the old engine
still uses, so the engine components and the swaps can land as separate
reviewable PRs.

How: 15 authored intermediate files (main content + additive symbols; no
behavior changes — new fields optional, new callbacks unused until wired)
plus 6 files whose final content is already purely additive. New exports
without consumers yet carry TEMP(studio-dnd) ignoreExports entries, removed
by the app-shell swap.

Test plan: tsc --noEmit in studio + studio-server (verifies BOTH engines
compile); bunx vitest run (full suite green incl. the 6 new
patchDocumentRootDuration tests); fallow audit clean.

* feat(studio): timeline interaction hooks and lanes component (unwired)

What: the timeline-side wiring layer, unwired: TimelineLanes (the lane
renderer driving drag/resize/marquee), timelineMarquee (+tests),
useTimelineStackingSync, useTimelineGeometry, useTimelineEditPinning,
useTimelineEditingDrops.

Why: everything between the pure drag math and <Timeline> itself; the
timeline-glue swap PR then only rewires Timeline/TimelineCanvas onto these.

How: new files, tsc-clean against the coexistence layer. Unwired components
carry TEMP(studio-dnd) entry registrations, removed at the app-shell swap.

Test plan: bunx vitest run timelineMarquee.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): NLE shell assembly (unwired)

What: EditorShell (the full editor layout replacing NLELayout +
StudioPreviewArea), TimelinePane (timeline host with sub-comp rebasing) and
useTimelineEditCallbacks (the callback bag bridging store edits to the
timeline), all unwired.

Why: the shell that App swaps to in the final step; reviewing it standalone
keeps that swap PR small.

How: new files against the coexistence layer; TEMP(studio-dnd) entries
until App mounts EditorShell in the app-shell swap.

Test plan: tsc --noEmit; bunx vitest run (suite unchanged); fallow audit
clean.

* feat(studio): timeline glue swap — Timeline/TimelineCanvas onto the NLE engine

What: flips the timeline glue to its final form (23 files): Timeline and
TimelineCanvas rebuilt on TimelineLanes/TimelineOverlays, useTimelineClipDrag
drives preview/commit through the new drag engine, range selection goes
multi-select, playback loop moves to useTimelinePlayerLoop. Deletes the 9
old-engine files this orphans (group drag, marquee selection, snap targets,
layer gutter, selection overlays + their suites) — each is compile- or
gate-forced by this swap, verified by probe.

Why: second swap step; timeline-only, canvas and App untouched.

How: modified files to final content + forced deletions.
playerStore/timelineEditing/timelineCallbacks stay at their coexistence
form until the app swap (the old App still runs on them).

Test plan: tsc --noEmit; bunx vitest run (full suite); fallow audit clean.

* feat(studio): clip thumbnail modules

What: ImageThumbnail (+tests) and thumbnailUtils (+tests) — frame decode
with SVG/AVIF format fallbacks and rounded-corner clipping — plus
VideoThumbnail updates.

Why: the decode layer for timeline clip thumbnails, ahead of the visual
refresh that renders them.

How: new modules + one modified file; purely presentational.

Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.

* feat(studio): assets/blocks panel behaviors + preview helpers

What: blocks tab install flow, right-panel and global drag-overlay polish,
music beat analysis and clip-content rendering hooks, and the
preview-helper utilities backing asset preview.

Why: completes the studio NLE stack on top of the visual refresh.

How: modified files only (kept as one PR: splitting further would produce
sub-150-LOC fragments of interdependent panel glue).

Test plan: bunx vitest run studioPreviewHelpers/studioUrlState suites; tsc
--noEmit; fallow audit clean.

* fix(studio): restore timeline playback loop

* fix(studio): restore missing GSAP helpers module

* refactor(studio): split timeline GSAP helpers

* style(studio): keep timeline helper under size limit

* fix(studio): restore timeline overlays module

* fix(studio): remove stale GSAP import

* fix(studio): restore canonical timeline dependencies

* style(studio): format restored timeline helpers

* style(studio): satisfy helper line limit

* fix(studio): repair rebuilt timeline integration

* feat(studio): complete rebuilt NLE cutover

* fix(studio): guard project and timeline race boundaries

* fix(studio): preserve graded resize and crop geometry

* fix(studio): log resize/rotate commit failures, move anchor accumulator to resize-local

* fix(studio): treat duration-0 tweens as static holds and settle resize position before persist

Instant holds (to()/fromTo() with duration 0) were classified as animated
tweens by every commit route, so resizing or rotating them converted the
hold into a corrupt duration-0 keyframes tween (new value at 0%, old at
100%) that GSAP drops; panel edits appended a losing set. A shared
isInstantHold() now routes them through the static replace-in-place path,
and percentage math guards zero-duration windows.

Separately, anchored-corner resizes painted 3-5 frames at the new size but
old position while the offset persist round-tripped the server. The commit
path now applies the corrected GSAP position synchronously before awaiting
the offset persist, mirroring the scale route's settle.

* feat(studio): gesture-transaction seam with commit observability

Introduce runGestureTransaction — one owner for a gesture commit's
settle -> persist -> record lifecycle. It settles the live DOM
synchronously before any async persist, folds every mutation into one
undo entry via a per-transaction coalesceKey, restores pre-gesture state
exactly once on failure, and asserts (dev console) + reports (PostHog:
commit_transaction / commit_invariant_violation / commit_transaction_failed)
that a persist never changes pixels. The box-size resize path is migrated
onto it; the ad hoc per-route coalesceKey/reload handling is removed.

Extract the resize draft-rect math into resizeDraft.ts to keep the
gesture-handler file under the size cap.

Also: keep url_hash telemetry to the route slug only (drop the query
string, which carried the user's selected element id/selector), and gate
the [hf-resize] diagnostics behind localStorage hf-resize-debug so they
ship as opt-in tracing rather than console noise.

* fix(studio): transaction owns the undo label

The coalesced history entry took the last sub-mutation's label, so a
resize surfaced as "Move layer" (the offset persist) in undo/redo. The
seam now stamps tx.label on every wrapped mutation, so the folded entry
reads as the gesture.

* fix(studio): atomic static size/position commits (no data loss)

Static resize/position holds updated an existing set via delete+add — two
undo entries, and a delete that succeeded before a failed add lost the
hold on disk. Use one in-place update-properties mutation when a set
exists (one undo entry, no partial-failure window). The keyframed-hold
heal that can't be expressed as a property update now adds before it
deletes, so any single failure leaves a recoverable duplicate, never a
lost hold. Transaction-owned commits are tracked via a WeakSet so the
heal path never double-wraps an already-wrapped gesture.

* fix(core): restore timed-clip visibility after a forced timeline rebind

__hfForceTimelineRebind force-rendered the re-registered timeline but never
re-ran the per-[data-start] visibility pass, so after undo or soft reload
every clip rendered regardless of its time window until a full page reload.
Extract the visibility loop into syncTimedElementVisibility and call it from
both syncMediaForCurrentState (unchanged) and the rebind.

* fix(studio): atomic z-order/keyframe/split commits, one undo entry each

Three edit-commit paths hardened onto the one-transaction invariant:

- Z-order reorder (useElementLifecycleOps): N per-element writes now fold
  into one undo entry (coalesceMs Infinity) and, on a failed persist,
  restore already-written files to disk so no partial reorder survives.
- Enable-keyframes (useEnableKeyframes/useGsapKeyframeOps): the intermediate
  convert phase no longer full-reloads the preview (skipReload), killing the
  black-flash remount; convert + edit share one coalesce key = one undo entry.
- Razor split-all (useRazorSplit): snapshot before the batch and restore on
  any failure, so a mid-batch error never leaves un-revertable partial splits.

Shared file-history helpers (RecordEditInput, DomEditCommitBaseParams,
readProjectFileContent, restoreFilesToOriginal) dedupe the rollback/commit
logic across these paths. Commit options thread as one partial object rather
than field-by-field. Test setup extracted into colocated helpers.

* fix(studio): fold multi-step edits into one undo entry; guard text revert

- Gesture recording (useGestureCommit): the per-property-group commits now
  share one coalesce key and only the last reloads, so a recording is one
  undo entry and one preview reload instead of up to four.
- Delete selected keyframes (deleteSelectedKeyframes, split out of
  timelineEditingHelpers): N removals fold into one coalesced undo entry
  with a single reload.
- Text-field commit (useDomEditTextCommits): commitDomTextFields now uses
  the same version-guarded revert as handleDomTextCommit, so a stale failed
  commit can no longer stomp a newer successful one.

* feat(studio): batch a gesture's mutations into one atomic server write

A transaction that emits N mutations previously did N sequential POSTs,
each rewriting the file and soft-reloading — the root of the multi-phase
persist window. Add a gsap-mutations-batch endpoint that validates every
mutation up front, applies them in one in-memory rewrite chain, and writes
the file once (all-or-nothing: an invalid entry rejects the whole batch,
no partial write). The seam buffers a transaction's commits and, when more
than one targets the same file, dispatches a single batch — one write, one
history entry, one reload. The batch capability rides on the existing
commit-function reference; no option fields are threaded through callers.

* fix(studio): soften off-canvas indicator outline to 30% opacity

The dashed off-canvas selection outline at 60% was noisy with many
protruding elements on screen; drop the resting opacity to 30% (hover
still restores full opacity so it stays discoverable).

* fix(studio): drop off-canvas indicator outline to 10% opacity

Follow-up to the 30% softening — 10% resting opacity reads much calmer
with many protruding elements; hover still restores full opacity.

* fix(studio): gate [hf-commit] console traces to dev only

The start/settled/persisted/restore lifecycle traces logged on every
gesture commit in all environments — console noise for end users. Route
them through a dev-only traceCommit helper (matching the pixel-violation
error's existing DEV gate). The commit_* PostHog events stay always on;
they are the production observability, the console lines are a dev aid.

* fix(studio): count actual reloads, not softReload requests, in commit telemetry

A resize's size and offset persists both request softReload; the seam
counted each request, so a batched gesture reported reload_count 2 even
though the batch is one write and one reload. Compute the count from what
dispatchBufferedCommits actually did — one for a batch, the request count
for the sequential fallback.

* fix(studio): rotate hover + off-canvas overlays with the element; flicker-free crop

- Hover overlay applied the element's rotation only to the selection chrome,
  not the hover box; it now rotates about center like the selection, via a
  shared orientedGroupAwareOverlayRect router (one owner for rotation-aware
  overlay geometry across hover/selection/off-canvas).
- Off-canvas indicator was axis-aligned; it now rotates with the element and
  inverse-rotates the canvas-exclusion clip into the element's local frame,
  so the protruding-sliver clip stays correct for rotated elements.
- Crop commit re-lifted the element only in the commit's .then(), so one
  frame painted the cropped state (the flicker). Re-lift synchronously right
  after onStyleCommit (which applies the clip before its first await), so the
  cropped state never paints; the persisted file value is unchanged.

* fix(studio): address code-review findings across the commit-hardening campaign

Correctness (would ship green, bite under latency):
- Enable-keyframes phase 2 now carries coalesceMs: Infinity, so the convert
  folds into one undo entry instead of splitting past the 300ms default.
- The SDK keyframe persist path forwards coalesceMs (CutoverOptions gains the
  field); multi-keyframe delete and convert coalesce correctly when SDK-routed.
- Razor split-all's rollback is guarded so a failing restore can't swallow the
  error toast that tells the user the split failed.

Simplification (single source of truth / no dead flexibility):
- Decompose resolveResizeDraftRect (drops a fallow-ignore suppression).
- Delegate the third readProjectFileContent copy to the shared helper.
- Inline setPatchFromUpdateProperties (its only caller passes one mutation).
- One toSdkPersistOptions translates gesture overrides to SDK options.
- Bundle the reorder-rollback deps into one object (was 7-9 positional args).
- Dedupe the 'last group reloads' ternary; type gesture options as
  CommitMutationOptions; drop a Map+array wrapper around a single write.

* feat(studio): atomic z-order reorder via batch patch-element endpoint

Z-order reorder issued N per-element inline-style patches (one server
write each), so a mid-chain failure could leave a partial reorder on disk.
Add a patch-elements-batch endpoint that validates every patch, folds them
over the file in one in-memory rewrite, and writes once (all-or-nothing;
unsafe input rejects with no write). The reorder now sends one batch per
source file and records one undo entry. Because a failed atomic write
persists nothing, the interim disk-write-back rollback (restoreReorderedFile
/ restoreFulfilledReorderFiles / ReorderRollbackDeps) is deleted — failure
rolls back only live DOM/store state. Closes the last disk-atomicity gap.

* fix(studio): razor-split undo no longer silently no-ops

The split clone was written to disk without a data-hf-id, so the split
endpoint recorded that unstamped HTML as the undo entry's afterHash. The
next reloadPreview() ran the preview route's ensureHfIds write-back, which
minted a fresh id and persisted DIFFERENT bytes — so at undo time the disk
hash no longer matched afterHash and editHistory's content-mismatch guard
silently refused the undo (no write, no network, no error). Stamp the split
output via ensureHfIds in splitElementInHtml before it is written/returned,
so the preview write-back is a no-op and the recorded afterHash always
equals the final on-disk bytes. Fixes at the source rather than relaxing the
mismatch guard. Corrects the stale comment that credited forceReloadSdkSession.

* feat(studio): closed-hand grab cursor on the rotate handle

The rotate handle used the default arrow cursor; show a grabbing
(closed-hand) cursor on hover to signal it's grabbed and dragged to rotate.

* fix(studio): dropping a dragged element over another no longer selects it

A moved drag's release fired the box click, which re-selected whatever now
sat under the pointer via the hover cache — so dropping an element over a
higher-z one selected the drop target instead of keeping the dragged
element selected. The drag-move branch now suppresses the next box click,
mirroring the resize branch.

* fix(studio): group drag is one undo entry, not one per element

Dragging a multi-selected group committed each member's position write as
its own undo entry, so reverting took N Cmd+Z presses. Force a shared
coalesceKey (infinite window) across every member's commit so they fold
into a single undo entry, like the other multi-step commit paths.

* fix(studio): undo of a split no longer leaves a ghost clip in the timeline

The file and the composition iframe revert correctly on undo, but the
timeline panel kept a ghost node for the split clone. The element-merge
that repopulates the timeline preserves elements the fresh scan dropped —
intended for enriched sub-composition children a bare DOM re-scan misses,
but it also preserved a genuinely-removed TOP-LEVEL element (the split
clone after undo), leaving a phantom clip. Restrict the preserve to
elements with a compositionSrc (the enriched sub-comp children); a
top-level element missing from the fresh scan was truly removed.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-13 02:55:36 -04:00
Miguel Ángel 9940503102 fix(producer): dedupe local font embedding by resolved path (#2317) 2026-07-13 02:01:28 -04:00
Miguel Ángel 796d5df156 fix(render): allow explicit entry without index (#2307)
* fix(render): allow explicit entry without index

* fix(render): unify explicit composition handling

* test(cli): allow explicit render integration under CI load
2026-07-12 23:01:27 -04:00
Miguel Ángel 7c0dcb0b14 fix(media-use): ingest derived video outputs (#2310)
* fix(media-use): ingest derived video outputs

* style(media-use): keep ingest types after imports
2026-07-12 22:30:31 -04:00
Miguel Ángel 38badff820 fix(render): scope strict lint to composition (#2261)
* fix(render): scope strict lint to composition

* fix(lint): validate explicit composition scope
2026-07-12 22:18:11 -04:00
Miguel Ángel 2e34a2d5a0 fix(media-use): fall back to bundled SFX (#2257)
* fix(media-use): fall back to bundled SFX

* chore(skills): refresh media-use manifest

* docs(media-use): design CLI fallback advisory

* docs(media-use): plan CLI fallback advisory

* fix(media-use): surface HeyGen CLI fallback guidance

* fix(media-use): derive bundled SFX extension
2026-07-12 22:17:52 -04:00
Miguel Ángel 44653b31da fix(cli): detect video motion in sweep guard (#2308)
* fix(cli): detect video motion in sweep guard

* docs(cli): clarify iframe fingerprint scope
2026-07-12 22:17:49 -04:00
Miguel Ángel b861afe454 fix(check): snapshot restored text after contrast audit (#2306) 2026-07-12 22:17:46 -04:00
Miguel Ángel 9c0c1f99d4 fix(render): stream long low-memory captures (#2245) 2026-07-12 22:17:44 -04:00
Miguel Ángel 995885484f fix(render): reject incomplete captured frames (#2293) 2026-07-12 22:17:41 -04:00
Vance Ingalls c830aa83c5 chore: release v0.7.55 v0.7.55 2026-07-12 11:52:53 -07:00
Vance Ingalls 27eec5be6c Merge pull request #2301 from heygen-com/07-12-feedback-skill-bug-repro-guidance
docs(skills): tell agents to file reproducible render bugs, not paraphrases
2026-07-12 11:51:10 -07:00
Vance Ingalls 2a0c603430 Merge pull request #2300 from heygen-com/07-12-fix-software-video-beyond-viewport
fix(producer): stop clipping the bottom edge of software-GPU video renders
2026-07-12 11:50:56 -07:00
Vance IngallsandClaude Opus 4.8 6a3da1f624 docs(skills): tell agents to file reproducible render bugs, not paraphrases
The `hyperframes feedback` convention only prompted for a free-text
`--comment` "with the failing composition pattern and what you tried".
Agents dutifully filed vague reports (blank CJK text, mid-run exit, 4K
timeout) with no error string, no failure-mode, and — critically — no
published composition, so none could be reproduced or root-caused.

Two additions to the CLI skill:
- Lead bug reports with `--file-issue` (+ `--dir`), which publishes a
  minimal repro of the project to a public URL. A comment alone almost
  never lets a maintainer reproduce; the composition is what does.
- Give the `--comment` a concrete bug checklist: exact error string
  verbatim + whether output was produced / fell back / hard-exited; the
  isolated trigger; exact command + HF_*/PRODUCER_* env; frame/timestamp +
  visual defect. Drop the "repeat env" ask (the CLI already attaches it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:28:09 -07:00
Vance IngallsandClaude Opus 4.8 de95daa096 fix(producer): stop clipping the bottom edge of software-GPU video renders
resolveVideoCaptureBeyondViewport gated Chrome's beyond-viewport screenshot
path to hardware-GPU captures, to skip the full-surface software
re-rasterization tax. But without beyond-viewport, the viewport-bound
capture clips the bottom edge of any frame containing a native video
surface (the same #1094 tall-portrait guard the alpha capture paths already
hardcode) — leaving ~87 bottom rows black.

This hit two cohorts: software-GPU macOS/Linux hosts, and — worse — EVERY
distributed chunk render, which hardcodes browserGpuMode "software", so the
whole distributed fleet shipped video renders with a black bottom band.
Reporter confirmed forcing resolveVideoCaptureBeyondViewport=true fixes it.

Correct output wins over the software perf optimization: enable
beyond-viewport for any render with a native video surface, regardless of
GPU mode. Drops the now-vestigial browserGpuMode parameter (and its type)
and updates both call sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:28:09 -07:00
Vance Ingalls 534286a17b fix(producer): fall back fast when the parallel DE streaming capture stalls (#2298)
## Stack
1. **#2298 — DE router stall watchdog** ← you are here
2. #2300 — video bottom-edge clip
3. #2301 — feedback bug-report guidance

## Problem
The DE parallel router auto-enables the interleaved parallel-**streaming** capture for the ≥24 GB macOS trial cohort. If a worker wedges mid-capture (a hung seek/screenshot at an early frame), the render makes **zero frame progress** yet sits until the per-frame CDP `protocolTimeout` (~5 min) fires before the pinned self-verify fallback can run — a silent multi-minute hang shipped to real users.

Reported: stuck at frame 2/2031 for 6+ min, no fallback, until the user manually set `HF_DE_PARALLEL_ROUTER=false` (71 s clean).

## Fix
Add a no-frame-progress watchdog to the parallel branch of `runCaptureStreamingStage`:

- Ticks off `executeParallelCapture`'s progress callback. If no **new** frame lands within `HF_DE_PARALLEL_STALL_MS` (default **60 s** — well under the 5-min protocol timeout, ≫ the 15–32 ms/frame budget), it fires.
- On trip: aborts the **reorder buffer** (so peer workers parked in `waitForFrame` reject instead of deadlocking the `Promise.all` pool) and aborts the pool via a **separate** `AbortController` linked to the parent abort.
- The parent `abortSignal` stays un-aborted, so the orchestrator reads the failure as a generic `capture_error` (not a cancellation) and re-renders on the pinned screenshot path — the same fallback a verify failure already uses.

## Test
- Watchdog trips on no progress → rethrows a stall error (routes to fallback).
- A genuine parent-abort is **not** relabeled as a stall (stays a cancellation).
2026-07-12 11:25:39 -07:00
Vance IngallsandClaude Opus 4.8 07fb35c375 fix(producer): fall back fast when the parallel DE streaming capture stalls
The DE parallel router auto-enables the interleaved parallel-streaming
capture for the >=24GB macOS trial cohort. If a worker wedges mid-capture
(a hung seek/screenshot at an early frame) the render made no frame
progress yet sat until the per-frame CDP protocolTimeout (~5 min) fired
before the pinned self-verify fallback could run — a silent multi-minute
hang shipped to real users (report: stuck at frame 2/2031 for 6+ min,
no fallback, until HF_DE_PARALLEL_ROUTER=false).

Add a no-frame-progress watchdog to the parallel branch of
runCaptureStreamingStage. It ticks off executeParallelCapture's progress
callback; if no NEW frame lands within HF_DE_PARALLEL_STALL_MS (default
60s, well under protocolTimeout and >> the 15-32ms/frame budget), it
aborts the reorder buffer (unsticking peer workers parked in waitForFrame
so the pool doesn't deadlock) and aborts the pool via a SEPARATE
controller linked to the parent abort. Because the parent abortSignal
stays un-aborted, the orchestrator reads the failure as a generic
capture_error (not a cancellation) and re-renders on the pinned screenshot
path — the same fallback a verify failure already uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:35:21 -07:00
kiritowoo ccf5f20b3b fix(cli): stop losing render telemetry to the exit race (#2105) 2026-07-12 21:05:20 +08:00
James Russo 9d4ef6e6f8 Merge pull request #2148 from heygen-com/07-10-test_repo_enforce_workspace_contracts
test(repo): enforce workspace contracts
2026-07-12 01:51:56 -07:00
James 83db364f81 test(repo): enforce workspace contracts 2026-07-11 21:57:45 -07:00
James Russo 505003e0a6 Merge pull request #2147 from heygen-com/07-10-fix_core_enforce_strict_runtime_safety
fix(core): enforce strict runtime safety
2026-07-11 21:56:47 -07:00
James RussoandClaude Opus 4.8 f1f317a20c fix(cli): grant s3 encryption actions in lambda policies user output (#2289)
`hyperframes lambda deploy` runs `sam deploy --resolve-s3`, and SAM's
managed artifacts bucket (aws-sam-cli-managed-default) is created with
default SSE encryption. Setting that requires s3:PutEncryptionConfiguration,
which the generated deploy policy did not grant, so a first deploy by a
user provisioned exactly per `lambda policies user` 403s on the bucket and
the managed stack rolls back.

Add s3:GetEncryptionConfiguration and s3:PutEncryptionConfiguration to the
s3Bucket action set (Get pairs with Put for CloudFormation update/drift
reads, matching the existing Get/Put pairs in the list). Also add a hint to
the sam-deploy failure path pointing at the ROLLBACK_COMPLETE recovery step,
since first-time users hit the stuck-rollback error on their retry.

Fixes #2137

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:36:17 -07:00
James 0af07a07c5 fix(core): enforce strict runtime safety 2026-07-11 21:31:30 -07:00
Miguel Ángelandukimsanov ebbd1eb2c2 fix(studio): continuation of #2281 (#2287)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): multi-clip drag preview math

What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.

Why: the group-drag math, standalone and DOM-free.

How: new files only; consumed later by TimelineLanes.

Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline z-stacking sync model

What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.

Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.

How: new files only; consumed later by timelineZones and the stacking-sync
hook.

Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline lane-zone model

What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.

Why: completes the z-model started in the stacking-sync PR.

How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.

Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): asset click policy and canvas nudge gate

What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).

Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.

How: new files only.

Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.

* test(studio): characterization suites for resize commit and razor history

What: two test-only suites pinning CURRENT behavior before the NLE swap:
anchoredResizeReleaseShift.test.ts (manual-offset resize release commits)
and useRazorSplit.history.test.tsx (razor split undo/redo history).

Why: regression tripwires — the later glue-swap PRs must keep these green.

How: test files only; they import existing main modules unchanged and pass
against them as-is.

Test plan: bunx vitest run on both suites; fallow audit clean.

* feat(studio): canvas context menu and z-order actions (unwired)

What: CanvasContextMenu (right-click menu for canvas selections) and
canvasContextMenuZOrder (tie-aware bring-forward/send-backward z-order patch
computation) with its test suite. Shipped unwired.

Why: the z-order rules are the substance; mounting is one line in the later
overlay swap.

How: new files, compiled against current main. Nothing mounts the menu yet,
so .fallowrc.jsonc gains TEMP(studio-dnd) entries (entry registration +
ignoreExports) — removed by the app-shell swap PR that wires everything.

Test plan: bunx vitest run canvasContextMenuZOrder.test.ts; tsc --noEmit;
fallow audit clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-12 00:19:43 -04:00
Miguel Ángelandukimsanov c6a508a9bc fix(studio): continuation of #2277 (#2286)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): multi-clip drag preview math

What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.

Why: the group-drag math, standalone and DOM-free.

How: new files only; consumed later by TimelineLanes.

Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline z-stacking sync model

What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.

Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.

How: new files only; consumed later by timelineZones and the stacking-sync
hook.

Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline lane-zone model

What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.

Why: completes the z-model started in the stacking-sync PR.

How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.

Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): asset click policy and canvas nudge gate

What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).

Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.

How: new files only.

Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.

* test(studio): characterization suites for resize commit and razor history

What: two test-only suites pinning CURRENT behavior before the NLE swap:
anchoredResizeReleaseShift.test.ts (manual-offset resize release commits)
and useRazorSplit.history.test.tsx (razor split undo/redo history).

Why: regression tripwires — the later glue-swap PRs must keep these green.

How: test files only; they import existing main modules unchanged and pass
against them as-is.

Test plan: bunx vitest run on both suites; fallow audit clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-12 00:19:36 -04:00
Miguel Ángelandukimsanov 8e5b18f740 fix(studio): continuation of #2280 (#2285)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): multi-clip drag preview math

What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.

Why: the group-drag math, standalone and DOM-free.

How: new files only; consumed later by TimelineLanes.

Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline z-stacking sync model

What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.

Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.

How: new files only; consumed later by timelineZones and the stacking-sync
hook.

Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline lane-zone model

What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.

Why: completes the z-model started in the stacking-sync PR.

How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.

Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): asset click policy and canvas nudge gate

What: two small pure modules with tests — assetClickBehavior (click vs
double-click policy for sidebar assets) and canvasNudgeGate (debounce gate
for arrow-key canvas nudges).

Why: policy dependencies of the upcoming asset card and nudge hook,
reviewable as plain decision tables.

How: new files only.

Test plan: bunx vitest run on both test files; tsc --noEmit; fallow audit
clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-12 00:19:00 -04:00
Miguel Ángelandukimsanov 44ffe4b41f feat(studio): timeline collision and placement model (#2279)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): multi-clip drag preview math

What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.

Why: the group-drag math, standalone and DOM-free.

How: new files only; consumed later by TimelineLanes.

Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline z-stacking sync model

What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.

Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.

How: new files only; consumed later by timelineZones and the stacking-sync
hook.

Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline lane-zone model

What: new pure module timelineZones — visual/audio track-zone
classification (classifyZone) and normalizeToZones, which re-packs lanes
into zone-consistent rows; tests cover the stacking/zones interaction.

Why: completes the z-model started in the stacking-sync PR.

How: new files; consumes isAudioTimelineElement (leaf-helpers PR) and
computeStackingPatches (stacking-sync PR); type-only playerStore imports.

Test plan: bunx vitest run timelineZones.test.ts; tsc --noEmit; fallow
audit clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-12 00:17:20 -04:00
Miguel Ángelandukimsanov e464b33bb3 fix(studio): stacking sync (#2270)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

* feat(studio): multi-clip drag preview math

What: new pure module timelineMultiDragPreview — group-drag passenger
offsets and clamped group deltas (isMultiDragActive, multiDragDeltaSeconds,
multiDragPassengerOffsetPx, clampGroupMoveDelta) with tests.

Why: the group-drag math, standalone and DOM-free.

How: new files only; consumed later by TimelineLanes.

Test plan: bunx vitest run timelineMultiDragPreview.test.ts; tsc --noEmit;
fallow audit clean.

* feat(studio): timeline z-stacking sync model

What: new pure module timelineStackingSync — lane order ↔ z-index
reconciliation (laneIsAbove, computeStackingPatches) with tests.

Why: the single source of truth for how timeline lane order maps to canvas
stacking; the ordering rules and tie-breaks live here.

How: new files only; consumed later by timelineZones and the stacking-sync
hook.

Test plan: bunx vitest run timelineStackingSync.test.ts; tsc --noEmit;
fallow audit clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-11 22:56:17 -04:00
Miguel Ángelandukimsanov 345f715c3f fix(studio): timeline snapping (#2269)
* feat(studio): timeline collision and placement model

What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).

* feat(studio): timeline magnetic snapping

What: new pure module timelineSnapping — snap-target collection and
pixel-threshold time snapping (collectTimelineSnapTargets, snapTimelineTime,
snapMoveToTargets) with tests.

Why: the magnet math for clip drags/trims, reviewable standalone.

How: new files only; type-only playerStore imports; consumers land with the
drag engine.

Test plan: bunx vitest run timelineSnapping.test.ts; tsc --noEmit; fallow
audit clean.

---------

Co-authored-by: ukimsanov <ular.kimsanov@heygen.com>
2026-07-11 22:39:54 -04:00
Ular Kimsanov 7f21d5dc49 feat(studio): timeline collision and placement model (#2195)
What: new pure module timelineCollision — zone-aware drop placement
(clampTrackToZone, resolveZoneDropPlacement, resolveInsertRow,
resolvePlacement, lane/overlap predicates) with its full test suite.

Why: the no-overlap core of the NLE clip-drag engine; plain functions, no
DOM, no React, no store writes.

How: new files only; type-only imports from the existing playerStore.
First runtime consumer arrives with the drag-engine PRs.

Test plan: bunx vitest run timelineCollision.test.ts; tsc --noEmit; fallow
audit clean (all exports test-consumed).
2026-07-11 22:38:16 -04:00
Ular Kimsanov 58a36d7078 Merge pull request #2268 from heygen-com/studio-dnd/pr03-studio-server-files-route
feat(studio-server): files route extensions
2026-07-11 18:21:32 -07:00