Commit Graph
4904 Commits
Author SHA1 Message Date
James RussoandClaude Opus 5.5 5f7c6bc5b2 docs(changelog): weekly digest 2026-09-21-2026-09-28 (#4669)
* docs(changelog): weekly digest 2026-09-21-2026-09-28

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(changelog): embed the Sep 21-28 weekly video

Adds the 50.4s square changelog video for the Sep 21-28 digest,
served from the static CDN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 17:17:56 +00:00
Miguel Ángel 52f849e2b4 chore: release v0.8.84 (#4671) v0.8.84 2026-09-28 10:00:31 -07:00
Miguel Ángel 42f6a3a27e fix(studio): unmounting mid-edit saves the typed words, as blur does (#4651)
* fix(studio): unmounting mid-edit saves the typed words, as blur does

* fix(studio): in-place text is not saved once the preview turns read-only
2026-09-28 16:16:02 +00:00
Miguel Ángel 523685985b fix(studio): the timeline reads clip speed with the same limits as playback (#4667) 2026-09-28 16:15:49 +00:00
Miguel Ángel e251d4c1de perf(studio-server): a host can import id stamping, file walking and history without the server (#4644)
* 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
2026-09-28 16:14:43 +00:00
Miguel Ángel cbcadbfbd0 fix(studio-server): uncopied HEVC clips no longer stall Studio while their preview copies are made (#4580)
* 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
2026-09-28 16:14:29 +00:00
Miguel Ángel 8fd5fd5a32 chore: release v0.8.83 (#4668) v0.8.83 2026-09-28 09:27:18 -07:00
Miguel Ángel 5257dd581f fix(studio): tone-map HDR preview proxies with the render tone map (#4606) 2026-09-28 15:48:58 +00:00
Miguel Ángel 16a010404c fix(studio): the shortcut list shows only keyframe keys that work (#4657)
* fix(studio): the shortcut list shows only keyframe keys that work

* docs(studio): animation guide drops the H and U keys and says what K does
2026-09-28 15:48:42 +00:00
Miguel Ángel 7fae35f36e fix(studio): carve and duck follow each clip's trim and speed (#4661)
* 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): level-out measures and places its lift on the clip clock at any speed

* fix(studio): round level-out lane times and tighten the half-speed test

* fix(studio): carve and duck follow each clip's trim and speed

* fix(studio): carve ignores speech after the bed stops playing

* test(studio): carve ignores speech past a bed that ducks and has no duration

* fix(studio): carve cuts speech past the bed's end at every strength
2026-09-28 15:48:26 +00:00
Miguel Ángel addc72d761 fix(studio-server): a request that fails after its project folder vanished says the folder is gone (#4617)
* 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
2026-09-28 15:47:13 +00:00
Miguel Ángel d2f0bc7f34 perf(studio): a host's first screen can import Studio's UI and Player without the editor (#4650)
* perf(studio): light ui and player entries for a host's first screen

* perf(studio): the ui entry's logo no longer loads the studio header

* test(studio): refuse only packages studio itself depends on

* test(studio): the light entries also refuse the hf-ids parser
2026-09-28 15:15:00 +00:00
Miguel Ángel 2251e5fad0 feat(studio): export DOM editing for a host outside EditorShell (#4640)
* feat(studio): export DOM editing for a host outside EditorShell

* fix(studio): host hears only single-box clicks, and a handle release no longer eats the next one

* refactor(studio): cut the sibling z-order comment to one line
2026-09-28 15:14:32 +00:00
Miguel Ángel 93ab2899f1 feat(studio): let a host app keep paint order in step with timeline rows (#4647)
* feat(studio): let a host app keep paint order in step with timeline rows

* docs(studio): say each stacking sync prop overrides separately
2026-09-28 09:50:09 +00:00
Miguel Ángel 26bfd38136 fix(studio-server): a save never recreates a project folder renamed away (#4602)
* 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
2026-09-28 09:48:22 +00:00
Miguel Ángel 6cb95d844d test(core): findProjects stop tests fit Windows' time budget and pin a stopped walk ending (#4653)
* test(core): findProjects stop tests build two projects, not four hundred files

* test(core): stop tests keep a folder queued at the stop, with room for Windows setup
2026-09-28 09:29:44 +00:00
Miguel Ángel 34a66744f2 fix(studio-server): a cached-only thumbnail miss answers 204, so the page logs no error (#4652) 2026-09-28 09:29:32 +00:00
Miguel Ángel 5eec513281 fix(studio): the timeline takes m4v video and flac audio (#4649)
* fix(studio): the timeline takes m4v video and flac audio

* fix(studio-server): check and label flac uploads as audio
2026-09-28 08:49:57 +00:00
Miguel Ángel 2119b188a2 fix(studio): a clip drag released outside the window is cancelled (#4646)
* fix(studio): releasing a clip drag outside the window cancels it

Pointer capture delivered the release to the timeline even outside the window,
so the move or trim committed wherever its preview was last drawn. A release
outside the window now cancels the gesture, the same as Escape.

* test(studio): pin a release exactly on the window's edge as outside

* fix(studio): releasing a fade-handle drag outside the window cancels it

Fade handles run their own gesture, so they now use the same outside-window
check as clip moves and trims, moved into one shared module.
2026-09-28 08:49:42 +00:00
Miguel Ángel 080d2c7445 fix(runtime): make the preview's scene swap safe before Studio turns it on (#4542)
* fix(runtime): a scene swap refuses when an animation outside the scene moves its elements

* test(runtime): pin the scene swap volume re-probe, host CSS variables and diagnostics-free swap

* fix(runtime): a scene swap keeps a video its scene script animates instead of reloading it

* fix(runtime): a scene swap stops every animation the old scene script started

* fix(runtime): record scene script animations only on pages a scene swap can act on

* docs(core): reword the scene animation recording comment

* fix(runtime): watch media as the page parses only on pages a scene swap can act on

* perf(runtime): a scene swap checks for outside tweens in one pass over the film's tweens

* test(runtime): pin the single revert and the cleared animation record of a scene swap

* fix(runtime): a scene swap rebuilds media when the edit changes the scene script

* fix(runtime): a scene swap undoes the sets a scene script made

* fix(runtime): a scene swap rechecks outside tweens and runs new scripts after every host

* test(runtime): pin that a tween started during the caption wait leaves the page untouched

* fix(runtime): a scene swap refuses when its script set a value on an element the swap keeps

* docs(core): say the scene recording drops a finished tween that the timeline would keep when paused

* fix(runtime): a scene swap refuses when two of its animations write to an element it keeps

* fix(runtime): a scene swap resets kept media and refuses animations outside the scene

* fix(runtime): a scene swap keeps a moved video in place and swaps tweens on plain objects

* chore(core): regenerate the inlined position-edits script for the kept-video move reapply

* fix(runtime): a scene swap checks the timeline registry as it is after the caption wait

* fix(runtime): a scene swap refuses when a scene changed during the wait or a revert re-registers

* fix(runtime): a scene swap stops every animation a revert chain registers and rechecks its hosts

* fix(runtime): a scene swap refuses a moved host or a cancelled swap and stops at a shortened end

* fix(runtime): a scene swap refuses when a scene leaves its place or the film during the wait

* fix(runtime): a scene swap checks only its host's place, so a later body append does not refuse it

* fix(runtime): a scene swap refuses when the film's scene layout changed during the wait

* fix(runtime): a scene swap refuses when the order around a scene changed during the wait

* test(runtime): pin that a scene swap checks the film root order when every scene is wrapped

* fix(runtime): a scene swap gives kept media the mute, rate and loop a fresh load gives it

* fix(runtime): a scene swap rebuilds media a script wrote attributes on, keeping the rest

* fix(runtime): a scene swap keeps media carrying the runtime classes or reordered attributes

* fix(runtime): a scene swap resets a kept video's volume as a load does, not through the volume probe
2026-09-28 08:49:12 +00:00
Miguel Ángel 5df0bea8e1 perf(core): a bundled film inlines each shared font once instead of once per composition (#4643)
* perf(core): a bundled film inlines each font once instead of once per composition

* fix(core): font dedupe keeps the last copy, so the rule the browser uses never changes

* fix(core): font dedupe merges only exact copies, from styles that always apply

A style postcss cannot parse ships as authored instead of failing the bundle.
The key keeps !important and whitespace inside quoted family names.
Styles behind a media query, a non-CSS type, <noscript> or <svg> neither lose
a copy nor keep one.

* perf(core): font dedupe runs before inlining, so bundling is no slower

Deduping after inlining made postcss parse every inlined font: a three-scene
fixture went from about 40 ms to about 450 ms. The same url text inlines to the
same bytes, so deduping first gives the same output.

* fix(core): a scene-part bundle keeps each scene's own font copy, so a scene still swaps alone

* chore(core): shorten the font dedupe comment

* chore(core): trim the font dedupe comments to keep the file's comment share
2026-09-28 08:48:59 +00:00
Miguel Ángel 4aa6b168fe feat(studio): a host app can reuse Studio's snapping and marquee selection in its own preview (#4635)
* feat(studio): export snapping, the marquee and picker describe for a host's own preview

* fix(studio): a host pick type must bring its own marquee resolver
2026-09-28 08:23:11 +00:00
Miguel ÁngelandKenneth Shortrede 3a64ea867c fix(engine): forced-SDR renders show tone-mapped HDR footage as Studio does (#4600)
* fix(engine): forced-SDR renders show tone-mapped HDR footage as Studio does

Forced-SDR extraction tone-maps HDR frames with zscale, but the frames kept the
BT.709 transfer tag, so Chrome re-curved them (a tone-mapped 388d96 painted as
2d8790), and JPEG frames kept the BT.709 matrix every JPEG reader decodes as
BT.601. Tone-mapped frames now get the same sRGB tag and BT.601 JPEG conversion
as other SDR frames.

macOS used VideoToolbox, which does not tone-map: its frames stay PQ and Chrome
tone-maps them itself, darker than the hable curve Linux and Studio use. macOS
now takes the zscale path whenever ffmpeg has zscale, found through the shared
ffmpeg filter probe; without it the old path stays and the render warns once.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): forced-SDR renders tone-map HDR footage that lacks matrix or primaries tags

zscale finds no path between colourspaces without them, so the extraction
failed. Untagged HDR footage is BT.2020 in practice, and now gets those tags.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* fix(engine): forced-SDR tone map also tags the transfer ffprobe read from the container

From ffmpeg 8.1 ffprobe reports a container-only PQ or HLG tag; the frames
still lack it, so zscale failed where macOS VideoToolbox used to render.

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>

* test(engine): gate the container-only transfer case on the ffprobe that reads it

* docs(engine): shorten the filter-probe comment to the comment limit

* fix(engine): keep valid per-frame colour tags when tone-mapping HDR footage

* test(engine): build the mixed HLG and PQ fixture without B-frames so its timestamps are valid

* test(engine): build the trimmed mixed fixture without B-frames too

---------

Co-authored-by: Kenneth Shortrede <95665480+sontiac@users.noreply.github.com>
2026-09-28 08:21:13 +00:00
Miguel Ángel 0744667293 fix(studio): dragging an audio clip's edge trims it instead of moving it (#4645)
* fix(studio): dragging an audio clip's edge trims it instead of moving it

The clip picture layer (waveform, filmstrip, stills) paints above the trim
handles and swallowed the press, so an edge drag started a move. The
picture layer now takes no input.

* docs(studio): say clip content takes no pointer input
2026-09-28 08:21:00 +00:00
Miguel Ángel 2449ea05b1 perf(core): light entries for safe paths, asset paths and the runtime script (#4642) 2026-09-28 08:20:47 +00:00
Miguel Ángel 00ac4ef571 feat(studio-server): list the projects the history cache knows, with their folders (#4639) 2026-09-28 08:20:35 +00:00
Miguel Ángel f39b179030 fix(cli): an update never replaces the CLI's files while a hyperframes command is running (#4637)
* fix(cli): an update never replaces the CLI's files while a hyperframes command is running

* fix(cli): a killed command's leftover pid file cannot hold updates back

* fix(cli): judge a stale pid file by the installer's own clock, so sleep cannot drop a live command

* fix(cli): after a sleep the installer watches every command's file afresh

* docs(cli): shorten the installer script comment

* docs(cli): keep the new files within their comment share

* refactor(cli): split the upgrade eligibility check out of the installer scheduler

* docs(cli): keep the scheduler's doc on the scheduler
2026-09-28 08:20:22 +00:00
Miguel Ángel 5969c6894b chore: release v0.8.82 (#4641) v0.8.82 2026-09-28 08:04:14 +00:00
Miguel Ángel bee6a8e17d fix(studio-server): remove the history of a project that no longer exists (#4634)
* 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
2026-09-28 07:17:46 +00:00
Miguel Ángel 5383e486bb fix(producer): gif renders survive rgb frames among transparent ones (#4610)
* fix(producer): gif renders survive rgb frames among transparent ones

* test(producer): run the gif mixed-frame encode test in the ffmpeg lane

* fix(producer): gif encode test passes the argv, reachability and comment checks

* fix(producer): transparent gifs clear every frame so an opaque frame never lingers

* fix(producer): only gifs that turn translucent after an opaque frame get whole cleared frames

* fix(core): plain text keeps its own gif control block; gif encode failures share one shape
2026-09-28 06:48:49 +00:00
Miguel Ángel e7a0b6be2c fix(studio): a resize after a drag writes its keyframe below the timeline it uses (#4638)
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.
2026-09-28 06:48:38 +00:00
Miguel Ángel 27f1cfd23e fix(lint): Studio's lint panel and the CLI label a finding by the composition it names (#4618)
* fix(lint): a missing image, audio or mask file names the composition that references it

* test(cli): a missing file is reported once per composition that references it

* fix(lint): Studio's lint panel and the CLI label a finding by the composition it names

* refactor(lint): drop a comment and unused first-entry lines from the grouping change
2026-09-28 06:43:16 +00:00
Miguel Ángel 625b07fe2d test(producer): run the late-host pip and dogs-captions fixtures in CI (#4625)
* test(producer): run the late-host pip and dogs-captions fixtures in CI

The pip fixture wrote its video start in root time without the global
basis marker that PR 3732 made the contract, so it rendered the empty
host. Mark it global and schedule both fixtures that never ran in CI.

* ci(producer): split regression fixtures across nine shards

dogs-captions runs about eight minutes on its own; a ninth shard keeps
the other shards near their previous wall time.

* ci(producer): run dogs-captions in a nightly regression job instead of on every PR

* ci(producer): base the dogs-captions timing and nightly reason on two CI runs
2026-09-28 06:09:39 +00:00
Miguel Ángel e738e10267 feat(core): export findProjects to list the HyperFrames projects on a machine (#4627)
* feat(cli): hyperframes projects finds the HyperFrames projects on this machine

* refactor(core): keep the project rule's comment share under its package's

* fix(cli): projects keeps Spotlight hits under a symlinked root and refuses a missing root

* style(cli): format the projects walker and its tests

* fix(cli): projects tests pass under a symlinked temp folder and a bad root errors on stderr

* test(cli): projects tests hold on Windows and cover an unreadable root

* style(cli): format the projects command and tests

* refactor(cli): split the projects Spotlight check and regenerate the skills manifest

* refactor(core): findProjects moves to @hyperframes/core/projects so Desktop can import it

* chore(core): publish the projects subpath

* fix(core): findProjects fails on a root it cannot read instead of finding nothing

* docs(core): name the project marker writers without naming one app

* refactor(core): ship findProjects as a core export only, with no CLI command

* feat(core): findProjects stops on an abort signal and defaults to the home folder

* fix(core): findProjects checks an aborted signal first and stops when onProject throws
2026-09-28 06:09:25 +00:00
Miguel Ángel ea494b4672 fix(studio): even out levels lifts the right part of a sped-up or slowed clip (#4636)
* 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): level-out measures and places its lift on the clip clock at any speed

* fix(studio): round level-out lane times and tighten the half-speed test
2026-09-28 06:09:11 +00:00
Miguel Ángel 7fedc689b8 fix: run the configured ffmpeg and ffprobe everywhere and say when an upload was not checked (#4611)
* 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
2026-09-28 06:09:00 +00:00
Miguel Ángel ff14263c03 fix(studio): a host that swaps its multi-clip trim handler gets the new one (#4630) 2026-09-28 06:03:42 +00:00
Miguel Ángel 358c04e2e2 fix(studio): playback keys do nothing while a dialog is open (#4623)
* fix(studio): playback keys do nothing while a dialog is open

With a modal dialog open over Studio (the lint results, for example),
Space, the arrow keys, J/K/L, M, I/O and A/E still reached the player,
so the film behind the dialog played and seeked.

The playback shortcut filter now ignores keys while a visible modal
dialog is in the page: an aria-modal dialog or a native dialog opened
with showModal, wherever focus sits. Dialogs left mounted but hidden,
display:none or inert do not block keys.

* fix(studio): a dialog hidden behind fullscreen preview leaves playback keys working

* test(studio): the no-checkVisibility case runs without an earlier stub
2026-09-28 06:03:31 +00:00
Miguel Ángel 4ed269ad1e perf(core): playback housekeeping runs on a timer, and rebinds stop re-sampling silent media (#4595)
* perf(core): playback housekeeping runs on a timer, and rebinds stop re-sampling silent media

While playing, the transport re-posted the clip manifest every 20 frames and
re-bound media every 30, frame-counted, so a 120 Hz display did both twice as
often for a film that was not changing. Playing, those jobs now run when the
composition changes (rate-limited as before) and on a one-second poll for
what nothing can push.

A rebind re-probed every media element's volume by seeking the whole timeline
60 times per second of clip, about 1,500 seeks and 50-140 ms in one frame.
The probe seeks with events suppressed, so only a tween on the element that
names volume can move it; an element no tween touches is now answered from
the tweens without seeking.

* fix(core): a data-width or data-height change applies while playing and parked

postTimeline applies the root's sizing, and it now runs when the composition
revision moves. The revision only watched timing attributes, so a size edit
waited for a pause while playing, and on main it was missed while parked
too. The observer now also watches data-width and data-height. The tick
comment is trimmed to three lines, the tick reuses its playing flag, and the
cadence test is renamed to what it checks now.

* test(core): share one 120 Hz frame step in the transport park tests

The two playing tests each defined the same frame helper; one frame120Hz
now sits beside settle and all four loops use it. No source change.

* refactor(core): drop the playing poll doc comments

The comment ratchet does not let a file's comment share rise; the reason
lives in the PR body.

* fix(core): a playing clip's track, label and z-index reach Studio again

The playing path had dropped the periodic manifest post, and the
composition observer does not see data-track-index, data-timeline-label or
an inline z-index on an existing clip (watching style would fire on every
GSAP frame). On the playing poll the runtime now builds the manifest and
posts it only when it differs from the last one posted, so an edit lands
within a second and a steady film still posts nothing.

* fix(core): keep sampling volume when a tween drives a non-DOM object

A tween on a plain-object proxy whose setter writes the element's volume
names neither the element nor volume, so the fast skip dropped it and
WebAudio-routed preview played the static data-volume (0 where the film
asks for 0.375). The skip now applies only when every tween targets DOM
nodes; any other target falls back to the full volume probe, as on main.

* fix(core): skip the volume probe again for spacers, calls and counters

The non-DOM fallback fired on any plain-object or function target, so a
tl.to({}, {duration}) spacer, a tl.call or a tweened counter brought back the
full probe on ordinary films. It now falls back only when a tweened property
resolves to a setter on the target or its prototype chain, which is what a
gain proxy or a class with a volume setter uses; data properties skip.

* fix(core): post a clip edit made while playing when playback stops

A label, track or inline z-index edited while playing and then paused before
the next one-second poll was never posted: once parked, nothing watches it.
The tick where playback stops now runs the same manifest diff once and posts
only if the manifest changed.
2026-09-28 05:58:37 +00:00
Miguel Ángel 13bee74960 fix(lint): warn when a clip ends past the root composition duration (#4633) 2026-09-28 05:58:25 +00:00
Miguel Ángel fa044a89e5 feat(cli): tag telemetry and feedback with the app that launched the CLI (#4632)
* feat(cli): tag telemetry and feedback with the app that launched the CLI

* fix(cli): keep the client tag to a short slug and correct the telemetry docs

* test(cli): read the delivered batch through the shared helper

* refactor(cli): let the tag pattern alone bound the client tag's length

* docs(cli): describe the client tag generically as the launching app's tag

* style(cli): format the client tag test cases

* test(cli): use a neutral example value for the client tag

* chore(skills): regenerate the skills manifest for the telemetry reference
2026-09-28 05:11:11 +00:00
Miguel Ángel f55c0bbcb4 fix(studio): the live clip list carries mute, volume and sound, and refreshes on change (#4626)
* fix(studio): the live clip list says whether each clip is heard and refreshes when that changes

* fix(studio): a muted clip reads as muted and a timed wrapper does not take its video's sound

* test(studio): a timed wrapper ignores the data-has-audio of the video inside it

* refactor(studio): keep the clip change check and audio reader under the complexity gate

* fix(studio): a fade-only change refreshes the live clip list
2026-09-28 04:38:07 +00:00
Miguel Ángel e1e2b16e21 perf(studio): a film with no manual edits skips the edit reapply on each seek (#4592)
* perf(studio): a film with no manual edits skips the edit reapply on each seek

The reapply runs after every timeline seek, so on every playback frame, and
ran six document queries plus the motion rebuild even when Studio had never
edited the film. One combined query now answers "any edits?" first.

* test(studio): the seek skip still clears a motion timeline after an undo

An undo removes a motion edit's marks while its timeline stays registered,
and the reapply is what kills it. A test now fails if the skip stops
treating that timeline as an edit. The moved comments are cut to three
lines and lose an all-caps word.

* perf(studio): the per-seek edit check no longer walks large films or adds a lookup with edits

The combined comma selector took Chrome off its lone-attribute fast path: about 150 us per
seek on a 1.5k-element film, against 6 us for the whole reapply on main. The check now uses
one selector per mark and caches its answer per document, rescanning only after a
MutationObserver sees a mark attribute or the tree change.

* test(studio): the seek edit cache sees inserted edits and ignores style writes

Covers the two observer settings the cache depends on: a node inserted with its edit mark
after a seek is reapplied on the next seek, and a style write between seeks causes no rescan.
2026-09-28 04:17:27 +00:00
Miguel Ángel b41ef425cc fix(release): name Release once in a changelog entry's tags (#4567)
* fix(release): name Release once in a changelog entry's tags

* fix(release): the release tag appears once in any case; four past entries lose the duplicate
2026-09-28 04:04:00 +00:00
Miguel Ángel a380036903 docs(cli): render help says when page-side compositing is actually used (#4572)
* docs(cli): render help says when page-side compositing is actually used

* docs(cli): page-side compositing help names webm and mov and the fast-capture flag

* docs(cli): say plainly that gif keeps page-side compositing
2026-09-28 04:03:48 +00:00
Miguel Ángel 078c493448 perf: the preview loads and decodes only the images of scenes on screen or due next (#4578)
* 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
2026-09-28 04:03:36 +00:00
Miguel Ángel bc73c411f0 feat(studio): let a host app mount the timeline toolbar, undo buttons, meters and clipboard (#4629)
* feat(studio): let a host app mount the timeline toolbar, undo buttons, meters and clipboard

* test(studio): lock in history button rules and trim duplicate export tests
2026-09-28 03:37:32 +00:00
Miguel Ángel 04d56d06aa fix(producer): renders use the font named in a var() fallback (#4619)
* fix(producer): renders use the font named in a var() fallback

When a font-family starts with var(--x, A, B) and --x is not defined, the
render now reads A, B the way the browser does. A system font at the head of
that fallback gets Inter placed in front of it, the same rule a direct
system-font stack already follows. The rewrite stays inside the var()
fallback, so a defined --x still wins. A defined property whose value is
itself a var() is followed too. The distributed plan check reads the same
answer.

Fonts named only in such a fallback are best-effort: they are embedded when
they resolve, and one that does not resolve is logged instead of failing a
distributed render. Fonts named directly, or through a defined property,
still fail closed as before.

CSS-wide keywords such as inherit are no longer treated as font names to
fetch.

Compiled the way a distributed render compiles them, 28 registry items now
embed a font they name this way, and none newly fails.

* refactor(producer): fold var() fallback resolution into one branch

* fix(producer): fonts behind a var() chain or empty fallback add no new render failures

A family reached through a chain of defined var()s is optional like a
fallback-only one: fetched fail-closed, skipped only when no source serves
it. An empty var() fallback makes the declaration invalid, so the browser
inherits and the plan check no longer rejects the families after it.

* test(producer): a family named after a leading var() stays required

* fix(producer): a transient error on an optional font warns and renders the fallback

* chore(catalog): record the font requests var() fallbacks now make

* fix(producer): required fonts fetch before optional ones can spend the time budget
2026-09-28 03:30:21 +00:00
Miguel Ángel 4094c7be4c fix(shader-transitions): shader transitions in renders keep scene layout, styles and animations (#4615)
* fix(shader-transitions): page-side shader renders keep scene layout and fonts and leave no ghost

* refactor(shader-transitions): one loop stages both scene copies

* fix(shader-transitions): stage scene copies inside copies of their ancestors

* refactor(shader-transitions): pin the staged root in one assignment

* fix(shader-transitions): keep ancestor copies out of scene layout in renders

* fix(shader-transitions): scene copies keep wrapper effects and live animation times

* fix(shader-transitions): hold copied animations with one lookup per scene and drop the no-op shift

* test(shader-transitions): pin the element and pseudo-element parts of the animation match

* docs(shader-transitions): keep the staging comments to two lines
2026-09-28 03:30:06 +00:00
Miguel Ángel 2ebfcc04a3 fix(studio): read and write a clip's in-point the way playback does (#4624)
* 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
2026-09-28 03:08:13 +00:00