* fix(studio): hide handles Studio would refuse to commit
Studio showed move, resize and rotate handles on animated elements whose
motion it cannot write back (a helper or loop, a runtime-computed value, a
live tween with no matching source). The drag ran, the commit was refused
at release, the element snapped back and a toast explained why.
The commit's refusal rules now also run when a selection resolves:
- move uses the existing drag preflight, now exported
- rotation and resize rules are extracted into preflight functions that the
commit paths call first, so both use the same decision code
- the session narrows canApplyManualOffset / Size / Rotation (with the
refusal's message as reasonIfDisabled) in one place, and the chrome,
nudge, group drag and design panel read that unchanged
- until an element's first check lands, its manual flags stay closed
A press on a box that cannot move now shows the reason at once instead of
after 4px of travel.
* refactor(studio): read the rotation preflight selector once and derive the scale flag
* fix(studio): make the handle check read what the commit reads, and never stall
- Match tweens to an element the same way everywhere: the animation fetch
used by the handle check and by group moves now also matches through the
live element, so an element a class tween animates keeps its handles.
- The handle check reads each file's parse once per edit version. A file with
no tweens is an answer, not a cold parse to retry, so plain elements get
their handles after one read and later selections in that file at once.
- A failed read is not remembered: the next selection asks again.
- While the check is still running a press stays silent, and only the primary
button explains a blocked press. A group press with a blocked member now
says why instead of doing nothing.
* fix(studio): check handles per project, keep answers on failed reads, refuse shared group tweens
- The handle check's parse cache now uses the same project and file key as
the parse request, so switching projects never judges one project's file
by another's parse.
- A failed re-read after a save or undo keeps the last good answer and asks
again on the next selection, instead of closing the handles.
- A group move stays refused when a member's motion comes from one tween
shared with other targets, as before: each member would write into the
same tween. The group check and the group commit use the same rule; a
single selection still edits the shared tween.
- The element-to-tween matcher moves to its own module so the move rule can
use it without an import cycle.
* docs(studio): the group move rule comment says which tweens it excludes
* feat(player): play and loop a range of the film with range-start and range-end
The runtime takes a set-play-range control and stops on the range end frame itself.
The player wraps to range-start with loop, fires ended without it, and reports a
range cut to the film with rangeclamped. Video and __timelines players check it per tick.
* fix(player): hold a range on its last frame and let the runtime own range jumps
A runtime with the play-range capability jumps to range-start itself when a new range leaves
the playhead outside, so a stale state no longer ends the film; older runtimes keep the
player fallback, guarded until a state lands inside. Every range stop shows the last frame
before range-end; a paused playhead re-parks only when it falls outside the new range.
* fix(player): stop a range a check early and enforce its end on a video in a background tab
* test(player): share the range video setup
* fix(player): a slow frame no longer cuts a range short; a video without a range is unchanged
* fix(player): a range still stops early where video time moves once per frame
* refactor(player): cut the range look-ahead comment to one line
* fix(player): a per-frame video time rounded down still stops a range early
* test(player): pin no rounding tolerance at a film's own end
* test(studio): expect the play-range capability on control messages
* feat(studio): a host can hide the timeline's audio effects controls
* test(studio): share the whole-timeline mount setup between timeline tests
* fix(studio): hidden audio effects also take the effect-rack shortcut off lane names
* test(studio): share the gain-automated group fixture
* test(studio): use the shared group fixture in every automated-group test
* fix(studio): a disabled group lane name does not light up on hover
* refactor(studio): keep the timeline provider and lanes under the file size limit
* refactor(studio): format the lanes row indexes within the size limit
* feat(studio): a host can save style edits on a selection through Studio's own style commit
* fix(studio): drop an import the moved writer no longer needs
* fix(studio): host style saves recover after a failure and never revert a later edit
* fix(studio): an escape cancels a preview marquee band and stops there
* fix(studio): name the band escape handler instead of commenting it
* test(studio): check the band is gone before the pointer is released
* fix(studio): drilling in opens the composition's full project path
* fix(studio): keep the shared source-path helper as is and trim ./ only for the drill path
* fix(studio): a project folder named preview keeps its path
* perf(studio): swap an edited scene into the live preview instead of reloading
On a preview reload, Studio fetches the rebuilt preview document and asks the preview to swap
in only the scenes that changed. When the preview cannot or will not swap (it predates the
swap, a full reload is already running, or the document changed outside its scenes), Studio
does the full shadow reload as before. A document from an older reload is never swapped in.
* fix(studio): re-sync selection and layers after a scene swap
A swap replaces scene DOM without a load event, so selection, manual
edits, the Layers panel and the edit callout kept pointing at the
replaced nodes. They now listen for either a load or a swap.
* fix(studio): drop a stale scene swap once the preview is replaced
A swap still pending when the preview reloaded or switched composition
could fall back to reloading the old one. Color grading and its compare
view are also re-sent after a swap.
* docs(studio): say exactly when the preview generation changes
* refactor(studio): cut scene swap comments to what names and types do not say
* fix(studio): fall back to a full reload when a scene swap download stalls
* fix(studio): give up on a scene swap that stalls at any step, not only the download
* 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
* test(studio): pin that a playing film keeps playing through 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(studio): a scene swap that times out is cancelled before the reload starts
* fix(runtime): a scene swap refuses when a scene leaves its place or the film during the wait
* fix(studio): a newer edit cancels an older scene swap still waiting
* fix(runtime): a scene swap checks only its host's place, so a later body append does not refuse it
* fix(studio): a scene swap cancelled before it starts does not run
* 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
* fix(studio): timeline Fit fills the width with the film, and a host can hide keyframe controls
* fix(studio): keyframe tests read the claimed K press, and the fit span stays private
* fix(studio): an edit that changes the film's length keeps the timeline scale, and Fit is an icon
* fix(studio): manual zoom keeps its scale whenever only the film's length changes
* fix(studio): drop the width check the kept scale never needed
* fix(studio): a composition switch refits, and the kept scale clamps against the new fit
* test(studio): a project reset refits the manual zoom
* fix(studio): back to the master after a nested drill restores the master playhead
* fix(studio): every way back to the master restores its playhead, 0 included
* fix(studio): the timeline playhead follows the restored master time
The timeline canvas unmounts while a composition level loads and remounts
with its playhead drawn at 0. The playhead effect only re-ran on a time or
zoom change, so a level switch that kept the time left the needle at 0 while
the readout showed the real time. It now also re-runs when the canvas remounts.
Drilling in during playback saved the store time, which playback does not
update per frame; the save now reads the live playhead.
* 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): 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
* 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
* 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
* 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.
* 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
* 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
* 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-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
* 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
* 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
* 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.
* 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
* 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
* 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
* perf(studio): playback no longer relayouts the timecode and playhead every frame
The timecode shows MM:SS but was rewritten on every playback frame, and the
timeline playhead moved by style.left. Both forced a layout and a repaint per
frame. The timecode is now written only when its text changes, and the
playhead moves by a composited transform.
* fix(studio): the moving playhead stays on whole device pixels
As a composited translate the 1px playhead line is anti-aliased, so at a
half-pixel offset it drew as two half-bright columns, where the old left
position snapped it to one. The transform now rounds to device pixels. The
helper moves to its own module as a named function with full parameter
names, which also keeps timelineLayout under the file-size limit.
* refactor(studio): drop the timecode write comment
The comment ratchet does not let a file's comment share rise; the reason
lives in the PR body.
* fix(studio): the playhead moves smoothly while playing and rests on its tick
Rounding the playhead to device pixels on every update made slow playback
stair-step: at fit zoom on a two-minute film, 120 frames of playback gave
22 distinct positions where main gives 120. While playing the transform now
keeps fractional pixels (120 of 120). At rest (paused, or a seek while
paused) it rounds to device pixels the same way layout rounds the ruler
ticks, so the resting line is one full column on its tick at 1x and 2x.
Pausing snaps the line even when the paused time equals the store's.
* fix(studio): quick timeline saves no longer refuse each other as outside changes
* fix(studio): canvas edits and block writes wait their turn in the file queue
* fix(studio): animated clip moves and the last direct writes wait their turn in the file queue
* perf(studio): the lint error badge stops pulsing after three beats
The badge pulsed forever, so a Studio with a lint error painted a frame on
every display refresh, idle or paused. It now pulses three times and holds.
* fix(studio): the lint badge counts findings, never files
The badge showed the manual result's finding count while its dialog was open
and the number of files with findings once it closed, while its screen-reader
label always said findings. The hook now owns one count, the findings in the
list the error pulse also reads, and the badge shows it in both states.
* fix(studio): the lint badge pulses again when its finding count changes
A CSS animation does not restart on a text change, so after its three
pulses a background lint that found more problems left the badge still.
The badge is keyed on its count, so a new count mounts a new element and
pulses three more times.
* refactor(studio): the lint badge names its pulse count, and App reads the hook as one object
The pulse count moves from a comment into a constant name, so the file's
comment share does not rise. App takes useLintModal as one object instead
of a nine-line destructure, which keeps App.tsx under 600 lines once
merged with main.
* fix(studio): list only real compositions under Variables, not generated preview files
* fix(studio): project color grading edits only real compositions, not generated preview files
* 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): an automation lane edit is refused on a locked clip and reports how it saved
Lane drags, Delete and paste save through the timeline's gated clip or group
save instead of the property panel's commit, so the host's canEdit applies and
the commit resolves saved, refused or failed. A landed save refreshes the lane
store and the panel's selection, as the panel's commit did.
* fix(studio): a refused automation save puts the preview and the lane back
A save that does not land (recording, no id, a failed write) reverts the live
preview it was given, the lane drops a draft whose save came back refused or
failed, the no-id and no-project failures toast like the others, and the panel
refresh reads the current selection. The panel commit's unused coalesce option
goes, and the lane value reuses automationAttrValue.
* fix(studio): a failed save restores the preview it was given, not an older one
Each timeline save claims the preview's before-value when it starts, so a
gesture begun while it is in flight records its own and a later failed save
cannot put back a value from before an earlier save landed.
* chore(repo): allow the automation gesture key files this branch removes
* fix(studio): a refused automation save leaves the lane store and paste mark as saved
Reverting a live value re-syncs the lane store from the restored preview, so
an overlapping save that landed first cannot leave the store holding a later
value that failed. A paste whose save is refused or fails gives back the
selection and paste chain it replaced.
* fix(studio): a failed save settles on what the file holds and a refused paste marks nothing
A save that does not land settles the preview and store on the file's value
(the claimed before-value only when the file cannot be read), and leaves a
gesture begun meanwhile alone apart from correcting its before-value, so two
overlapping failed saves cannot leave either on a value that never landed. A
paste selects and marks its span only once it lands and only over the
selection it was pasted at.
* fix(studio): a failed save reads the file only after writes queued to it land
The settle read goes through the same per-path queue as the writes, so a save
that fails while another is queued settles the preview, the store and any
newer gesture's before-value on what that queued save wrote.
* fix(studio): an older failed save never settles over a newer edit on the same lane
Live-preview bookkeeping moves to createLiveLanes, one owner for both timeline
saves: each lane key carries a generation that every live write and save start
advances, and a save settles the preview and store (on its landed value, or on
the file after a failure) only while it is still the newest. A paste marks its
span only if it is still the latest paste.
* test(studio): a newer live drag keeps its preview when an older save recovers
* fix(studio): every finished save records what the file holds, even under a newer edit
A save's settle now always writes the file's value (landed, or read back after
a failure) into the lane store and a live gesture's before-value, and moves the
preview only while nothing newer has touched the lane. The clip store is set
from that value rather than copied from the preview. A paste marks its span
only while its clip is still the selected one.
* fix(studio): an older automation save never overrides a newer gesture on its lane
A failed save's recovery read could settle after a newer save started, and the
save's own optimistic preview patch and failure unwind ran over a newer drag.
Every preview write a save makes now goes through the lane's ownership check,
only the newest save records its value, and a refused drag hands the lane back
to the newest pending save.
* fix(studio): timeline attribute saves land in the order they start
An attribute save read the file before joining its write queue, so an older
save with a slow read could land after a newer one, overwrite it on disk and
leave the lane showing the newer value. The save now joins the queue first and
reads, patches and writes inside it. A save moves the preview only while it is
the newest save and no drag is live, which replaces the per-lane generation.
* refactor(studio): name the in-queue history write for the queue it needs
* test(studio): pin save start order and a newer release's preview mid-flight
* fix(studio): a drag keeps its points when an older save lands under it
An older save that landed mid-drag wrote the lane store, the lane dropped its
draft, and the release committed the older points. The lane now keeps its draft
while a gesture is live. A save that cannot read the file back settles on the
last value read or saved on its lane, never on an unsaved one, and an older save
that lands records its value there even when a newer save is pending.
* fix(studio): a lane's first failed save still puts its drag back
With the claimed-value fallback gone, a first save on a lane that failed and
could not read the file back settled on nothing, leaving the drag playing. A
save begun with no other save pending now records the value under its drag as
verified, since that is what the file last settled on.
* fix(studio): only a before-value read with no save pending counts as saved
The value under a drag was trusted when its save started with nothing pending,
but it was read when the drag began, possibly over a paste that later failed.
A lane now marks a before-value clean only when no save was pending as the drag
began. A group lane's preview carries its store mirror, as its live writes do.
* fix(studio): a lane's saved value never crosses into another project
The lane bookkeeping outlives a project switch and its keys had no project, so
a value saved in one project could settle into another project's clip with the
same key when a save there failed and could not read the file back. Lane keys
now carry the open project, and a save moves the preview or store only while
the project it was made in is still open.
* fix(studio): a failed save reads its file back from its own project
A save that failed after a project switch read its recovery from the newly
opened project and recorded that project's value as its own. Each save now
captures its project when it starts and reads back from it.
* fix(studio): a failed save never brings back a value an undo removed
A lane kept its last verified value after its saves settled, so after an undo a
save that failed and could not read the file back settled on the removed value.
The value now lives only while a save on the lane is in flight, and each save
records what its own in-queue file read found. The group preview's store mirror
is no longer needed and is gone.
* test(studio): pass the file-read callback in the direct attribute-save tests
* fix(studio): a drag whose before-value a save settled counts as clean
When a settle rewrites a live drag's before-value to a verified value, that
value is known saved, so the drag's own save can fall back on it even when its
read and read-back both fail. Adds a test for a failed save reading back from
its own project while another save is in flight across a switch.
* fix(studio): a save started in one composition never lands in another of the same project
Live lanes were scoped to the project only, so a save held across a switch
to another composition of that project settled its value into the new
composition's preview (clip and group lanes) and store (group lane).
Lanes are now scoped to the project and the composition open when the save
starts. createLiveLanes moves to its own module for the 600-line cap.
* fix(studio): an undo during or after a lane gesture stays undone when the next save fails
A live gesture records the lane's value before it starts, and a later save
that cannot read the file settles on that value. An undo mid-drag, or after a
gesture that ended without saving, left that value stale, so the save brought
back what the undo removed. Every undo and redo now re-reads each open lane's
value from the files it restored.
* fix(studio): undoing group automation clears it from the members' lanes at once
The group-info cache only noticed a change when its MutationObserver callback
ran, so the soft restore's store sync, which reads it synchronously, mirrored
the pre-undo group automation onto the members. A read now counts mutations
the observer has not delivered yet.
* fix(studio): an undo reaches every lane and pending save it rewrote, in any composition
The undo re-read only covered lanes in the open composition and live gestures.
History is project-wide, so undoing from another composition left the
rewritten composition's lanes on the pre-undo value, and a save released while
the undo was in flight kept that value as its fallback. The re-read now covers
every lane of the project on a restored path, including saves still pending,
and an undone file it cannot read makes the value untrusted instead of dropped.
A missing element reads as unreadable, the same as a save's read-back.
* fix(studio): a cancelled fade, stretch or preset audition leaves no stale lane value behind
A cancel put the gesture's start back through a live write, which left the
lane's before-value in place until the next save trusted it, even after the
property panel, the source editor or an agent had changed the file. The fade
cancel, the edge-stretch cancel and leaving a preset audition now end through
the lanes' revert, so a before-value exists only during a live gesture.
* test(studio): pin that a group write is read back fresh in the same task
* style(studio): drop two comments the names already carry
* fix(studio): a failed group save reads its own file back; fade cancels work without revert
- The group save's failure read-back used the file of whatever composition was
loaded when the write failed. It now reads the file captured when the save
started, as the clip save does.
- A fade cancel writes its start back live before it reverts, like the other
cancel sites, so a host without the optional revert callback still puts the
fade back.
- The edge-stretch cancel comment says what the code now does.
- The save test harness passes the revert callbacks, as the app does; without
them the cancelled-gesture tests exercised a host that lacks them.
* refactor(studio): trim three long comments on live automation saves
* 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
* 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