* test(studio): record each edit-accuracy shard's file writes and reloads when HF_WRITE_TRACE is set
* style(studio): format the write trace
* fix(core): hash the write trace with sha256
* style(studio): wrap the trace hash helper
* style(studio): format the trace hash helper
* fix(studio): turn the reload log on in the Studio page the trace watches
* test(studio): real-Chrome tests inline GSAP so their page waits on no request
* test(ci): smoke projects inline GSAP so required checks wait on no CDN
* feat(skills): a Remotion port carries Remotion's box-sizing rule so bordered boxes keep their size
* chore(skills): regenerate the skills manifest after the format pass
* fix(hooks): the skills manifest is hashed after the formatter has run
* test(studio): a drag is measured against the box the preview shows
* test(studio): another element is read bare until it is selected
* test(engine): the URL-read audio mix test stops its mix before cleanup and is not timed
* test(studio): bank the open-film thumbnail counts on the CI Chrome
The CI Chrome moved to 155 and renders one thumbnail fewer: 13 renders, 14 requests.
* fix(engine): a long audio mix prepares its clips a few at a time
A 200-clip mix started 200 ffmpeg at once and starved the host.
* chore(engine): trim the long comments in the audio mixer
Reasoning moved to the PR.
* test(engine): the mix order test starts every preparation at once
The mixer prepares clips in a core-sized pool; the test now sets enough cores.
* test(studio): retain Chrome launch diagnostics
* fix(studio): read Chrome's version from the file on Windows instead of running it
* fix(studio): shorten the Chrome version probe
With no shard results, ratchet.mjs threw, wrote no report, and the workflow
printed "Edit accuracy fell against the base branch" for what was a build or
shard failure. A shard that did not report was also counted as a drop. The
gate now exits 2 with "Edit accuracy did not run: N of M shards reported
results" when nothing that ran fell, writes its comment and report in that
case too, and the workflow prints the gate's own verdict.
* test(studio): real-Chrome tests recover from a stuck Chrome launch or close
* test(studio): retry a stuck Chrome launch that Windows reports as already running
* perf(studio): settle the edit accuracy bench on frames and network, not a fixed window
settled() waited for the box and the preview frames to hold still for 500 ms,
7 to 15 times a case. It now resolves once they read the same on 3 animation
frames in a row while the Studio page has no request in flight (the event
stream and server-rendered timeline thumbnails excepted) and the server has
announced every project file written since its last file-change event, so a
save, a preview reload or a reload the event starts still holds it open.
EDIT_ACCURACY_STILL_MS restores the fixed window with no network signal.
Also polls studio_seek every 50 ms instead of 250 ms while Studio opens.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf(studio): keep one producer Chrome open across edit accuracy renders
Every render launched and closed its own producer Chrome. The run now holds a
lease on the engine's pooled browser (a capture session with the renders'
launch settings and its page closed), so each render opens a page in that
browser instead. Each render still gets its own page and file server, and
clears the browser cache first. A held browser that failed to launch or whose
Chrome died is dropped and held again, never reused.
Studio's server stays per case: it serves one project folder, stamps it on
start and keeps that folder's undo history open, so switching folders under it
would leak history between cases. Its start is polled every 50 ms instead of
200 ms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(studio): import render.mjs at the top of the edit accuracy render test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio): restart the settle window when a busy request's frame goes away
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* style(studio): format the edit accuracy settle test
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio): seek the edit bench only after Studio binds the timeline
openStudio now polls every 50 ms, so after a reload its first studio_seek
often lands before the runtime binds the timeline (Studio still reports
duration 0, but the playhead moves). The bind then replays the playhead from
near 0 (core init.ts progress(0.0001) then totalTime). A tween that starts on
the playhead was already initialised by the early seek, so GSAP skips its
first keyframe. The preview then holds a box no fresh seek or render shows.
In resize-fromto-px-r0-nested-z100-on that box is 90 px off, and it persists
until the next seek.
Wait for studio_look to report a duration before seeking.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf(studio): cut fixed waits from the edit accuracy bench
- Settle window 1000 ms -> 500 ms (EDIT_ACCURACY_STILL_MS overrides it).
- Drop the fixed 1 s sleep in openStudio; the settle that follows is the
real readiness wait.
- Mark composition hosts data-no-timeline in the producer's page when the
saved project has no GSAP timeline, so the render skips the 10 s
playerReadyTimeout wait.
- Run the blank-page smoothness control once per gesture per browser.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(studio): trim the edit accuracy grid to 741 cases
Drops zoom 200, the pct and center placements, and nested keyframed cases
from the grid (2061 -> 741 cases; the pr slice 435 -> 265). Drag paths and
sequences are unchanged.
baseline.json loses the 1320 removed cases. The ratchet no longer counts a
base case that the head's baseline.json does not hold, and lists such cases
as retired, so removing cases from the grid and baseline.json together does
not fail the gate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio): measure the blank-page control again after it fails
The control is cached per browser and gesture, so a rejected measurement stayed
cached and failed every later case with that gesture before Studio was tested.
Drop a rejected entry so the next case measures again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(studio): keep the nested keyframed resize cases in the edit accuracy PR slice
Two historical gate runs failed only on resize-scale-px-r0-nested-z100-on,
which the trim removed, so the PR slice would have passed that build. Keep
the nested keyframed resize on a keyframe at zoom 100 without rotation, one
case per GSAP mode, with their baseline entries. PR slice 265 -> 271 cases,
full grid 741 -> 747.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(studio): run the edit accuracy slice on PRs and the full grid in the merge queue
The edit accuracy gate used about 83% of CI job-minutes, ran on changes that
cannot reach the bench (encoder, render and CLI command code), and never ran
in the merge queue, so main could drift and fail every later PR.
- Path filter: its own paths-filter step with some-with-excludes, limited to
what the bench runs: Studio, studio-server, core, parsers, player, sdk, lint,
the CLI preview server path, and the engine and producer modules in the
import closure of browserManager, frameCapture and both fileServers. Test
files, fixtures and docs no longer trigger it. The bench's webmcp-host.mjs,
which it imports but the old filter missed, now does.
- merge_group: the filter's base is the event's base_sha there. In the queue
the full grid always runs (20 shards) whenever the filter matches. The queue
is the last point before main, and nothing else measures the full grid
now that PRs run a slice. Skipping it would let zoom 50/200 and rotated
regressions land on main, where they fail every later PR against the
banked baseline. That drift caused about a fifth of past gate failures.
- PRs: the existing `pr` grid becomes zoom 100 with no rotation in both
nestings (435 of 2061 cases), in 5 shards. Of the 11 PRs whose real
regressions the full gate caught, this slice held a regressed case for 10.
- Gate: newly passing cases missing from baseline.json are listed in the
comment and as a notice, not failed (45% of past gate failures). A partial
grid compares only the cases it ran. `--shards` fails the gate when a shard
reported no results, and only a full grid writes a bankable baseline.json.
- The required check "Studio: edit accuracy gate" keeps its name. It is
skipped when the filter does not match, as before.
- check-test-reachability models negated globs as exclusions under
some-with-excludes and still fails closed elsewhere.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(studio): run the edit accuracy gate when its install inputs change
The path filter missed the root package.json, the dependency install script
and the ffmpeg setup action, which Build and every shard run. A change to any
of them could alter the bench while the gate skipped. Add them, and a test that
every local input of the jobs that build and run the bench is in the filter.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci(edit-accuracy): cover transitive bench inputs in the path filter
The filter test now walks the files the bench actually runs: the bench
entry files, their relative and @hyperframes/* imports, the CLI commands
the bench invokes, and the build scripts, configs and files those builds
read for every package the bench reaches. Globs are matched with
picomatch, the matcher dorny/paths-filter uses.
The walk found 183 inputs the filter missed, including the producer's
frameCapture.ts, build-hf-early-stub.ts and stubs/hf-early-stub.ts, the
engine services behind engine/src/index.ts, and the CLI browser command.
The filter now covers them.
picomatch is not in the no-install Test reachability job, so the filter
test moves to test:scripts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A 9:16 fixture with a looping A-roll video, an untimed full-frame backdrop above
it that the film's own GSAP script shows only from 6 s to 8 s, captions on top
and two sub-compositions. The check plays it in Studio's preview and reads the
preview's pixels and the element on top at the frame's centre: the video must
show at rest and while playing outside the backdrop's window, and the backdrop
must show inside it. It fails with #5458's visibility skip reverted.
* fix(studio): the timeline viewport gate gives the same verdict on the same code
Steps are timed from the frame the jump is made in, so each is a whole number of frames and the
3.5-frame budget no longer cuts through one cluster of samples. When the head still misses a budget,
it is measured against its base in alternating blocks on the same runner and fails only when it
misses on more steps than the base beyond chance.
* style(studio): format the viewport gate verdict tests
* fix(studio): a head slower than its base fails the viewport gate comparison
* fix(studio): the viewport gate opens head and base fresh before comparing them
* fix(studio): each viewport gate comparison block runs alone in a fresh Chrome
* fix(studio): the viewport gate comparison also fails a head that makes late steps later
* fix(studio): the viewport gate summary names every count behind a comparison verdict
* fix(studio): label the viewport gate frame-gap counts by threshold
* fix(studio): a runner stall cannot fail identical builds in the viewport gate comparison
* fix(studio): the viewport gate serves its base the same projects as the head
* fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels
Resized sizes stay on whole CSS pixels (GSAP centres a -50% translate from the
integer offset size), so the centre-anchored resize left the grabbed corner up to
a third of a pixel times the element's scale off the pointer: 0.53 px on a 1.5x
scaled layer. The centre pin now takes half the rounding remainder along the
element's rendered edges, so the grabbed corner lands where the wanted size puts
it while the written size stays whole.
* fix(studio): a scale resize saves the box exactly where it was dropped
finalizeScaleResizeCommit skipped its position correction whenever the box sat
within 0.5 px of the drop point on each axis, so a sub-pixel drop (now the
norm, with the grabbed corner on the pointer) saved up to half a pixel off.
It now skips only when the correction rounds to nothing at the precision the
file is written in.
* fix(studio): a scale resize lands centred on the box that was dropped
finalizeScaleResizeCommit lined the committed box up with the drop by its
top-left corner. When one scale cannot reproduce the dragged width and height
(the draft rounds each to whole pixels), that put the whole gap on the far
edges, up to half a pixel. The resize is centre-anchored, so it now lines up
centres and the gap splits evenly.
* chore(studio): keep the complexity note on the resize intercept, not the new helper
* test(studio): bank the two scaled resize cases that now pass the edit accuracy bench
* fix(studio): a scale resize leaves animated positions alone and mirrored corners track
* test(studio): bank only the two newly passing scaled resize cases
* fix(studio): a mirrored or parent-rotated layer's resize corner tracks the pointer
* fix(studio): a resize corner on a skewed layer picks its side from the box's own edges
* fix(studio): resizing a layer that renders with no area moves nothing
* test(studio): launch real-Chrome tests like the e2e launcher, bounded by the test budget
* test(studio): share the real-Chrome page setup between the two runtime tests
* test(studio): keep the real-Chrome test helper with the other test helpers
* test(studio): bench moves pressed right after Add keyframe
* fix(studio): an undo that reloads the preview keeps the selection, so a drag after it moves
* fix(studio): a drag started while a resize saves keeps its animations paused
* fix(parsers): an edit pins no position on a keyframed tween it did not touch
* fix(studio): a move pressed while the preview changes lands on the new preview, at the time pressed
* test(studio): bench waiting presses, a held box resized twice, and reloads at the last playhead
* refactor(studio): tidy the waiting press, bound its wait, and freeze edit stamps after early exits
* test(studio): load the property panel once so its first test stops timing out
* fix(studio): presses queued on a held box keep their release, hover and newest outline
* test(studio): judge bench undo by the history groups Studio claims, not by file writes
* refactor(studio): one waiting-box lookup in the bench and one raf-pause write per release
Behaviour-identical tidy of the waiting-press work: the bench reads the waiting
box through one helper, releaseActiveGesture sets the raf pause once, clearPointerState
writes the false it always computed, and previewGestureStarts calls the opts setters
directly instead of through aliases named like the gesture hook's gated setters.
* fix(studio): a queued press lands on the element it pressed and survives a window switch
* fix(studio): undo cancels a waiting press, a stuck save stops holding presses, scene swaps hold them
* refactor(studio): keep the complexity waiver on clearPointerState itself
* fix(studio): undo of a queued press leaves nothing waiting, and hosts keep their own undo
* fix(studio): a waiting press keeps the box still after a blur, and undo reports what it took back
* style(studio): bring the PR's comments within the comment limits
* fix(studio): bank the four bench cases this fixes and meet the comment, complexity and duplication gates
* test(studio): pin that a waiting press is never a pending edit, and name the new duplication waivers
* test(studio): keep the undo socket gate's sockets pinned past Chrome's idle media suspend
* test(studio): the socket gate retries its video scan and says only what its pins prove
* test(studio): bench a plain element animated through auto-record and Add keyframe
* fix(studio): seed a first tween from the offset a plain move left in CSS translate
* test(studio): bench resize then drag on a GSAP tween, root and nested
* fix(studio): a resize under auto-record keys size at the playhead, held at all times
* fix(studio): keyframe tweens Studio writes end on their last key, so a seek matches playback
* test(studio): keep the plain-to-keyframes moves inside the nested frame
* fix(studio): a resize moves its anchor from the drag stamp it read at release
* fix(studio): an undo shown in place is no longer replaced by an older reload
* refactor: split the trailing keyframe trim and drop an unused GSAP reader
* test(studio): settle the resize-then-drag bench steps so a two-save resize is one undo
* fix(studio): a preview reload waits for an undo shown before its write lands
* fix(studio): Add keyframe reads a subtracted calc() translate with its sign
* refactor: tidy the keyframe hold, trim and resize code after review
* fix(parsers): the keyframe trim leaves looping, chained and relatively placed tweens alone
* fix(parsers): a size hold pins only a lone key, so an edit never changes frames left alone
* fix(studio): every undo applied in place counts as a preview change, script-only ones too
* fix(studio): a resize keys size only when a move or size is keyframed, not a fade
* test(studio): a bench step whose save never lands fails the case
* fix(parsers): holds and trims change only the tweens an edit touches
* test(studio): bank the plain-to-keyframes and resize-then-drag bench cases
* fix(studio): undo takes back the first edit of a film whose media is still being copied
* fix(studio): the first undo of a film repaints the preview in place
* fix(studio): background media copies keep the media ledger and survive a history prune
* fix(studio): a busy project history never makes writes wait for its lock again
* refactor(studio): name the restore stamping instead of commenting it
* perf(studio): undo stamps only a restore side that has no stamps
* fix(studio): closing a project history finishes its media copy first
* fix(studio): media copies stay resumable until recorded and skip a replaced folder
* fix(studio): the history lock wait comes back after a successful open
* 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
* 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
* 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
* 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
* 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
* 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
* 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
* fix(parsers): a keyframe edit no longer overrides an earlier tween at the start
Writing a keyframe into a tween that starts later added a t=0 hold for every position prop of
its first keyframe, so a from() that already moves x before it lost its start value. The hold
now skips props an earlier tween on the same target writes; both writers share one helper.
* fix(parsers): keep the hold for label-placed tweens and re-sync one-line scripts stably
The recast parser leaves a label start unresolved; an unknown start no longer counts as earlier,
so the hold stays. The acorn insert now ends the hold on its own line when the next statement
shares the timeline line, matching what removal takes, so a second sync changes no bytes.
* fix(parsers): the recast hold sync reads tween starts from the acorn parse
The recast parser cannot resolve label positions, so an earlier label-placed tween looked
unordered and kept a hold it should not. Both writers now decide holds on the same parse.
* revert(parsers): keep the recast hold sync on its own parse
Deciding recast holds on the acorn parse lost selectors only recast resolves (an element bound
through a helper). Recast is the migration fallback and keeps its old hold for a start it cannot
resolve; the earlier-label case is decided by the default acorn writer, which resolves labels.
* style(parsers): name the hold insert's line ending instead of commenting it
* style(parsers): fit the hold helper's doc in four lines
* test(studio): bank the from/to edits this fix makes accurate and the resizes #4920 fixed
* test(studio): serve the edit bench GSAP from the repo, never the network
* test(studio): serve the installed gsap dist and never crash the bench on a closed request
* test(studio): serve only regular files from the bench gsap dist
* fix(studio): an undo or drop on an animated layer keeps it where it was
* test(studio): bank the edit accuracy cases the soft reload fix makes pass
Spliced from gate run 37151157876 (merged with main 7dea1e58): the 207 newly passing lines only, byte for byte; every other line is main's.
* fix(studio): an edit on a GSAP-animated layer writes its value at the playhead into the file's tween
* fix(studio): undo pressed while an edit saves undoes that edit, not one started after the key
* fix(studio): an edit stays saved when the history reply after it cannot be read
* feat(studio): export ColorField and GradientField; a session style commit takes a map
* refactor(studio): name the map form instead of commenting it
* feat(studio): a host can move layers by exact film px, one undo per call
handleDomGroupMoveBy(moves) on useDomEditSession and DomEditValue moves each
selection by its delta in composition px, for Desktop's align and distribute.
It reuses the group drag's own path: a drag member per element (press-time
route, measured offset map), the delta carried to screen px through the
preview frames, then handleGsapAwareGroupPathOffsetCommit, so one shared
coalesce key gives one undo entry and GSAP members take the GSAP write.
A member that can't move refuses the call before anything is written; the
refusal rejects with its reason and shows no Studio toast (the group commit
takes refusalToast: false; telemetry stays). Zero deltas are skipped.
Tests: a pre-offset element and one under a half-scale parent move by exactly
{10,0} and {0,-5} under one group key; a half-scale parent with a 30 deg
rotated ancestor lands within the 0.001 px a translate is written with; a
GSAP member goes through the GSAP write; three refusal kinds write nothing,
leave the boxes where they were, show no toast and leave no gesture mark,
while a canvas group drag still toasts; the session hands the call to the
group commit with refusalToast: false. Each mutation (no frame mapping, no
zero skip, no restore, no teardown, no capability check, toast on refusal)
fails at least one of them.
* fix(studio): an outside change during a drag waits for the drop to show
While a gesture mark is live in the preview, refreshPlayer takes the shadow load instead of
swapping scenes in place. A swap replaced the node under the pointer mid-drag; the shadow load
is then held by the existing gesture hold and, if the drop saved, reloaded fresh by the stale
rule, so this adds no second "released" signal.
Test: an outside change during a live drag on a preview that can swap scenes leaves the dragged
node in place, shows nothing under the pointer, and after the drop loads fresh with the dropped
position. It fails on the parent without the condition ("swapped under the pointer").
Cost case, measured in a real headless preview on the edit-accuracy bench's per-frame teleport
check. Every GSAP instant patch was forced to miss and fall back to reloadPreview during its own
commit; move and rotate of a held element (gsap.set and tl.set holds), px and xPercent
placement, r0 and r30, root and nested, at 100%:
- The gesture mark was live when the fallback asked for the reload, but had always ended by the
time refreshPlayer ran (refreshKey is consumed in an effect after the commit settles). So the
new condition never fired there: root previews offer no scene swap and load shadow on both
builds, and nested previews swap on both builds.
- Worst post-release frame offset, 32 cases on this build and 32 on the parent: 0.00 px over
76-106 frames each (release to settle). No frame was off by more than 0.5 px.
- Sensitivity: a 3 px nudge injected 50 ms after the fallback reads back as 3.00 px.
* fix(studio): an edit no longer reopens the live-updates stream, and thumbnails ask one at a time
* fix(studio): an undo no longer waits behind paused preview videos for a connection
* feat(studio): a host whose API needs same-origin cookies can have Studio send them
* docs(studio-server): a host on same-origin cookies sets the Studio credentials opt-out
* test(studio): an undo is not held up by the preview's paused videos
* fix(studio): a resize inside one gesture writes a mixed tween once, in place, never beside it
* chore(studio): keep the carried fixes inside the comment ratchet; telemetry posts through studioApiFetch
* test(studio): a drag on a mixed tween is one write under the gesture key
* fix(studio): freeze-frame, loudness and peak-map requests go through studioApiFetch too
* fix(studio): an undo with no project open reloads in full instead of skipping nested files
* refactor(studio): drop the soft reload's reparse seeding, the held-element sweep covers it
* test(studio): keep the undo-restore browser test off the network and inside its timeout
* test(studio): nested composition reads expect the cookie-free request studioApiFetch sends
* fix(studio): an edit at the playhead rewrites its tween in place, so later tweens keep their times
* fix(studio): a rotate on a tween that turns another axis holds its angle and never turns early
* fix(studio): plan every group member before the first write so one refusal moves none
A group drag or nudge preflighted each member against the move refusal rule only. The value-at-playhead planner ran later, in the commit loop, after earlier members were already flushed to disk, so a tween with extras, eased keyframes or an unknown ease left the group half moved.
The group preflight now runs the commit's own route and keyframe plan for every member with its real offset, so any refusal refuses the whole group with nothing written and one toast attributed to the refusing member. The plan is not reused in the commit: earlier members' writes change the file under it, so each member re-plans against the fresh tween, from the same planValueEdit.
* chore(studio): the fetch lint rule also catches window.fetch, globalThis.fetch and self.fetch
* docs(studio-server): set the credentials flag before Studio loads; a mounting host injects it
* test(studio): the undo socket test needs six pinned videos under Vite and checks each undo restores
* fix(studio): Studio leaves cookies out of its requests only when served from loopback
* fix(studio): undo takes back the first edit, and the first save never waits on media
* fix(studio): undo of a move whose save failed stays undone and steps back no older edit
* fix(studio): a style set by its JavaScript name saves under its CSS name
* test(studio): the undo socket test holds round 0 to a full restore too
* fix(studio): the host revert keeps returning what shows the edit again
* fix(studio): an entrance rewritten at the playhead still shows its start state before it starts
* fix(studio): undo takes back the edit pending at the key even after a later edit claims
* chore(studio): lift the late-write quarantine now that undo never waits on videos; format
* test(studio): pin an entrance's start render on the written tween, not a happy-dom GSAP run
* revert(studio): leave entrances as on main, immediateRender does not render a keyframed start
This reverts commit ee9d130d3c and its written-vars test.
* revert(studio): undo goes back to the newest claim after the key, as on main
This reverts commit 5b9009a823.
* revert(studio): first-undo history opening moves to the next PR
This reverts commit 4e1e75d7f4.
* revert(studio): the undo socket test exempts round 0 again until first-undo lands
This reverts commit 1f8f97047c.
* test(studio): bank the edit accuracy cases this PR makes pass
* refactor(studio): share one project file fetch between the plain and optional reads
* test(studio): share the WebMCP host shim between the browser gates
* test(studio-server): share one stub adapter across the route tests
* refactor(studio): type the preview slot props from the NLE context
* test(studio): share the setup the new editing tests copied between files
* refactor(studio): split playhead tween picking and undo path merging into small tested helpers
* test(studio): pin when a resize at the playhead also writes the anchor move
* test(parsers): pin rewriting a tween in place as keyframes
* chore(studio): cut comments the new helper names already say
* fix(studio): composition thumbnails keep two fetches in flight, as the document lifetime fix expects
* fix(studio): a playhead edit parses a tween not yet played and matches only its own tween
* fix(parsers): a new position argument goes after the last argument, so trailing commas stay valid
* fix(studio): a second undo pressed during one save steps back to the edit before it
* test(studio): pin the slot an aborted thumbnail frees and the shared tween refusal
* refactor(studio): split the unplayed-tween parse into small helpers and name the claim release
* fix(studio): a playhead edit keeps the tween's default ease, delay timing and loop settings
* fix(studio): a group move refuses layers animated in two files and plans at the real drop
* style(studio): format two test files
* refactor(studio): check a group's files inside the preflight helper
* refactor(studio): move the group preflight checks out of the editing hook, under 600 lines
* fix(studio): read an unreached tween without redrawing other layers, and compare write files
* test(studio): use the shared selection helper in main's keyframe-at-time telemetry test
* chore(studio): fix a comment that read as a repo path and cut one added comment
* fix(studio): read a later tween's start as playback does, and count every keyframe add
* fix(studio): two undo presses during one save step back two edits, not one twice
* fix(studio): an undo that succeeds by any route retires the claim on the edit it undid
* fix(studio): a claim is never handed back to an edit another undo has already undone
* fix(studio): an undo picks its claimed edit at the press and skips it once undone
* fix(studio): a press refuses, as main did, once its edit is undone and a newer edit exists
* fix(studio): an unplayed keyframe tween clears every keyframe's channels before it is read
* fix(studio): reading an unplayed tween puts back every transform value its clear removed
* fix(studio): reading an unplayed tween restores each layer's whole style and GSAP state
* fix(studio): reading an unplayed tween skips plain-object targets and restores attributes
* fix(studio): shared tween-target filter uses GSAP's own styled-element test
* fix(studio): match a parsed tween on any target, and check a later keyframe channel's start
* docs(studio): say the unplayed-tween read restores styled elements only
* test(studio): a drag on a fromTo start shows the dropped x through the runtime seek
* test(studio): the edit accuracy bench uses the port its server bound
A busy port made the CLI bind the next free one, and the bench then failed
the case with "studio moved from port N to N+1" instead of using it.
* test(studio): drop a comment the doc line already says
* test(studio): the bench server test removes its temp dir
* test(studio): the bench stops its Studio server on Windows too
Windows has no process groups, so the group kill never reached the server and
the stop waited forever (the new server test timed out on windows-latest).
taskkill /T, the CLI helper for the same job, ends the tree there.
* test(studio): a failed taskkill surfaces, and the server opens no console on Windows
* perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips
Dragging or keyboard-moving a selection of 4 or more clips is refused with a
toast pointing at the prompt box; 1 to 3 clips move as before.
Root causes of the multi-second waits on large projects:
- every selected clip fired its own source-existence probe; they now leave as
one request per file
- a timeline delete sent one request per clip, each re-parsing the file; it is
now one request that parses once
- each click on a clip from another file re-fetched GSAP data for every
composition; covered files are kept and unchanged files answer 304
- any root index.html write re-captured every composition thumbnail; only a
root head edit does now, since a scene page borrows nothing else from it
Adds timeline-bulk-actions.mjs, a browser benchmark of select-all, move 3 and
delete-all on a real project.
* chore(studio): trim the bulk benchmark header to the comment limit
* refactor(studio): tidy the bulk-action fixes after review
Root head memo is keyed by the root file's content so every subscriber of one write gets one answer, and a thumbnail request can no longer overwrite it. The GSAP read reuses the shared content version as its ETag.
* fix(studio): keep a head edit's thumbnail refresh when a later write replaces it
File changes coalesce to the newest while a drain is in flight, so the affected-composition lists are merged instead of replaced. A failed GSAP read stays unloaded and is retried, and the GSAP read's ETag is salted with the parser version.
* feat(studio): cap hand-resizing a multi-selection at 3 clips like moving
Dragging a resize edge with 4 or more clips selected is refused with the same toast as a move; the rule is one constant and one helper shared by pointer move, pointer resize and keyboard pickup.
* fix(studio): keep a head edit's thumbnail refresh through a replaced conflict
A newer blocked change replaces the held one, so it now inherits the thumbnails the held one owed.
* fix(studio): keep a held head edit's thumbnail refresh when a later write saves cleanly
The clean-save path and the failed-cleanup path now merge the held blocked change's affected compositions, and the blocked-state merge runs inside the state update so two conflicts in one tick cannot lose the first.
* chore(studio): keep useTimelineClipDrag under the 600-line cap
* feat(studio): cap by the clips an edit would change, and cut the added comments
The 3-clip rule counts the clips a move or trim would write to: a locked clip swept into a move stays put, and a trim some member cannot take edits only the grabbed clip. Comment share is back at or under the base in every changed file.
* fix(studio): clear fallow audit and comment length on the multi-select cap
* fix(studio): keep bench script comments under the block limit
* fix(studio): keep the failed file's payload when another file's snapshot delete fails
* fix(studio): parse modern CSS colors without a new dependency
* refactor(studio): one colour parser for the text toolbar, stricter rgb numbers
The inline text toolbar kept its own canvas fallback for named colours.
parseCssColor now resolves them, so the toolbar uses it alone.
rgb() channels must be well-formed numbers; that replaces the NaN check
after parsing and rejects rgb(1. 2 3) as CSS does.
* refactor(studio): drop comment lines the comment ratchet refuses in colorValue
* fix(studio): linear-time rgb() patterns and no new colour literal in the parser
The rgb() number pattern now has one way to match each digit run, which clears
CodeQL's slow-regex alerts. The canvas reset uses transparent so the Studio
colour-literal ratchet stays at its baseline.
* fix(studio): legacy colours resolve in browsers without relative colour syntax
parseBrowserColor reads the colour itself from the canvas first and accepts a
#rrggbb or rgba() result, then tries color(from ...). Named, hsl() and percent
rgb() colours resolve again in the text toolbar on Chrome before 119 and Safari 17.
* test(studio): a colour the canvas rejects parses to null, not transparent black
* fix(studio): an inserted gradient stop takes the colour the gradient paints there
The stop colour is color-mix() in oklab, or srgb when both stops are legacy,
resolved by the browser, so it matches the painted midpoint including
premultiplied alpha; the channel blend stays as the fallback. The parser tries
color(from ...) before the legacy read, keeping alpha precise, and calls
CSS.supports inside its try. A Chrome-147-shaped fake canvas covers the modern
path in unit tests.
* test(studio): keep the fake browser canvas inside its test file
* fix(studio): mix an inserted gradient stop in the space the whole gradient uses
* fix(studio): let an unresolved var() stop keep a legacy gradient in sRGB
* fix(studio): only a bare var() stop leaves the gradient space to its neighbours
* fix(studio): pick light-dark() text colour from what the element paints
* refactor(studio): drop the neighbour clause the whole-gradient check already covers
* fix(studio): resolve the picker colour from the element that paints the selected text
* fix(studio): read the picker colour from the character the swatch colour came from
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* ci(studio): measure any edit cases on quiet runners, five runs in parallel
* perf(studio): an undo or save stops re-rendering every panel
* test(studio): the edit bench types one key per frame, as a person does
* fix(studio): the edit bench measures frame work when a task outside the drag has no thread time
* refactor(studio): hooks hand back stable handlers themselves, keeping App.tsx under 600 lines
* test(studio): the edit bench says why a case's frame work is unknown
* fix(studio): the edit bench counts a task without thread time at its wall time
* perf(studio): draw toasts on an opaque surface without backdrop blur
* fix(studio): keep stable handlers bound to the project they were made in
* fix(studio): every stable handler names the project it belongs to
* style(studio): format the stable handler call sites
* refactor(studio): name the timeline editing handler objects before wrapping them
* feat(studio): preview new tracks at drop boundaries
* fix(studio): preserve group ownership across overlapping moves
* refactor(studio): share timeline drop decisions and fixtures
* fix(studio): open an empty audio zone below visual tracks
* fix(studio): cancel thumbnails when project pages leave
* refactor(studio): share thumbnail regression setup
* test(studio): gate thumbnail socket release in browser CI
* chore(studio): keep optimistic revision comments within ratchet
* fix(studio): pin regression proxy requests to its backend
* test(studio): bound the proxy origin probe
* test(studio): probe proxy origin through the ready document
* docs(studio): map filmstrip clip rendering contract
* feat(studio): give timeline clips a separate filmstrip name band
* test(studio): require the filmstrip picture wrapper
* fix(studio): keep fallback clip warnings clear of menu badges
* fix(studio): reserve clip text space beside status badges
* fix(studio): separate clip FX and fade hit areas
* fix(studio): separate clip tool pointer targets
* fix(studio): reserve FX space without lowering fades
* refactor(studio): trim fade geometry measurement
* test(studio): refresh fade target after closing FX menu
* test(studio): collapse keyframe lanes before pointer witness
* test(studio): register filmstrip control browser gate
* test(studio): score edits on GSAP-animated size, scale, rotation, keyframes and from/fromTo
* test(studio): settle the edit bench only once no shadow preview is waiting to be swapped in
* test(studio): end a drag at its recorded pointer-up, even when no frame painted while held
* ci(studio): run the keyframed edit accuracy cases in CI across 20 shards
* refactor(studio): split the keyframe render and stray-css checks into small helpers
* test(studio): bank the edit bench's smooth verdict as smooth, judged on the worst frame's work
* style(studio): name the edit gate's smooth count helper
* test(studio): count smooth edit cases only among the accurate ones
* test(studio): only keyframed cases wait for the preview swap, so others keep main's undo timing
* refactor(studio): keep the edit bench's keyframe check apart from the hidden preview scan
* test(studio): check the keyframed cases are in the full grid without pinning its size
* test(studio): quarantine the nudge sequence whose redo waits behind preview videos (#4889)
* test(studio): bank the 255 keyframed edit cases that pass
* fix(studio): a hand drag moves only with its own pointer pressed
Chromium re-sends the last known pointer point with no button held after a
layout change. The overlay's move handler took those as drag travel, so the
element flipped between the pointer and that stale point, and the release
committed whichever came last.
* test(studio): the edit accuracy grid drops a drag after a buttonless move at the press point
Nine pathstray cases: a 180,90 drag whose last move before the release is a buttonless pointermove back at the press
point, as Chromium resends after a layout change. Main saves the element where it started; this head saves it where
the pointer let go. The frame sampler ignores that untrusted move as pointer position.
* fix(studio): keep the drag rule's comment to one line
* fix(studio): crop handles and the marquee band also ignore a buttonless move mid-gesture
* test(studio): bank the nine stray-move cases in the edit accuracy baseline
* test(studio): one crop drag helper, the band's candidate refresh in its own step, and formatting
* style(studio): wrap the pointer gesture comment under 120 characters
* fix(studio): Renders dropdown options take a mouse click again
* test(studio): name a Select whose list never opens and keep both Studio layout checks running
* fix(studio): a text edited in place shows its caret in a scaled preview
* fix(studio): the drawn caret follows a resized preview and stands right at bold words and empty lines
* fix(studio-server): a write receipt's replaced version comes only from the bytes it overwrote
* fix(studio): the drawn caret stands on the selection inside a transformed canvas
* test(studio): unmount the caret tests' roots before clearing the page
* test(studio): the caret gate waits for each key to land and retries a text that does not open
* test(studio): the caret gate checks what the DOM can tell at a soft wrap, and shows a text that would not open
* test(studio): the caret gate opens a text with a double press when click and Enter do not
* fix(studio): the drawn caret follows a pan or zoom of the preview
* fix(studio): the inline text style bar stands by the selected text inside a transformed canvas
* fix(studio): after End at a soft wrap the drawn caret stays at the line's end, as the system caret does
* fix(studio): only End keeps the drawn caret at a wrapped line's end, as Chrome's own caret
* refactor(studio): share the test mount helper and split the caret's placement for the code gates
* docs(studio): trim the drawn caret's comments to the package's share
* fix(studio): open inline edit on styled text whose nodes come from another window
* refactor(studio): name the realm-safe html check instead of commenting it
* docs(studio): shorten the caret fixture comment to the 120-character limit
* fix(studio): undo paints a move or nudge back at once while its save is still running
* test(studio): the edit accuracy gate judges the two part C sequences again
* fix(studio): a save lasts until undo can paint it from the history
* test(studio): the bench reads the box after a key's frame, as a user sees it
The nudge step already waits a frame per key (sequences.mjs:162); undo and the text step's Enter now do too.
* refactor(studio): say which registrations a pending edit adopts
* fix(studio): resizing an element GSAP only positions saves its CSS width and height
* fix(studio): a resize never saves GSAP's scale mask as the element's CSS
* fix(studio): keep the resize size writer inside the file and comment budgets
* fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders
* fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it
* test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview
* fix(studio): a move keeps an authored xPercent centring set in the saved file
* fix(studio): a drag on a centred element no longer flashes off-centre on release
* fix(studio): a font pick saves the edit and its @font-face in one write
* fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit
* test(scripts): pin that a slash after i-- before a quoted slash is division
* fix(studio): an inspector edit saved while a preview reload loads stays on screen
* fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it
* fix(studio-server): a font name or path with braces in quotes still saves its @font-face
* fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule
* test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass
* fix(studio): undo right after an edit undoes that edit, even before its save lands
* fix(studio): undo right after a nudge undoes the nudge
* fix(studio): undo during a nudge's wait for more keys never paints the edit before it
* fix(studio): a failed save never blocks undo, and a nudge burst always ends its pending edit
* fix(studio): undo waits out every failed or conflicting edit, and Delete counts as pending
* fix(studio): every GSAP script commit counts as a pending edit, so undo waits for it
* fix(studio): keep App under the file size limit
* test(studio): bank the nudge-then-undo sequences the gate now passes
* fix(studio): the timeline scroll gate catches dropped frames in the virtualized view
* fix(studio): the timeline scroll gate measures a failed arm once more before failing
* fix(studio): the virtualized timeline gate holds interactions to 58.3 ms
* fix(studio): an undo that lands mid-drag keeps the box under the pointer
* fix(studio): an undo that lands on a pressed layer keeps what its gesture draws
* fix(studio): every gesture mark names what it draws, and a text edit draws nothing
* test(studio): gate the drag, undo, drag cases again now that their race is fixed
* fix(studio): a text edit's gesture mark draws nothing, so an undo under it reverts
* test(studio): keep the one drag, undo, drag case whose press can still jump in quarantine
* test(studio): bank the drag, undo, drag cases that now pass