Commit Graph
5075 Commits
Author SHA1 Message Date
Miguel Ángel a2efe7b46d fix(studio): timeline lanes hold still while a clip drags onto a new track (#4892)
* fix(studio): timeline lanes hold still while a clip drags onto a new track

* fix(studio): name the timeline pin guards instead of commenting them
2026-10-02 00:55:41 -07:00
Vance IngallsandClaude Opus 5.5 70900216f0 feat(studio): audio gain on G, speed in clip names, additive selection and partner clamp (8/8) (#4821)
* feat(studio): show clip speed in the clip name as [150%] or [ramp]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): one fx badge per clip replaces the per-tool and link badges

Grey with no effects, white with any. The tooltip lists look, voice, other
fx, crop and ducking; a click opens the clip menu. The volume badge stays
when volume is off 100%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): premiere-style audio gain dialog on G

Set Gain to, Adjust Gain by, Normalize Max Peak to, Normalize All Peaks to
and Normalize loudness to -16 LUFS, with the selection's peak amplitude read
from the peak map. Writes data-volume on every selected clip with sound as
one undo step. The clip menu's Normalize loudness becomes Audio Gain... (G).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): use a theme blue for the audio gain OK button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep shortcuts out of the audio gain modal; refuse while recording

Keys pressed inside an aria-modal dialog no longer reach the global hotkeys,
so Delete on the focused OK button cannot delete the clips the dialog edits.
The multi-clip write refuses during a recording like the single-clip one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): offer link for any unlinked video and audio pair

Link no longer requires the same file and identical timing, matching Premiere.
Same-file pairs get a sync origin so a trimmed or moved audio shows its
offset; different files link without one. Merge back is disabled until the
pair is in sync. Linked moves keep the offset, and a trim only carries to a
partner whose edge is at the same time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): shift and cmd-click add timeline clips to the selection

A modifier click on a clip replaced the selection, so a video and an audio
could never both be selected and Link never appeared. Shift, Cmd and Ctrl
now toggle the clip (and its link partners unless Alt) in the selection;
Shift no longer turns a clip click into a time-range popover.

Right-clicking a lone unlinked video or audio offers Link when exactly one
opposite-kind clip from the same file sits on another track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep audio inside its partner video and make cmd-click add in every host

A modifier click passed the toggle as an argument to an optional
onSelectElement call, so hosts without that handler (desktop) never ran it.

An audio clip linked to a video, or sharing its sync origin, now stays
inside that video's span while dragged or trimmed, Alt edits included,
unless the video moves or trims in the same gesture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): hold the drag actor at the partner-video clamp

The dragged audio's ghost followed the pointer past the video's end while
the landing stayed clamped; it now stops where the clip will land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): preview a look on hover in the clip menu before applying it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): trim comments the clip-link names already carry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): unlinking clears the timeline selection

After Unlink both clips stayed selected, so the next edge trim ran the
multi-select group resize and the former partner still followed. Unlink
from the menu, the shortcut or an Alt edit now leaves nothing selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): keep a linked partner's end when a trim changes the grabbed duration

Copying the grabbed clip's duration to a partner that starts later pushed the
partner's end past the clip both ended on. The partner's duration is now the
shared new end minus its own start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep every moving detached audio inside its video, after placement too

The containment clamp ran before collision placement and only for the
grabbed clip, so a drop pushed past an obstacle, or audio carried along in a
multi-selection, could land outside its video. The allowed shift is now the
intersection over every moving audio, and a placement outside it is
rejected. Link partners are matched within their source file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): carry the normalize refusal and save outcomes into the gain dialog

Normalize moved from the clip menu into the Audio Gain dialog here, so the
volume-lane refusal and the saved/refused/failed checks go with it: a lane
owned clip is refused, and a failed single or multi-clip save rejects instead
of reporting success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): trim comments the gain and multiselect layer added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(sdk): split the linked partner's duration out of partnerTiming

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): refuse a linked trim whose new end would cross the partner's start

With linked video [2,8] and audio [3,8], setTiming(video, {duration: 0.5})
computed a partner duration of -0.5, skipped it, and committed the video alone,
leaving the audio outside its video. The partner edits are now planned before
any mutation; a non-positive partner duration throws ("Linked audio would start
after the new end — unlink or trim the audio first.") and can() reports
E_LINKED_PARTNER_CROSSED, so nothing is written and the undo stack is
untouched. In Studio the refusal fails the cutover (never declines into the
legacy path) and the timeline resize toast shows the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): key a linked edit's grabbed baselines by composition scope

planLinkedTiming stored each target's timing under its raw data-link id, while
linkedPartnerIds scopes link ids to their host composition. One setTiming naming
a root video and a nested video that both use lk-1 kept only the nested baseline,
so the root audio was skipped and left past its trimmed video. Baselines are now
keyed by host scope and link id, the same rule linkedPartnerIds uses, for both
the partner trim and the crossing refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(sdk): run the two-scope grabbed-baseline cases on an inline composition too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:19:53 +00:00
Miguel Ángel 6388845ac9 fix(studio): a hand drag moves only with its own pointer pressed (#4880)
* 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
2026-10-02 00:08:20 -07:00
Miguel Ángel 2b495e29dd Merge pull request #4893 from heygen-com/release/v0.8.109
chore: release v0.8.109
v0.8.109
2026-10-02 00:04:05 -07:00
Miguel Angel Simon Sierra 6691e59b1a chore: release v0.8.109 2026-10-02 00:02:36 -07:00
Vance IngallsandClaude Opus 5.5 45b8e171e2 feat(studio): sync origin, out-of-sync repair, linked selection toggle and grouped clip menu (7/8) (#4820)
* feat(studio): order the clip menu into time, sound, picture and delete groups

Split and Freeze frame; Normalize, Voice, link items and Duck; Look and Crop;
then Delete beside Delete this clip only. Each item file renders its pieces
separately, empty groups collapse, and the viewport clamp uses the measured
menu height instead of a row-count estimate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): mint freeze-frame right-half links with the shared lk-N helper

Freeze frame now calls core relinkSplitHalves, so split halves and freeze
halves share one link-id convention instead of L -> L-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): apply speed edits to every data-link member in one undo step

The inspector speed slider, rate lane edits and ramp presets on a linked clip
now commit once, writing the selection and, via expandToLinkedMembers, each
partner from its own current value. Unlinked clips keep their usual path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): drop the audio parent tick in favour of the data-link chain badge

A linked clip now shows one link indicator: the chain badge driven by
data-link. The waveform no longer draws its 1px parent tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): follow late clip-menu rows and target link partners like S2

The menu height follows a ResizeObserver so rows that read the live preview
after mount still clamp inside the viewport. Speed fan-out builds partner
targets with buildPatchTarget, matching the link edit plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): show the sound strip and audio menu items without studio's shell

The audible-video waveform strip drops AudioWaveform's 16px label band, which
left a 0px canvas on short lanes. Normalize loudness and Duck under voice fall
back to the timeline session's project, the live preview iframe store and a new
onNotice edit callback when no StudioShellProvider is mounted.
usePreviewIframeStore is exported so a host can publish its preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): show duck under voice only when a voice overlaps

The item now appears only when an overlapping voice exists or the bed is
already ducked. Its check mark moves to the right edge so unchecked rows
align with siblings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): name the partner in the clip menu link and unlink items

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): clip menu stops offering unlink once the clips are unlinked

The timeline skipped store updates when only data-link changed,
so the menu kept reading the stale link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(core): sync origin, offset math and move/slip into sync for linked pairs

data-sync-origin survives Unlink; split gives right halves a fresh origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): split re-mints the sync origin of unlinked pairs too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): detach, link and keep-sound cutout write a sync origin that unlink keeps

TimelineElement.syncOrigin is read on both parse paths; merge back drops it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(studio): expect sync-origin on keep-sound cutout write

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): linked selection toggle in the timeline toolbar, on by default

Off: a click selects one clip; drag, trim, razor, speed and delete stay on it
(delete unlinks the survivor). Alt-click still selects one half when on.
Right-click holds the out-of-sync indicator preference. Both persist in UI prefs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): link groups share a label colour; out-of-sync badges with move and slip

Red badge on both halves of a sync-origin pair that drifted (frames, s:ff past
a second); hidden when rates differ or the preference is off. The whole badge
opens Move / Slip into Sync; each edits that clip alone in one undo step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): take link label colours from theme tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sdk): syncOffset, moveIntoSync and slipIntoSync for sync-origin pairs

Core gains findSyncPartner, the DOM pairing shared by the SDK and the CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(cli): timeline rows report syncOffsetFrames and print out-of-sync

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cli): the timeline row fixture carries syncOffsetFrames

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): data-sync-origin in hyperframes-core

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core,sdk): keep a one-sided cut's sync origin and slip the in-point playback reads

relinkSplitHalves reminted the right half's sync origin even when only one
member of an unlinked source pair was cut, so the uncut partner kept the old
origin and the new half lost its badge. The origin is now reminted only when
both the video and the audio were cut.

Slip into Sync wrote data-media-start whenever it existed, but playback
reads data-playback-start first, so a clip carrying both never moved. The
link timing reader now uses playback's precedence and the slip writes the
attribute that wins; lint compares the same field. A sync partner is also
looked up only in the clip's own composition (core) or source file (Studio).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): flip the out-of-sync menu above the pointer near the bottom edge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: trim comments the sync-origin layer added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(core): split the scoped candidate lookup out of findSyncPartner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core,studio): find sync partners inside the link scope

findSyncPartner and Studio's syncPartnerOf now scope by the same rule as
link groups: the nearest data-composition-id or data-composition-file
ancestor, plus the row's compositionScope in Studio. An origin reused in a
file host or an inline composition no longer pairs across it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:08:37 +00:00
Miguel Ángel b5e3ffa6a7 fix(studio): soft reloads restore what GSAP wrote and holds from the saved files (#4871)
* fix(studio): soft reloads put a nested element's tweened style back from its own file

* fix(studio): timing edits, held elements and DOM-building scripts reload from the files

* refactor(studio): one owner settles the nested file reads for every soft reload
2026-10-01 23:24:32 -07:00
Miguel Ángel 6dcc300fd2 fix(studio): Renders dropdown options take a mouse click again (#4890)
* 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
2026-10-01 23:22:06 -07:00
Vance IngallsandClaude Sonnet 5.5 0559be0b13 feat(studio): link a video and its audio with data-link (6/8) (#4818)
* feat(core): add data-link media link helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(lint): warn on out-of-sync and orphaned linked clips

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): select, drag and trim linked clips together

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): detach, merge, link and unlink source transforms

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): link-aware split, delete, alt-edit unlink and link edit actions

Split and delete fan out to link partners. Alt drag/trim edits one member
and unlinks it. onLinkEdit runs unlink, link, detach and merge as one undo
step each. Single-clip timing edits of a linked clip skip the SDK path so
its linked default cannot move the partner behind the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio-server): give split right halves of a linked pair their own link

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): clip menu detach, unlink, link, merge back and delete-one items

Items live in clipMenuLinkItems.tsx with a one-line hook in ClipContextMenu;
the same resolver drives the Cmd+L, Opt+Shift+D and Opt+Delete shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(sdk): setTiming applies to data-link partners by default

{ linked: false } edits one member and unlinks it, matching Studio's Alt-edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): document data-link linked clips in hyperframes-core

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(studio): keep comment share at or below base in link plumbing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(studio): audio pill link follows data-link, not a shared file name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): alt-click selects one linked member even inside the pair selection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(lint): drop the rule-list comment for the linked clip rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk,lint): keep a link group inside its own composition

Link ids are minted per file, so lk-1 is normal in the root and in a child
composition. The SDK's linked-timing query descended into nested hosts and
addressed every match with the initiating scope, so moving a root clip moved
the child's pair too; lint grouped them into one drifting group. Both now
group only members of the same composition scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): match linked media by full asset path, scope links, keep hidden sound silent

Merge and Link compared only a lowercased basename, so assets/one/talk.mp4
and assets/two/talk.mp4 enabled a destructive Merge. Clips now compare the
src resolved against each clip's own source file, and Merge re-checks the
two srcs in the source before it runs. Link membership in the timeline
(linked members, gesture expansion, unlink orphans) is scoped to a source
file. Detach and the keep-sound cutout carry a hidden video's data-hidden
to its audio, and Merge refuses when only one member is hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): move the timeline move-edits handler into its adapter

App.tsx sat at 600 lines on this layer and 602 once merged with main.
useTimelineMoveEditsHandler now lives beside persistTimelineMoveEditsAtomically,
same callback and dependencies, bringing App.tsx to 592.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk,studio,lint): keep link groups inside inline compositions

A link group now stays inside the nearest data-composition-id or
data-composition-file ancestor. The SDK used to recognise only file hosts,
and Studio compared only sourceFile, so an inline child composition that
reused a link id was edited together with the root group. core/media-link
linkScopeOf is the shared rule for the SDK and Studio. Lint cannot import
core, so it uses the same two attributes directly. Studio rows record their
compositionScope, and link and merge refuse pairs that cross it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sdk): refuse a link edit whose partner address names another clip

A partner addressed in the caller's scope can resolve to a different element
when an inline composition reuses a root clip's data-hf-id. setTiming now
checks that each partner's address resolves back to that partner and throws
before mutating when it does not, instead of editing the root clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): a remote media url keeps its query when matching an asset

Two different videos at one remote path with different queries were treated as
the same file, so Merge could delete the audio and use the wrong video's
sound. Project-local srcs still drop query and hash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(studio): keep a remote url's query exactly as written when matching assets

Decoding the whole url folded %26 into & so two different media urls matched.
Decode only the path; the query is compared as authored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(studio): compare a remote url as serialized, decode only local paths

A remote pathname with an encoded reserved character (%3F) matched the same
path with a literal ?, though they can return different media. Remote urls
keep origin, path and query exactly; local srcs are still decoded and
stripped of query and hash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 05:23:25 +00:00
Miguel Ángel e205207583 fix(studio): a text edited in place shows its caret in a scaled preview (#4875)
* 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
2026-10-01 22:31:23 -07:00
Miguel Ángel df30bf8795 Merge pull request #4891 from heygen-com/release/v0.8.108
chore: release v0.8.108
v0.8.108
2026-10-01 22:21:29 -07:00
Miguel Angel Simon Sierra a63c8d0577 chore: release v0.8.108 2026-10-01 22:20:30 -07:00
Miguel Ángel c967a055fb fix(studio): keep a pick made just before a reload swaps the preview frame (#4888)
* fix(studio): keep a pick made just before a reload swaps the preview frame

* fix(studio): trust only frames that were the primary preview, one pick helper in the tests

* test(studio): a zoomed frame is not trusted once the zoom is gone

* refactor(studio): name the trusted frames instead of commenting them
2026-10-01 22:05:15 -07:00
Vance IngallsandClaude Opus 5.5 824e4100c6 feat(studio): waveform strip, clip light, peak marks and video beat source (5/8) (#4817)
* feat(studio): draw a waveform strip under video clips that carry sound

An audible video keeps its thumbnails and gains the audio waveform along the bottom
38% of the clip (also when thumbnails are off); muted and silent video are unchanged.
The waveform route already decodes video audio; a test now proves it on an mp4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): latch a CLIP light on the master meter at the export ceiling

The light turns red when the preview master sample peak reaches -1 dBFS (the export
true-peak ceiling) and stays lit until clicked. Mutation-tested threshold and latch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): paint the CLIP light from theme colour tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): mark where each clip redlines on its waveform

studio-server computes an absolute per-50ms sample-peak map once per media file
(native channels, cached by size+mtime next to the waveform cache); the timeline
paints red marks where the source at the clip's own volume reaches -1 dBFS and
names the loudest point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): let a video with sound be the beat source

Studio's isMusicSourceElement accepts an audible, unmuted video for the music track
and the beat fallback (lane zoning unchanged); core findMusicAudioSrc accepts
<video data-has-audio="true"> without muted; the beats CLI message mentions video.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): decode each peak map once and never offer Duck on a carve source

Concurrent peak requests for one file share a single ffmpeg decode; Duck under voice
is withheld on a clip another bed already carves against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): decode the same audio stream the peak map probes

The channel probe read stream a:0 while the decode left stream choice to
ffmpeg, which picks a later stereo stream over a mono first one; every frame
was then read at the wrong width, doubling the bins and taking the other
stream's level. Both commands now select a:0, and the cache key moves to
peaks-v2 so maps built from the wrong stream are rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): terminate the peak map's ffprobe options before the input

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): reject a peak map whose decode exits nonzero

A decode that emits some samples and then fails no longer resolves as a
complete map, so /peaks cannot cache a truncated clipping map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:43:26 +00:00
Vance IngallsandClaude Opus 5.5 9244c47328 feat(studio): normalize loudness and duck under voice, with a limiter report (4/8) (#4816)
* feat(studio-server): share loudness measurement with an absolute target and video support

normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness,
gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and
accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio-server): add a measure-only loudness normalize endpoint

POST /projects/:id/loudness/normalize measures a clip's played window (audio or
video) and answers the -16 LUFS data-volume plan; Studio writes the attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: register the loudness and audible-video subpaths in their descriptors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: format the loudness and audible-video subpath descriptors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): add Normalize loudness and Duck under voice to the clip menu

Normalize measures the clip on studio-server and writes data-volume as one undoable
edit; Duck toggles a carve against every overlapping voice (audio or video with
sound) at the default strength. Both live in clipMenuAudioItems with one hook-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): tighten carveOutput docs to the package comment share

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(producer): report how far the true-peak limiter lowered the export

The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered
by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio
servers carry in the complete SSE event and the /renders record. Also exports
useAudioMetersVisible from @hyperframes/studio for hosts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): scan HTML start tags without a backtracking regex

The start-tag regex backtracked quadratically on '<' followed by many spaces
(CodeQL js/polynomial-redos). A linear character scan reads the same name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): refuse to normalize a volume-automated clip and honour save outcomes

A volume lane owns the clip's gain in preview and render, so writing
data-volume changed nothing while the toast claimed success; Normalize now
refuses with a clear message. Normalize and Duck also check each quiet-save
outcome, stop at the first failed or refused write and report it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(core): drop the duplicate audible-video subpath descriptor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:51:34 +00:00
Miguel Ángel 880b4c41b3 fix(studio): undo paints a move or nudge back at once while its save is still running (#4877)
* 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
2026-10-01 21:02:03 -07:00
Vance IngallsandClaude Opus 5.5 000ccd426a feat(studio): speed presets, voice, look, crop presets and freeze frame in the clip menu (3/8) (#4815)
* feat(core): add gentle ramp-in, ramp-out and slow-mo-middle speed presets

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): add voice, look and crop clip-menu items and clip badges

Voice and Look are single-choice submenus writing data-fx-chain and
data-color-grading; None clears. Crop opens the canvas crop preset bar.
ClipBadges reads the live preview node and follows attribute edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): drop the unstyled badge classes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): add crop aspect presets to the canvas crop handles

Free, 16:9, 9:16, 1:1, 4:5, Reset and Done float above the clip opened from
the clip menu. Presets commit a centred inset clip-path; Reset removes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(studio): read the committed crop from the inline clip-path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio-server): add freeze-frame file mutation

Extracts the frame under the playhead with ffmpeg (media time honours
data-media-start, playback rate and a rate lane) into assets/freeze/, then in
one write splits the video, inserts a 2 s <img> still with the video's box
styles, shifts the rest of the track and splits a data-link partner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): import replaceFileAtomically from core atomic-file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): add freeze frame to the clip menu

Freeze frame on a video under the playhead posts the studio-server
freeze-frame mutation and records its before/after as one undo step.
onFreezeFrame is threaded through the timeline edit callbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): keep the freeze still inside assets/freeze

The clip id from the project HTML went straight into ffmpeg's -y output
path, so an id with ../ segments wrote outside the project. The id is now
reduced to one safe filename component, the output is pinned inside
assets/freeze before extraction, and the still's src is built from that
name rather than from a filesystem path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio-server): give every freeze still its own immutable name

Two clip ids that sanitise alike (a.b / a_b), ids sharing an 80-character
prefix, or a second freeze of one clip at the same playhead all produced the
same still path, and ffmpeg's -y replaced the first still's pixels. The name
now carries a hash of the raw id and a per-extraction token, the route
refuses a name that already exists, and ffmpeg runs with -n so it can never
overwrite. The freeze media time reads data-playback-start before
data-media-start, as playback does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:08:50 +00:00
Vance IngallsandClaude Opus 5.5 3c299acc7f feat(core): audible videos in preview web audio, audio groups, meters and carve (2/8) (#4814)
* feat(core): add isAudibleVideoElement predicate and audible media selector

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(runtime): route processed audible videos through web audio in preview

An audible <video> joins the Web Audio schedule, hidden-toggle and
group-capture passes when it needs a gain el.volume cannot express: a
boost above unity, an fx chain, automation, or an audio group. A plain
audible video at unity stays on native output. The clock master stays
audio-only; a video whose capture fails stays on native output (never
decoded, never failed closed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(audio): let audible videos join audio groups in preview and render

resolveAudioGroups, audioGroupOf, group render ids, the engine mixer and
the audio_group_no_members lint rule accept an audible <video>. A hidden
group drops its video members from the mix; muted videos stay inert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(editing): offer the audio fx section on audible videos

EditableElementFacts gains hasAudio; studio and sdk derive it from muted and data-has-audio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): show audio meters when a project has an audible video

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): offer audible videos as carve voices

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(studio): let audible video clips join audio groups

Drops the video refusal; grouping and group audition count audible videos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(engine): honor hidden audio groups for auto-detected video audio

Muted videos that declare data-has-audio are no longer mixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep authored src, loop and honest status when keeping cutout sound

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core): treat only data-has-audio=true as audible in runtime and lint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(core): trim the web audio route and group-key comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): capture ramped clips so a group's bus carries them in preview

A rate lane skipped Web Audio entirely, so a ramped video or audio clip in a
group played native output without the group's fader, mute or FX while the
export applied them. Only the decoded-buffer fallback cannot follow a rate
curve; the media-element capture can, so the skip now guards that fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): show meters only for videos Web Audio carries

A plain audible video at unity stays on native output, which the meters
cannot read, so it no longer advertises them. The runtime's join rule and the
meter gate now share audibleVideoNeedsWebAudio (core/audible-video).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:28:30 +00:00
Miguel Ángel e32ce9c5b9 Merge pull request #4885 from heygen-com/release/v0.8.107
chore: release v0.8.107
v0.8.107
2026-10-01 19:27:43 -07:00
Miguel Angel Simon Sierra c3a20c589d chore: release v0.8.107 2026-10-01 19:05:00 -07:00
Vance Ingalls c1f6983dbf Merge pull request #4887 from heygen-com/fix/restore-persist-prepare-content
fix(studio): persist honors prepareContent again so audio edits keep their content step
2026-10-01 18:49:38 -07:00
Vance IngallsandClaude Sonnet 5.5 396f2fbf80 fix(studio): a failed prepared-content write is a failed commit, not saved
The server patch is already durable, so keep it and its undo entry and keep
the preview matching the file, but throw after history so the caller cannot
report success (the cutout would say Sound kept with no audio saved).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 18:24:56 -07:00
Vance IngallsandClaude Sonnet 5.5 2d690968cc chore(studio): trim comments to the ratchet
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 18:15:45 -07:00
Vance IngallsandClaude Sonnet 5.5 76c4fc7b3c fix(studio): persist honors prepareContent again so audio edits keep their content step
#4869 dropped prepareContent from the persist options and the server path
while #4813's attribute batch commit (mediaAudioEdits) still passes it, so
main fails typecheck and the audio edit would lose its prepared content.
Restore the option, take the server patch when it is set, and write the
prepared content over the patch under the same file queue.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 18:07:54 -07:00
Miguel Ángel d4756f597c fix(studio): edits on GSAP-animated layers keep their keyframes and file positions (#4869)
* 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
2026-10-01 17:24:35 -07:00
37f30b1d06 feat: keep a video's sound on the clip by default (1/8) (#4813)
* feat(cli): one owner for init media placeholders, audio stays on the video

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted

Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>

* feat(registry): example templates keep the a-roll's sound on the video

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(lint): video audio rules recommend keeping sound on the video

Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(parsers): a video is either audible (data-has-audio) or muted, never both

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): follow the compiler's audio rule in the audio-track toggle

Confirm before background removal drops a video's sound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): hyperframes-core teaches sound on the video by default

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): workflows and scaffold guidance keep a video's sound on the clip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: footage with sound keeps it on the video clip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact

The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm

Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false

An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples

lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): encode init media filenames and probe --audio duration

Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(lint): audio-state-aware media rules and time-aware double-source check

video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): audio-stream check for remotion ports, followable J-cut recipe

Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(lint): trim comments to satisfy the comment ratchet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): atomic muted edits, cutout keeps sound on a linked audio

Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(producer): skip hidden videos when auto-adding video audio

A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(studio): keep useDomEditSession under the 600-line cap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
2026-10-01 23:56:36 +00:00
Miguel Ángel 7a60b97bf8 feat(studio): hosts can run Studio's agent tools on their own edit session (#4882)
* feat(studio): hosts can run Studio's agent tools on their own edit session

Exports buildStudioTools, collectStudioLookScene and their types from the package index. EditorShell keeps using the same builder; no behaviour change.

* style(studio): format the agent tools export and keep its comment share
2026-10-01 16:57:15 -07:00
Miguel Ángel d218cec091 ci(studio): gates read the base the run measured, and drag frames on a quiet runner (#4872)
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control

* ci(studio): gates read the base the run measured, not the base branch tip

* style(ci): format the base-file test
2026-10-01 16:35:49 -07:00
Miguel Ángel c3b6e55f7e fix(studio): undo right after an edit undoes that edit, even before its save lands (#4807)
* 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
2026-10-01 15:22:03 -07:00
Miguel Ángel 21a29c2ac7 fix(studio): the timeline scroll gate catches dropped frames in the virtualized view (#4855)
* 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
2026-10-01 14:52:39 -07:00
Miguel Ángel a59d93a027 fix(studio): a focused video or audio player keeps its arrow and play keys (#4854)
* fix(studio): a focused video or audio player keeps its arrow and play keys

* fix(studio): every remaining shortcut asks the shared typing check
2026-10-01 14:48:57 -07:00
Miguel Ángel fd105807c4 fix(scripts): count comments after regex literals in the comment ratchet (#4879)
* fix(scripts): count comments after regex literals in the comment ratchet

* fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet
2026-10-01 14:09:44 -07:00
Miguel Ángel 83035e08b8 fix(studio): an undo that lands mid-drag keeps the box under the pointer (#4853)
* 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
2026-10-01 14:00:32 -07:00
Miguel Ángel d05344b3ee fix(studio): a reload requested while a canvas save is in flight loads again once it lands (#4874)
* fix(studio): a reload requested while a canvas save is in flight loads again once it lands

* test(studio): type the late-reload drag helper for every edit kind
2026-10-01 13:56:37 -07:00
Miguel Ángel 30c55694cb fix(studio): a first animation in a sub-composition file runs from inside its template (#4806) 2026-10-01 13:40:55 -07:00
Miguel Ángel 78e849235a fix(core): a composition made only of a set shows it at a seek in the preview (#4865)
* fix(core): a composition made only of a set shows it at a seek in the preview

* chore(core): keep the comment share of the runtime init file level

* test(studio): bank the four resize-hold rows the preview now gets right
2026-10-01 12:08:42 -07:00
Miguel Ángel c72b735a8c fix: a save's temp file no longer reaches Studio as an outside change (#4857)
* fix: a save's temp file no longer reaches Studio as an outside change

* refactor: the project file walk and its tests carry no new duplication or complexity

* test(studio): the edit accuracy gate judges the two nudge sequences again
2026-10-01 11:48:00 -07:00
Miguel Ángel 89704189d8 chore: release v0.8.106 (#4873) v0.8.106 2026-10-01 11:46:50 -07:00
Miguel Ángel 57da627e76 fix(studio): a hand edit after a preview reload draws on the element now on screen (#4856)
* fix(studio): a hand edit after a preview reload draws on the element now on screen

* fix(studio): a preview reload waits for the gesture on screen to end

* fix(studio): a marquee after a reload selects from the preview on screen

* fix(studio): a reload requested before a canvas save loads again once the save lands
2026-10-01 11:32:06 -07:00
Miguel Ángel 44138931ce perf(studio): track the motion path only while a path or create ring is drawn (#4795) 2026-10-01 11:14:55 -07:00
Miguel Ángel 59bbe7f098 feat(studio-server): a write receipt names the version the write replaced (#4864)
* feat(studio-server): a write receipt names the version the write replaced

* test(studio-server): route receipts carry the version each write replaced

* refactor(studio-server): the receipt's from field needs no comment
2026-10-01 11:13:20 -07:00
Miguel Ángel a9929b60ee fix(studio): the preview serves a same-size edit written right after the last one (#4792)
* fix(studio): the preview serves a same-size edit written right after the last one

* chore(studio): keep the comment share and complexity gates green

* refactor(studio): keep the project signature doc comment on the function it describes

* fix(studio): the preview re-reads files written in the last two seconds, whoever wrote them
2026-10-01 11:10:58 -07:00
Miguel Ángel ce76be50c7 feat(studio): paste and duplicate can keep a clip at its own time (#4859)
* feat(studio): paste and duplicate can keep a clip's own time, on the next free track

* fix(studio): an in-place paste uses the copied time only in the composition it was copied in

* test(studio): an in-place duplicate inside a sub-composition keeps its local time

* refactor(studio): the paste anchor is its own function, and the in-place tests share their setup

* refactor(studio): paste names its undo step and note in one place, and two paste tests share their steps
2026-10-01 10:19:50 -07:00
Miguel Ángel e666a8a2a3 feat(studio): the code editor follows studio's light and dark theme (#4862)
* feat(studio): the code editor follows studio's light and dark theme

Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet.

* test(studio): the code editor theme takes every colour from a theme.css token

* fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line

Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go.

* fix(studio): code matches carry no background, and types read apart from constants

Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes.

* fix(studio): the current search match reads apart by width, not colour alone

A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word.
2026-10-01 10:13:48 -07:00
Miguel Ángel 9c19327e38 test(studio): catch teleports and multi-step edit bugs in the edit accuracy bench (#4801)
* test(studio): catch teleports and multi-step edit bugs in the edit accuracy bench

* test(studio): share the edit accuracy case harness between single gestures and sequences

* test(studio): keep edit accuracy helpers private unless shared

* test(studio): score plain-CSS edits on pages that load GSAP for other elements

* test(studio): add an idle GSAP axis value so plain-CSS edits are scored on GSAP pages

* test(studio): report a slow undo save as late with its latency, apart from a lost one

* test(studio): sample the gap between back-to-back drags and allow one frame of lag

* test(studio): read the edge from the element's fractional size, so a crop edge is exact

* test(studio): keep waiting for a late save until an undone file stays put for 3 s

* ci(studio): run the edit accuracy grid in 12 shards to stay near 25 minutes each

* test(studio): read edit bench frames just before paint, so unpainted states never count

* test(studio): scope the edit bench frame sampler so page globals cannot clash

* test(studio): let a lagging box catch up the pointer travel it owes, so a held frame is no jump

* test(studio): record every save a sequence makes, even two that land 10 ms apart

* test(studio): end a drag's teleport scoring at its pointer-up, not at the next element's press

* test(studio): bank for each edit accuracy case a run that agrees with the gate's verdict

* test(studio): fail the edit accuracy gate on a teleport or an unmeasured drag

* test(studio): fail the edit accuracy gate when a text edit is not saved or shown

* test(studio): measure but do not gate the edit accuracy cases a known Studio race flips

* test(studio): bank the edit accuracy cases that newly pass with the teleport metric

* test(studio): quarantine the two nudge sequences whose redo write sits at the 2 s limit

* style(studio): cut three bench comments to one line each
2026-10-01 09:42:05 -07:00
Miguel Ángel d4176c4ce6 fix(studio): undo that reloads the preview keeps the timeline clips on screen (#4790)
* fix(studio): undo that reloads the preview keeps the timeline clips on screen

* chore(studio): keep the comment share and complexity gates green
2026-10-01 09:32:53 -07:00
Miguel Ángel 9f4d9e64b2 feat(studio): a light and dark theme toggle in the header that remembers the choice (#4839)
* feat(studio): a light and dark theme toggle in the header that remembers the choice

The sun and moon button flips Studio between Graphite light and dark with a
circular reveal, saves the choice, and follows the system until one is made.

* fix(studio): studio opens light the first time, then keeps the saved theme

* fix(studio): drop the theme field's comment, the theme owner states the rule

* fix(studio): the theme toggle shows only in studio's own app and follows the theme on screen

A host that embeds StudioApp gets no toggle; the button reads the document's theme, the reveal drops the default crossfade, and reduced motion skips it.

* fix(studio): the theme reveal leaves other view transitions alone; one default for the toggle

The crossfade rule applies only while the toggle's own transition runs, StudioApp passes the header's single default through, and the reveal's reduced-motion and keyboard paths are tested.

* fix(studio): a second theme click mid-reveal keeps the crossfade off

Only the latest reveal clears the document marker; tests pin the marker while a reveal runs and the circle's radius.

* refactor(studio): the theme toggle shares its icon props and reads as plain functions

Simplify pass: one icon props object for the sun and moon, no useCallback around a handler nothing memoises, cannotReveal named for what it answers, the test reuses the theme reader.

* fix(studio): studio's own app turns the theme toggle on through context; tests parse index.html

App.tsx stays at main's size: main.tsx provides ShowThemeToggle and the header reads it, default off for hosts. The boot-script tests read index.html through DOMParser instead of an HTML regex.
2026-10-01 08:44:57 -07:00
Miguel Ángel b6c879c4e9 fix(studio-server): a folder rename past an unreadable folder or file answers ok (#4860)
* fix(studio-server): a folder rename past an unreadable folder or file answers ok

* fix(studio-server): a rename skips only folders and files it may not read

* fix(studio-server): the rename's skip helpers say it by name

* test(studio-server): the rename tests share one request
2026-10-01 08:41:39 -07:00
Miguel Ángel 8593b01a6d perf(studio): stop font list work from landing in drag frames (#4794)
* perf(studio): load the font lists when the font picker first opens

Selecting an element no longer fetches and dedupes the 1.7k-name Google font list, so the response cannot land inside a drag frame. The lists are fetched once per session on the first open.

* test(studio): expect the font lists to load on the first picker open

* perf(studio): load the font lists at session start and process them when input is idle

* style(studio): drop a comment the font list names already carry

* perf(studio): process the font lists in idle slices of at most 3 ms

* perf(studio): join the font lists without iterating them when they arrive
2026-10-01 08:35:28 -07:00
Miguel Ángel 07af05c816 feat(studio): fade handles ride the fade's end and the waveform shows the fade (#4844)
* feat(studio): fade handles ride the fade's end and the waveform shows the fade

* refactor(studio): read fade handle keys from one lookup

* fix(studio): fade handle keys stay with the handle and snap past the clip's own edges

* fix(studio): Cmd shortcuts keep working on a focused slider and fade boxes stay on their tabs

* fix(studio): keep a fade handle while it is in use and restore the viewport gate's Chrome path

* fix(studio): a fade's draft clears with its own save and grow drags never shrink it

* fix(studio): saved fades reach the timeline store so the handle stays where it was dropped

* fix(studio): a fade longer than its clip starts a drag or key from the clip's end

* fix(studio): fade comments state the rule, not its history
2026-10-01 08:15:10 -07:00