* feat(studio): shortcut combos in the key resolver
* feat(studio): every editor shortcut is a remappable combo
* refactor(studio): playback keys dispatch through one table
* fix(studio): combo parsing without backtracking, symbol keys match, fewer comments
* style(studio): format the shortcut files
* chore(studio): drop a doc comment the ratchet counts
* chore(studio): drop a doc comment the ratchet counts
* fix(studio): modifier combos fire for playback keys, caption guard and held stop key follow the map
* fix(studio): a preview reload that timed out is tried once more, so an edit is not left unseen
* fix(studio): trim the retry hook's comments to the package's share
* fix(studio): a clip's thumbnail strip stays put while you trim it
* fix(studio): cover the trim-to-hold link and drop the comment
* refactor(studio): the held trim range lives in its own small hook
* refactor(studio): the thumbnail's held request inputs move into one hook
* chore(studio): drop the hook docstring
* feat(studio): a host that draws its own loader can turn off both of Studio's preview loaders
Player and NLEPreview take loadingUi: 'none', which hides the composition cover
and the 'Preparing preview assets' wait while still reporting that the preview is
loading. suppressLoadingOverlay keeps its narrower meaning: skip only the
composition cover on a later load.
* refactor(studio): name who draws the loader in the value, loadingUi 'studio' | 'host'
The value says the host draws its own loader, so no doc line has to tell it apart
from suppressLoadingOverlay, and the files' comment share does not rise.
* refactor(studio): the loader choice sits outside Player, the stage size beside NLEPreview
Player's draw decision moves to a small helper so the component's complexity does
not rise, and resolvePreviewStageSize moves to its own file so NLEPreview stays
under the 600-line cap with the new prop.
* refactor(studio): Player reads the host's loader choice without another destructured prop
Destructuring one more prop raised Player's cognitive complexity above main's; the
rest of the props carries loadingUi instead.
* refactor(studio): NLEPreview's stage constants live in one small module
* style(studio): format NLEPreview's stage import
* feat(core): the playback health reading says which clock the film followed
* style(core): format the clock's new reading
* refactor(core): let the clock's names say what the comments said
* feat(core): playback health reports how long a video on screen showed no moving picture
* fix(core): a frozen picture is judged by the clip's own timing, not the film's
* style(core): shorten a test comment under the line limit
* fix(core): the frozen-picture check reads a video from any realm
* refactor(core): cut the frozen-picture comments the comment ratchet counts
* perf(studio): a long timeline no longer lays itself out on every render while it settles
* perf(studio): compare row data with sameData and drop the added comments
* fix(studio): a split at a clip's edge leaves the file unchanged
* fix(studio): keep the split margin's doc within the comment width
* chore(studio-server): keep the split margin's comments within the file's share
* perf(studio): films with thousands of timed clips stop re-reading every clip each frame
* perf(studio): drop the added comment lines the ratchet counts
* test(studio): edit accuracy evidence screenshots capture only the viewport
* test(studio): edit accuracy screenshots only in the gate's confirmation re-runs
* test(studio): keep the inStudio doc comment on inStudio
* ci(studio): run the pull request edit accuracy slice in 10 shards
* ci(studio): run the pull request edit accuracy slice in 15 shards
* ci(studio): pack edit accuracy shards by measured case time so they finish together
* ci(studio): load shard timings with require so happy-dom tests can import the grid
* ci(studio): the slow runner stop judges runner speed, not how long a shard's first cases are
* ci(studio): keep the slow runner comment on one line under 120 characters
* feat(studio): a preview reload that gives up on the runtime says how far the page got
* refactor(studio): read the reload's page phase in one place, before the frame is let go
* test(studio): every reload page phase is covered, and the hook gains no branch for it
* refactor(studio): the reload page phase reads the window only once the frame has a document
A composition with no GSAP timeline was treated as zero length after an edit, which dropped preview audio, and an edit made during playback left the transport stopped. The runtime now keeps the declared length when it rebinds, and the soft reload resumes a transport that was playing.
* fix(studio): an undo or redo counts as a preview change from its key, not from its reload
* refactor(studio): count a history step from its key with the page flag on noteIdle
* fix(studio): a tab's save keeps the newer video version another tab recorded
The thumbnail index merge let this tab's ETags win for every source, including
ones it only read at startup, so another tab's move to a new version was undone
by this tab's next save: the next session validated the old ETag, missed the
disk and decoded once more. Now another tab's ETag wins except for sources this
tab recorded a new version of since its last save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): the ETag merge says whose moves it keeps in its names, not comments
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(studio): the stored-thumbnail reset forgets this tab's drops and moves too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* perf(studio): timeline thumbnails come back from disk in the next session
Reopening a project re-read every video's index and re-decoded every strip:
the frame cache and the source facts lived in memory, keyed by the session.
Now each source's facts (aspect, duration) and every decoded frame are kept
in Cache Storage under the file's ETag, the version the server says it is. A
new session checks each source once with a conditional one-byte read: a 304
means the stored frames are still that file's, and a strip they cover paints
without opening a reader. A new ETag drops what was stored for the old one.
Sources are dropped whole, least recently used first, past 256 MiB. A store
that is missing or fails (private window, quota) warns once and the strip
decodes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): a reopened project checks its videos again, and the disk index counts a frame once
The stored version a session validated was kept per video for the whole page,
so reopening a project after one of its videos was replaced at the same path
painted the old file's frames until a full reload. It is now kept per video
and project open (its contentVersion): each open re-validates once.
The index now counts a frame stored again under its name once, and merges
with what other tabs saved before writing, keeping caches this tab dropped
out, so the 256 MiB budget holds across tabs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): a preview media load that fails is reported, not swallowed
* fix(core): an audio fed by source children reports its failed load; the shape lives in the protocol
* fix(core): the audio load watcher finds elements from any realm
* build(core): regenerate the inline position-edits render script
* fix(studio): a timeline edit on a clip the file lacks says how the clip was looked up
* fix(studio): name only the lookups a missing clip was really found by
* refactor(studio): split the missing-clip lookup words into small helpers
* refactor(studio): fit the missing-clip lookup words under the no-visible-change size
* fix(studio): a thumbnail reader forgets a start's failures after a minute
A pooled thumbnail reader lives for as long as clips keep asking it, and each start
byte has a retry budget of two. That budget never reset, so two transient failures at
one start early in a session made a third, twenty minutes later after the cache evicted
it, give up at once: the reader retired and the clip showed an error.
A start's failures now count for a minute. One bout of retries (8 s stalls, 0.5 s and
1 s waits) fits inside it, so a start that keeps failing still gives up; one met again
much later starts afresh. Clearing the count when a read from that start delivers bytes
would not do: a server that ignores Range sends bytes 0 onward again before it breaks at
the same byte, and would retry forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): the start-failure window says what it is in its name, not a comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): a start's retry budget resets when a read from it ends cleanly, not after a minute
The one-minute window only held while each attempt took under a minute. An attempt has
no limit before its answer, and a body that keeps sending never stalls, so a server that
ignores Range, sends slowly and breaks past a minute re-downloaded from 0 forever.
A start's failures are now forgotten when a read from it ends cleanly: its body finishes,
or mediabunny cancels it at its target. A server that breaks before the target never ends
a read cleanly, so its budget still runs out, however slowly it fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): a read mediabunny stops at its target ends cleanly and resets its start's budget
mediabunny's UrlSource never cancels the body: once a read reaches its target, _runWorker
aborts the signal it passed in, and the pending pull fails with an AbortError. So the reset
on cancel never fired, and any open bytes=N- read stopped at its target kept its start's
failures.
The request now listens on its own signal, which follows mediabunny's and is unlinked once
the read ends: an abort while live counts as a clean end only when mediabunny's signal is
the one aborted, never a stall or a dispose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): a slow thumbnail job keeps a reader that other clips still decode from
A job that timed out judged the reader by its own progress only. A poster job that got
an already-open reader while a sibling was decoding on it, and took more than its
budget to reach its one frame (a slow link, Chrome's six connections held), logged a
timeout and retired a healthy reader, so the next clip re-read the index.
The pooled reader now counts the frames every job decodes from it, and a timed-out job
retires it only when no frame came from it while the job held it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): the reader's frame count says whose frames it counts in its name, not a comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(core): name why a choppy preview window dropped frames
The playback reading now says how much of the window was frames skipped
and how much was a playing video waiting for data, and carries the p50
and p95 gap between the runtime's ticks, so a report can tell a slow
compositor from a stalled decoder.
* chore(core): drop comments the ratchet counts