* fix(studio): let the host name Split's key in the clip menu
* fix(studio): editor shell and timeline pane forward the Split key label
* fix(studio): drop two added doc comments the comment ratchet refuses
* refactor(studio): the auto-id write posts through the one patch-element client
* test(studio): pin the auto-id write's saves, refusals and unsafe targets
* test(studio): pin the auto-id write's plain refusal and missing changed flag
* test(studio): the auto-id write needs a toast handler and sends the write token
* refactor(studio): one request for every GSAP mutation write
* fix(studio): a refused id write no longer leaves the id on the previewed element
* style(studio): drop the two comments the names already carry
* fix(studio): keep minting the id on the element so overlapping adds never share one
* fix(studio): an id the file already holds counts as saved, and no project leaves no id
* fix(studio): the server picks an added effect's element id, so ids never repeat
The browser minted the id from the live preview and rolled it back when the
write failed. A reply lost after the id was saved left the file holding an id
the preview had dropped, so the next add on a sibling wrote the same id again;
and a refused add could strip the id an overlapping saved add had just given
the same element.
A new patch-element op, ensure-id, keeps the element's id or makes the proposed
one unique in the file, and every patch-element reply names the element's id.
Studio sets the id on the preview only from that reply, so there is nothing to
roll back. nextUniqueId moves to sourceMutation as the one owner (split and
duplicate already used it).
* test(studio): pin the gsap write token and the refused-write toast
* refactor(studio): one unique-id helper on the server, and shorter id comments
* style(studio): keep the id helper doc under the line limit and the test-utils header true
* fix(studio): an added effect's id stays unique across every composition file
* style(studio): keep the files route's comment share at the base
* fix(studio): count an id written right after a quote or slash as taken
* fix(studio): a quote-ended string no longer hides another file's id
* feat(cli): feedback says whether a person or an agent wrote it, with the email an app attaches
* refactor(cli): split the .env loader so each part stays simple
* fix(cli): a broken .env loader fails loudly; only a missing .env is ignored
* fix(cli): publish with an expired login stops and asks you to log in again
The server now answers 401 to a rejected credential instead of publishing
anonymously, so the CLI says so and drops the warning for the old fallback.
* fix(cli): publish refreshes an expired login first, and names a rejected API key
An expired access token with a refresh token is refreshed (the cloud commands
already do) instead of being sent and refused. Only the requests that carry the
credential map a 401; an environment API key is named, since logging in would
not replace it.
* fix(cli): an expired login without a refresh token asks you to log in again
A stored login past expiry with no refresh token now resolves as
LOGIN_EXPIRED instead of "not signed in", so publish stops with the
re-login message before sending anything. A signed-in publish that the
server answers with an unclaimed project fails the same way and links nothing.
* fix(cli): keep the resolver comment share at its base
* fix(cli): an API-key publish still succeeds, and only an owned publish is linked
* fix(cli): --update and --space refuse an API key instead of being ignored
* fix(cli): publish uploads with the credential it checked, so --update and --space are never dropped
* fix(cli): a login that expires during the bake is refreshed, not reported as expired
* fix(cli): an expired login that cannot refresh stops publish before anything is sent
* fix(cli): a token refresh never writes over a login that replaced it
* fix(cli): a refresh that cannot read the saved login reports that, not a changed login
* fix(studio-server): freeze still appears whole, so a slow extraction cannot break its undo
The freeze route reserved the still's final name with an empty file before
ffmpeg ran. When extraction outlasted the history's quiet time, history
committed that empty file as an outside change, and Cmd+Z then restored a
0-byte PNG. ffmpeg now writes under an atomic temp name the history skips,
and the still is published with createFileAtomically (EEXIST still refuses
a taken name).
* fix(core): a direct exclusive write that fails midway leaves no partial file
Without hard links, createFileAtomically writes the destination directly.
A full disk then left a partial file behind (a corrupt upload or freeze
still); it is now removed, and a name another writer holds is never
touched. Freeze tests: the slow-extraction test leaves a half-written temp
file while history commits, and concurrent extractions meet at a barrier
instead of a real timer.
* fix(core): a quota reported only at close still removes the partial file
* test(core): the direct write closes once and keeps its write error when cleanup fails
* test(core): a failed direct write closes its descriptor exactly once
* refactor(studio-server): freeze names carry what their comments said
* fix(core): a call on the playhead fires once when the preview redraws it
The silent redraw steps off the playhead and back, which re-armed a call sitting exactly on it, so the following seek fired it again. Keep each such call's fired state across the redraw.
* fix(core): a redraw keeps every call's fired state, wherever the call sits
Restoring one field for calls found by position missed a fresh call at 0, calls in repeating or reversed timelines and double-speed scenes. Snapshot ratio and _zTime of every zero-duration callback tween instead.
* fix(studio): an audio track can be muted again after it is unmuted
The audio track header showed its toggle only while the track was muted, so a track unmuted once could never be muted again. The toggle now shows on every audio-only track as a speaker (Mute track N / Unmute track N), decided by the same rule the undo entry already used.
* refactor(studio): one owner for the track toggle words, drop comments the mute fix made false
* fix(studio): an audio clip or group can be muted and unmuted from the property panel
The panel offered its hide toggle on audio only while already hidden, and a selected <hf-audio-group> had no toggle at all (it has no timeline row). The panel now offers Mute/Unmute both ways; a group writes data-hidden through the group attribute writer. Track rows, the panel and their undo entries share one verb and icon helper.
* style(studio): format the panel test and keep the comment share flat
* fix(studio): the panel's mute label follows the write, without a reselect
* fix(studio): after a panel mute, refresh what is selected now, not the muted item
* perf(studio): read the selection ref from the actions context so the inspector does not re-render on hover
* refactor(studio): move the panel's mute toggle into usePanelHiddenToggle
* feat(registry): eight ad templates show their video preview in the catalog
* refactor(scripts): the preview backfill reuses the shared entrypoint guard
* fix(studio): load MotionPathPlugin at the composition's own gsap version
* fix(core): list the gsap-cdn subpath where core's exports are generated from
* fix(core): every default gsap URL comes from the one gsap-cdn owner
* fix(core): the gsap a new composition or first tween gets comes from the gsap-cdn owner
* fix(studio): undoing a freeze frame removes the still it made
* fix(studio-server): a failed frame extraction leaves no partial still
* fix(studio-server): a freeze claims its still's name before extracting
* fix(studio-server): a freeze past the media's end fails instead of saving an empty still
* refactor(studio-server): freeze route names say what its comments said
* fix(engine): fail the render when an injected video frame cannot decode
* fix(engine): report BeginFrame capture errors at once, not after the stall watchdog
* fix(engine): skip the diagnostic screenshot for any session launched for BeginFrame
* 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.
* perf(core): a paused Studio drag lets the preview loop sleep until the drag ends
A paused manual-edit gesture marked a reconciling seek as owed, and an owed seek kept the transport ticking every frame for the whole drag: timing reads, a full clip-manifest post every 20 frames that Studio rebuilds its clip list from, and media rebinds every 30. Nothing is owed until the gesture marker clears, and the gesture watch already wakes the loop at that moment, so the loop now parks mid-gesture.
* docs(core): keep the park comment to one line
* fix(core): releasing a paused-media lease wakes the parked loop to stop that media
Release only dropped the lease, so a parked transport kept letting the released element play until an unrelated wake. The lease test passed only because it started the media again after release, which fires a play event.
* test(studio): pin what the one DOM edit writer saves, records and reloads
useDomEditPersist had no test file of its own. These cases pin its current behaviour before the
patch-element and gsap-mutations clients are folded into one each.
* test(studio): pin the DOM writer's partial save, SDK fallbacks and save counter
* test(studio): pin that the DOM writer reloads only after its history record settles
* fix(studio): soft-reload a batch when one of its writes has no in-place patch
A corner resize saves size and position in one batch; only the position had an in-place patch,
so the preview patched it and skipped the reload, and the new size snapped back until reload.
* fix(studio): a deferred write without an in-place patch still reloads at the group's end
A group drag defers each member's preview sync and carries a missed patch to the final render.
A write with no patch at all never set that latch, so a last member that patched in place
skipped the reload and the earlier member kept its old position on screen.
* 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(core): load the runtime once per file so the entry tests' first timeout covers only its own work
* fix(core): a torn-down runtime's pending readiness check no longer touches the next document
* fix(core): a seek onto a tween's start shows that tween, not the earlier one's end
The seek re-render stepped to just below the target and back. A child
starting exactly at the target sits at local time 0 both before and after
that step, so GSAP skips it going forward and an earlier tween on the same
property keeps the frame (fromto edits at the playhead read the from()
end, 35-50 px off). One forced silent render at the target walks every
child forward in authored order instead, so same-time steps still apply in
order.
* style(core): format the seek re-render change
* fix(core): re-render a seek from just above too, not with a forced render
A forced render at the seek time redrew every tween GSAP had ever rendered,
including ones that start later, at their recorded start values: after a
seek past the end, a later tween's start showed early (style-5-prod,
hdr-regression). The re-render now steps silently just above the target,
then just below, then to it. The step up makes a child that starts exactly
at the target cross its start on the way down, so it renders; tweens that
have not started are left as the seek put them, and the last move is still
forward, so same-time steps keep their authored order.
* fix(core): re-cross only the tweens that start at the seek time
The step up past the target (d5c5d797) initialised tweens starting within
the next millisecond, so an overwrite: "auto" tween killed the one before
it, and it crossed a repeatRefresh boundary sitting on the target. The
re-render is back to #4911's step from just below. That step skips a
keyframes or stagger tween already at its start (GSAP passes no start
crossing into the tween's own timeline), so after it each such tween that
starts exactly at the target, at any nesting depth, re-crosses its own
start alone. No other child moves.
* refactor(core): keep the GSAP members the re-cross reads local to the adapter
The shared child type gained members that clash with RuntimeTimelineLike in
the runtime typecheck. The adapter now types them locally and skips any child
that is not a GSAP animation, which also keeps the walk simple.
* fix(core): re-cross only forward-playing tweens, by a microsecond
GSAP skips paused and reversed children when it renders a timeline, so the
re-cross does too: it read a reversed keyframed tween's start instead of its
end and reset a paused one. The step past the start shrinks from 1 ms to
1 us, so it cannot start a stagger's next target or cross a short tween's
repeat.
* fix(core): re-cross only keyframed tweens, by less than GSAP's time step
A 1 us step still reached a stagger target 0.5 us in and a 0.4 us repeat,
both sizes GSAP can represent. The step is now 2e-8: above GSAP's zero
threshold, so the tween leaves its start, but below its 1e-7 time rounding,
so the tween's own timeline does not move. Only keyframed tweens are
re-crossed; a stagger is left as main renders it.
* fix(core): move keyframed tweens across their start before the forward step
Re-crossing a keyframed tween after the timeline's forward render made it
win over a later same-time sibling (a set after it at the same time showed
the keyframe, not the set). The keyframed tweens that start at the target
are now moved across their start during the step from below, and the
timeline's own forward step renders them with every sibling, in authored
order.
* ci: a test-only change under studio or player needs no captures
Tests are not behaviour a user sees, like the markdown the check already skips.
A bench-harness PR (#4953) was asked for Before and After screenshots.
* ci: pin a mixed source and test change, and say tests are not counted
* 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
* fix(cli): close every browser on SIGTERM and SIGINT through one launch owner
* fix(cli): own SIGTERM before the GPU probe browser launches
* fix(cli): keep tests on the managed GPU mode and drop the engine signal flag
* test(cli): give the browser fallback fake a process handle
* test(cli): signal the GPU probe case only once the signal owner is installed
* test(cli): mark the launching case only after the signal owner is installed
* fix(studio): a hovered menu row fills its row, with no band around it
* refactor(studio): one shared menu style owns every context menu's panel, rows and dividers
* fix(studio): menus clip their rows to the panel's corners and cover every hand-built panel
* fix(studio): asset menu Delete shows red, trim menu owner comments, test submenu placement
* docs(studio): shorten a submenu comment
* feat(studio): a host can hide the clip menu tools without losing the edits fades need
* test(studio): the clip menu tools fixture is splittable
* test(studio): the default clip menu shows Voice, the control for hiding it
* docs(studio): shorten the menu owner comment
* docs(studio): drop comment lines the ratchet counts
* docs(studio): the menu owner carries one comment
* style(studio): format the files the repo formatter listed
* test(studio): the menu style scan names files with forward slashes on Windows too
The seek path skips its silent re-render on timelines with a zero-duration callback, because that
re-render re-arms the callback and the next forward seek fires it again. The answer was cached per
timeline on the first seek, so a tl.call added later was never seen: the re-render ran and the call
fired twice. The check now reads the timeline's current children on every seek.
* fix(core): music keeps playing smoothly across video cuts in preview
While no Web Audio buffer plays, the preview playhead follows an audio element. It followed the first
one in page order, so a sub-composition whose shots each carry a short sound clip took the playhead
away from the music bed. At every cut the next shot's sound starts late, the playhead waited for it,
the music played on, and media sync then pulled the music back: a jump at each cut. The followed clip
still keeps the playhead; otherwise the in-window clip that runs longest takes it.
* refactor(core): check the followed clip first and drop the clock pick's flag
* fix(core): an ended clip never leads the playhead, and the followed clip keeps it
A bed whose file ends before its authored length no longer wins the pick and leaves the playhead on wall
time. Pins the rule that a longer clip starting late cannot take the playhead from the followed one.
* fix(ci): let markdown-only changes under studio and player skip the captures check
* docs: map the Studio edit path, the edit accuracy bench and test setup for agents
* docs: name the Studio captures rule and the local comment checks
* docs: point the Studio gate notes at the scripts that own the rules
* docs: correct the Studio save paths and add the producer stage map
* docs: name where producer sizes workers and which captures encode inline
* 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(media-use): forward TTS speed to Kokoro
synthesizeOne built the `npx hyperframes tts` argv without --speed, so a
request speed (audio_request.json `speed` or audio.mjs --speed) was ignored
on the Kokoro path and every line rendered at 1.0. Forward it when it is not
the default.
Kokoro now enforces the CLI's speed range, so audio.mjs rejects a speed
outside (0, 3] up front instead of dropping every line with an opaque
subprocess exit.
Fixes#4926
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(media-use): reject zero and non-numeric TTS speed
`Number(x) || 1.0` turned 0 and non-numeric speeds into 1 before the range
check, so they passed silently. The engine now parses the raw request or
--speed value once, with no fallback, and the faceless-explainer,
pr-to-video and product-launch-video adapters forward the raw flag instead
of applying their own copy of the fallback.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* test(cli): time history turns on one injected clock instead of real waits
* fix(studio-server): end an idle history window by its own clock, not a bare timer
* fix(studio-server): cap a history window's idle timer at Node's timer range
* fix(studio): show each timeline row its own layer instead of a grey bar
A row got a frame only if it spanned under 92% of the film and its id did not
look like a backdrop; every other row drew a plain grey bar. Every element row
now gets its frame, and the capture fades the row element's siblings first, so
the frame shows that layer and not the whole film. Studio's dev server and the
CLI share one capture helper; Studio's own copy is gone.
* fix(studio): read timeline rows from any realm so each keeps its selector
* refactor(studio-server): always isolate the element in a thumbnail clip
* fix(cli): undo a thumbnail's isolation before its page takes the next capture
* chore(core): generate the dom-realm export and trim thumbnail clip comments
* fix(studio-server): restore sibling opacity after a clip, hash selectors in cache keys
* fix(studio-server): keep faded siblings in a page-owned list so restore is exact
* feat(studio): draw a text layer row as its own live text instead of a captured picture
* refactor(studio): move timeline text reading into its own module
* fix(studio): keep text rows readable and current, and leave painted layers as pictures
* fix(studio): refresh a canvas-edited text row by its own identity and source file
* fix(studio): refresh a canvas-edited text row known only by its data-hf-id
* fix(studio-server): a rename rewrites only references that name the renamed path
* fix(studio-server): rename keeps root leads and reads escaped separators
* fix(studio-server): rename matches whole references only and cannot backtrack
* fix(studio-server): rename leaves paths that exist alone, whatever their names hold
* fix(studio-server): rename checks every existing path once and reads escaped spellings
* fix(studio-server): rename finds existing longer paths by lookup, on normalised text
* fix(studio-server): rename inventories every path and indexes lengths per suffix
* fix(studio-server): rename inventory follows linked folders
* fix(studio-server): rename classifies its source before moving it