* 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
* fix(cli): hide console windows on Windows for background/detached spawns
Add windowsHide: true to 8 spawn/execFile call sites that were missing
it: ffprobe/ffmpeg probes in the producer and engine packages, the
telemetry exit-flush and studio dev-server spawns, and the browser-open
and background-preview detached launches in the CLI.
previewLifecycle.ts's spawnDetachedPreview redirects stdout/stderr to a
raw log-file descriptor; libuv only adds Windows' CREATE_NO_WINDOW flag
when no stdio slot inherits a raw handle, so windowsHide alone does not
fully suppress a console flash there (documented inline) — it's still
added for consistency and because it correctly sets the process's
show-window state regardless.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): correct windowsHide flash-risk claims and add missing dev-server spawn tests
- The previewLifecycle.ts comment (and its mirrored test comment)
overstated the residual console-flash risk on the detached background
preview spawn: DETACHED_PROCESS already leaves that child without a
console regardless of whether CREATE_NO_WINDOW gets applied.
- Add the two missing regression tests for preview.ts's runDevMode and
runLocalStudioMode spawns, exporting both for test access.
- Drop an unused export in audioPadTrim.ts flagged by the repo's dead-code
gate; the constant is only ever used within its own file.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(engine): hide console window for the drawElement self-verify PSNR comparison
psnr.ts's execFile call runs in-process inside the Studio server's render
path (parallelCoordinator.ts's drawElement disk self-verify), so a render
started from Studio's UI under a detached preview hits this call from a
console-less parent — the same flash case the other windowsHide sites in
this PR already cover. psnrFilterAvailability.ts's probe is on the same
in-process render path and already had the flag.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): keep only the spawn sites main still misses and leave browser windows visible
windowsHide maps to libuv UV_PROCESS_WINDOWS_HIDE, which also starts GUI apps with SW_HIDE,
so the detached browser launch must not set it. The background preview child already has
no console (DETACHED_PROCESS with inherited log fds), so its flag was a no-op. The
telemetry, psnr and audioPadTrim sites shipped on main via #3839 and #4604.
* fix(cli): stop the studio dev server when the terminal closes
With windowsHide the dev server runs in its own hidden console on Windows, so closing the terminal
only signals the wrapper (SIGHUP). Handle it like SIGINT and SIGTERM and kill the process tree.
* fix(studio-server): hide the console window for loudness measurement
The background preview has no console, so its ffmpeg loudness run opened a visible window on Windows.
* refactor(cli): drop the comment on the studio shutdown signals
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* 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>
* 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>
* 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
* 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>
* 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>
* 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>
* fix(studio): resizing an element GSAP only positions saves its CSS width and height
* fix(studio): a resize never saves GSAP's scale mask as the element's CSS
* fix(studio): keep the resize size writer inside the file and comment budgets
* fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders
* fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it
* test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview
* fix(studio): a move keeps an authored xPercent centring set in the saved file
* fix(studio): a drag on a centred element no longer flashes off-centre on release
* fix(studio): a font pick saves the edit and its @font-face in one write
* fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit
* test(scripts): pin that a slash after i-- before a quoted slash is division
* fix(studio): an inspector edit saved while a preview reload loads stays on screen
* fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it
* fix(studio-server): a font name or path with braces in quotes still saves its @font-face
* fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule
* test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass
* fix: 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
* 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
* 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
* 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
* fix(studio): open projects whose folder name has a colon
* fix(studio): open projects named with a colon or '#' in Studio and its preview
* fix(studio): refuse a Windows stream id reached through a session alias
One adapter rule, isServableProjectId, now guards the requested id, a
session's projectId and the folder listing. The bundler-failure preview
places its <base> inside an attributed <head>, and the media metadata
cache keys on the encoded URL so ids with ':' can't collide.
* test(studio-server): history prune tests allow a file time a moment past Date.now()
* ci: temporary probe for the history prune test flake
* fix(studio): timeline scroll-end timer no longer fires after unmount
* ci: remove the temporary history prune probe
* test(studio): check the virtualizer teardown with a spy and a 200 ms wait
* fix(parsers): moving a clip on the timeline moves its inner animations too
* fix(parsers): a timeline move keeps chained and partly known tweens where they belong
* fix(parsers): moving a clip leaves chained outside tweens and template aliases in place
* fix(studio): a clip timing edit syncs its animations once, not twice
* fix(studio): a timing edit falls back to the server when the SDK wrote no animation change
* fix(studio): the SDK sync check reads scripts inside a template too
* fix(studio): the SDK sync check finds template scripts through the DOM walker
* fix(core): a clip retime writes only its own timed tweens, never outside ones
* style(core): format the clip tween import
* fix: readers never see an empty or half-written project file
* test: pin the create routes and block install to whole-file writes
* fix(sdk): a write through a link to a missing file creates that file
* fix: saves through a linked folder reach the real file, atomic writes handle long names
* test(sdk): check for the stray file, not the folder listing
* fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does
* fix: agree the helper move in the deletion check, and match temp paths without a path regex
* test: check POSIX file modes only off Windows
* fix(producer): render names a data-composition-src that points at a folder instead of crashing
* fix(lint): name a data-composition-src that points at a folder instead of crashing
* fix(cli): compositions, keyframes and Studio insertion survive a folder composition source
* fix(lint): tell a folder composition source to point at the HTML file inside it
* fix: read a composition source through one file handle so its type check and read cannot race
* fix(parsers): report a named pipe as a composition source instead of blocking on it
* revert(parsers): open composition sources read-only again, which CodeQL reads as no file creation
* fix(parsers): open composition sources with a private mode, skip pipes, and treat bad paths as missing
* fix(studio): undo and redo swap the restored scene in place instead of reloading
* fix(studio-server): a preview asked for right after an undo shows the undone state
* fix(studio): an undo that restores a page outside every scene still reloads in full
* fix(studio-server): a preview built across a write is not cached under the old signature
* test(studio): a soft undo keeps the scene hashes so the next edit still swaps
* feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk
`hyperframes clean` lists leftover render work dirs, debug renders, the history of projects
that no longer exist, idle video proxies and extracted video frames, with sizes, and removes
them. `--dry-run` removes nothing, `--json` is for agents, `--snapshots` also removes
snapshot folders (otherwise listed as reclaimable). It never touches outputs, sources or
anything a running render or preview still owns.
* docs(cli): say that clean does not search a project's subfolders
* fix(cli): clean test compares folder names on Windows and the skills manifest is regenerated
* fix(studio-server): the proxy cache sweep removes only files the transcoder named
It removed any idle .mp4 or .webm in .transcode-cache, so a user file placed there, or reached through
a symlinked cache folder, could go. Proxies are always <sha256>.<ext> and their temp files
.tmp-<uuid>-<proxy name>; nothing else is swept.
* style(producer): keep a renderDirOwner doc comment within 120 columns
* fix(studio-server): the server loads where sharp's native binary is missing
* fix(studio-server): a missing sharp warns once and is checked before the image is read
* test(studio-server): a write landing between Studio's read and its patch survives undo
* test(studio-server): the outside write also survives a claim that arrives after receipts expire
* fix(studio-server): undo keeps an outside change even when Studio's history claim arrives late
* fix(studio-server): history forgets replaced bytes on delete and frees unused bytes past budget
Also recreates the history folder before storing replaced bytes, hears writes inside the queued start, and pins the agent-turn claim that arrives after the receipt expired.
* fix(studio-server): answer 413/415 instead of reading huge or binary files as text
* test(studio-server): prove the 413 path never reads the file via peak RSS
* perf(studio-server): light entries for safe paths, hf-id stamping and project history
* docs(studio-server): list the light entries a host can import
* fix(studio-server): a cold proxy copy answers 202 at once and the preview retries until it lands
* fix(studio): timeline filmstrips wait for a clip's proxy copy instead of failing on the 202
* fix(studio-server): a copy that cannot be made reports its failure instead of answering 202 forever
* test(studio-server): pin that a full queue is not remembered and a reset probe stops waiting
* test(studio-server): pin that a failure outside ffmpeg is remembered too
* style(studio-server): keep comment share at base in the transcoder and decoder
* fix(core): a clip's proxy URL goes on the video only once the copy is served, so every reader of it works
* fix(player): the parent media mirror follows a clip's src, so it plays the preview copy once it lands
* perf(player): the mirror re-points a clip only when the parent takes over playback
* style(core): shorten the sync-state comment in the proxy swap
* fix(studio): the timeline follows a clip re-pointed at its preview copy
* fix(core): a failed readiness check is retried instead of leaving a clip on its original
* fix(studio-server): a composition card rendered while preview copies are made is not cached
* fix(core): frame captures wait for a pending preview copy as long as for a loading video
* fix(studio-server): the preview-copy check uses the shared realpath from main
* fix(player): re-pointing a clip onto another clip's file no longer plays that file twice
* fix(player): a clip that replaces another on the same file keeps its sound
* fix(studio-server): a save never recreates a project folder renamed away
* style(cli): wrap imports after merging main
* fix(studio-server): proxy and upload answer that the project folder is gone instead of 502 or an empty 201
* test(studio-server): rename the project folder while an upload is being read
* fix(studio-server): a proxy or backup answers that a vanished project folder is gone
* test(studio-server): clean up the renamed folder after the proxy test
* refactor(studio-server): drop an unused re-export and share the folder-gone test setup
* fix(studio-server): any request failing after its project folder vanished says the folder is gone
* fix(studio-server): the folder-gone answer works on every host and drops the failed route's headers
* fix(studio-server): the folder-gone 404 keeps the host's own headers
* fix(core): no folder outside a project is created once the project folder is gone
* test(studio-server): a rename that makes the codec probe fail answers that the folder is gone
* test(studio-server): a refused render leaves no outside renders folder behind
* docs(core): wrap the mkdirWithinProject comment under the line limit
* refactor(studio-server): let the folder-gone middleware's names carry what its comments said
* fix(studio-server): say the folder is gone when a render names a composition
* fix(studio-server): check renders against the folder path and share one folder-gone answer
* test(studio-server): pass the project folder to the preview document store
* fix(studio-server): a save never recreates a project folder renamed away
* style(cli): wrap imports after merging main
* fix(studio-server): proxy and upload answer that the project folder is gone instead of 502 or an empty 201
* test(studio-server): rename the project folder while an upload is being read
* fix(studio-server): a proxy or backup answers that a vanished project folder is gone
* test(studio-server): clean up the renamed folder after the proxy test
* refactor(studio-server): drop an unused re-export and share the folder-gone test setup
* fix(core): no folder outside a project is created once the project folder is gone
* test(studio-server): a refused render leaves no outside renders folder behind
* docs(core): wrap the mkdirWithinProject comment under the line limit
* test(studio-server): pass the project folder to the preview document store
* fix(studio-server): remove the history of a project that no longer exists
* fix(studio-server): keep a project's history while its drive is disconnected
* fix(studio-server): keep an older history whose drive may only be unmounted
A canvas drag stores the element position as a global gsap.set above the
timeline declaration. A resize that followed it inserted its tl.set right
after that line, so the saved script referenced the timeline before it was
declared and every reload and render threw a ReferenceError.
Both GSAP writers now bound the "after the last statement" anchor by the
timeline declaration, whatever the timeline variable is named. A drag or a
resize on its own writes the same bytes as before.
* fix(studio-server): ffmpeg and ffprobe run from the configured binary; unchecked uploads say so
* fix(studio-server): the upload result type lists unchecked files
* fix(media-use): spawn the configured ffmpeg and ffprobe, and name a missing configured ffprobe
* style: format the ffprobe path test, doctor spawns and upload check
* fix(studio): show the not-checked upload notice as info, not an error
* fix: a configured ffmpeg or ffprobe path that cannot run fails loudly
* fix: refuse a folder as the ffprobe path, and report broken paths through --json
* test: reuse the file manager test setup and split the upload stream check
* fix(media-use): a configured ffmpeg or ffprobe must run -version, and a relative one runs itself
* fix(media-use): a configured ffprobe path must name ffprobe, not another tool
* perf(core): skip hidden clips' content so the browser stops decoding their images
Hidden timed clips get content-visibility:hidden while their inline visibility is hidden,
from script evaluation on. A clip due within the look-ahead (2 s by default) keeps its
content live, so its images load before it shows.
* perf(studio-server): load preview images lazily so hidden scenes fetch nothing
* fix(core): stop the readiness scan waiting on and decoding later scenes' images
* perf(studio-server): keep first-frame images eager so the preview opens as fast as before
Only images inside a clip with a numeric start after 0 go lazy; a start the route
cannot read stays eager.
* perf(core): keep clips that start at 0 laid out before the first pass so the first frame is on time
* perf(core): open the preview on time by hiding only lazy images before the first pass
GSAP reads the computed style of the elements it animates while timelines build. Inside a
content-skipped clip every read forces a style update, which cost about 65 ms at open on the
bench composition. Before the first pass the rule now takes only lazy images out of layout, so
hidden later scenes still do not load theirs; after it, the skip rule keeps their content skipped.
* fix(core): hold a paused jump on the previous picture until the next scene's images decode
* fix(studio): swap in a reloaded preview only after its restore seek has painted
* fix(core): apply a held paused jump after 1 s when an image never decodes
* fix(core): keep hidden-scene image skipping and held jumps to Studio's preview, never renders
* fix(studio): reapply manual edits once a held paused jump lands
* fix(studio): copy a clip without the preview's lazy-loading marks
* fix(studio-server): mark preview images lazy without re-encoding the document's entities
* fix(core): share one preview-mark constant between the runtime and the preview route
* fix(studio): reapply manual edits a second time only after a seek that was held
* refactor(core): share the hidden-ancestor walk between clip visibility and image holds
* fix(core): import the studio preview mark from its own subpath
* fix(studio-server): find preview images with the linear html scanner
* chore(core): drop two doc comments the scanner's names already carry
* fix(studio-server): serve the preview mark as its own meta, apart from the GSAP fallback script
* fix(studio-server): capture thumbnails and frames from the preview without lazy images or the mark
* fix(core): let the full readiness scan wait on every image except lazy ones off the first frame
* fix(studio-server): keep an authored loading attribute in any letter case on preview images
* fix(core): leave template content out of findStartTags so preview images match the DOM
* fix(studio): send Ask agent the live markup without the preview loading marks
* chore(core): drop the template note from findStartTags, its test carries it
* perf(studio-server): splice lazy attributes into the preview in one pass
* fix(studio): keep authored image loading on copy and Ask agent by marking preview-made lazy images
* test(studio-server): follow the capture preview URL back to its sub-composition for every project id
* refactor: name the upcoming-clip and GSAP fallback marks once
* fix(studio-server): keep capture builds out of the on-disk preview store
* fix(studio): read and write a clip's in-point the way playback does
* fix(studio): a group head trim keeps the minimum clip length off the grid
* fix(studio): a group head trim stops where a single-clip trim does and never goes below zero
* fix(studio): group members on the grid move together when a trim stops at its limit
* fix(studio-server): background removal writes only where it checked, even through planted links
* test(cli): read the cutout through a no-follow handle instead of checking then reading
* feat(studio-server): a host can make Cmd+Z undo the newest change whoever made it
* fix(studio-server): Cmd+Z for everyone leaves an agent turn that is still open alone
* style(studio-server): format the undo scope check
* refactor(studio-server): the undo scope option names itself
* fix(studio-server): an open turn's parts are known by id, and a new change ends redo for everyone
* style(studio-server): format the open turn check
* fix(studio-server): an open turn's part is known before its entry is announced
* fix(studio-server): history saved by 0.8.78 opens with every entry, and an unreadable id is never overwritten
* test(studio-server): a malformed history id is refused and kept, not replaced
* refactor(studio-server): name the 0.8.78 record check instead of commenting it
* docs(studio-server): hosts keep project histories in historyCache, which reopens a replaced folder
* fix(studio-server): an unreadable 0.8.78 record keeps its history, and another path to the folder is not a copy
* style(studio-server): format the 0.8.78 history tests
* fix(studio-server): name the history id error, and a recorded path that cannot be read is not a copy
* style(studio-server): format the 0.8.78 history fixture
* fix(studio-server): a write through a link lands on the file the system resolves
* fix(cli): resolve catalog install paths as the system does, matching the install root
* fix(cli): list installed files relative to the real project folder
* fix(studio): keep a clip in sync when its head is trimmed at non-1x speed
* fix(studio): keep head-trim sync at the media start, on snaps and through splits
* fix(studio): keep group head trims rigid and read in-points like the runtime
* fix(studio): reload the preview only when a file it loaded changes
* test(studio): read the owner id from the new change-owner shape
* refactor(studio): drop a comment the parser name already says
* fix(studio): keep reloading until the preview is built, and count what its document names
* style(studio): format the reference pattern and name the kept reload
* fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload
* fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it
* test(studio): name the preview ownership test for what it checks
* chore(ci): allow the watchIgnore removals and trim the new comments
* fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out
* perf(studio-server): check a changed folder in one lookup and cover every reference form
* fix(studio-server): record read folders for a project at the filesystem root
* fix(studio-server): reload the preview when the file a symlinked asset points at changes
* fix(studio-server): resolve symlinks on every preview read and key projects by their real folder
* fix(studio-server): record every link a preview read passes through, resolved as the system does
* fix(studio-server): a thumbnail no longer fails when a file it reads is removed mid-request
* fix(studio-server): a thumbnail survives its cache folder being removed mid-request
* fix(studio-server): a stray file where the manifests folder goes no longer fails a thumbnail
* fix(studio-server): drop a comment the code already says
* fix(studio): the dev server leaves catalog installs to hyperframes preview
* test(studio): name the dev server's install test for what it checks
* fix(studio): a finished install replaces its 'Adding' toast instead of sitting beside it
* test(studio): pin that the Adding toast goes after the install's own toast
* test: cli and studio-server tests remove the temp folders they create
* test(cli): each temp folder is removed where it is made, even when a test fails
* test(cli): the one-liner helper relies on its project's own cleanup
* fix(studio): avoid preview reloads for temporary and excluded files
* docs: capture public preview reload reproduction
* fix(studio-server): normalize watch paths in linear time
* fix(studio): register dynamically opened preview projects