The shadow reload promotes on frame-driven readiness (the player's
paint-and-idle, then two requestAnimationFrames) but failed on a
wall-clock 15s setTimeout. A hidden tab renders no frames, so every
external edit made while Studio was in the background failed with
"it took too long to load", the toasts stacked, and the stale preview
stayed up even after returning to the tab.
The budget now only runs while the document is visible: hiding the tab
clears the timer, and showing it again restarts the full budget for the
pending shadow, which then promotes normally.
Fixes#4290
— Rames Jusso
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(registry): add text-match-cut — one text becomes another mid-move on a single 85 % curve
Video primitive reversed from an After Effects tutorial (Editing Snapped, "The Match
Cut Technique Every After Effects User Needs"), measured per frame at 60 fps. Both
texts ride one keyframe pair, cubic-bezier(.85, 0, .15, 1) over 2 s, and hard-swap at
the speed peak (1.0 s). A `mode` variable picks the property that carries the cut:
scale down (the tutorial), scale up, tracking in, or a same-direction slide in four
directions. Optional cyan-to-blue gradient sweep on its own 87 % curve, navy or
transparent background.
* chore(registry): integrate text-match-cut with current main
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
* feat(registry): add extended-keyframe, the never-fully-lands arrival with motion blur
A hyperframes:component. An element covers most of its move in the first fifth of the
duration and creeps the rest to the last frame (After Effects' extended-last-keyframe
technique), exits along the vector it arrived on, and carries shutter motion blur:
slides via the bundled feOffset filter smear, scale beats via a velocity-driven blur.
var a = attachExtendedArrival("#word", tl, { at: 0.3, from: { x: -1400 } });
attachExtendedArrival("#next", tl, { at: a.end, from: { scale: 0.35 } });
Direction and property agnostic (x / y / scale). The ease is exposed on its own as
EK.ease() for any tween, with EK.next() for sequencing and EK.gate() as a numeric check
(single velocity peak, no rises after it, tail above zero to the last frame).
registry/registry.json, docs/docs.json and docs/public/catalog-index.json regenerated
with the two generator scripts; they also pick up drift already present on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(registry): publish live extended keyframe payload
* fix(registry): tear down extended keyframe blur
* fix(catalog): refresh extended keyframe payload
* fix(catalog): preserve merged catalog entries
* fix(catalog): regenerate catalog index
* fix(registry): trim extended-keyframe comments
* chore(registry): retrigger checks
* fix(registry): refresh extended-keyframe artifacts
* fix(catalog): refresh extended-keyframe payload
* fix(registry): refresh extended-keyframe artifacts against current main
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
check-registry-set-delta.mjs only recognized removals: an added name in
registry.json had no matching allowance, so any pure-addition PR failed
with "unexpected in registry.json: <name>" even though the item's own
registry-item.json was added in the same diff. Mirror the deletion rule
for additions: a name is expected when its own registry-item.json was
added or deleted in the same diff, same condition either side already
required.
CI's audit tool flagged the CLI entry itself (high-crap-score rule,
scripts/check-registry-set-delta.mjs:30 main, CRAP 42.0 threshold 30.0,
cyclomatic 6): main() shelled to git and parsed JSON inline. The git/fs
boundary is now injected as runRegistrySetDeltaCheck(base, { run,
readRegistryJson }), covered directly by a test with a fake run/read, so
main() is a thin print-and-exit wrapper.
* feat(studio): render geometry-sized video filmstrips
* fix(studio): keep poster tier during timeline scroll
* fix(studio): cap rendered thumbnail tiles
* chore(studio): refresh filmstrip checks
* chore(studio): refresh filmstrip captures
* chore(studio): refresh four-times captures
* fix(studio): stop capping filmstrip render tiles at 33
computeThumbnailStrip no longer clamps frameCount; the decode-side cap already lives in quantizeThumbnailFrameCount. VideoThumbnail only acquires the rich filmstrip lease when requestFrameCount > 1, so a narrow clip issues one decode job instead of two identical ones. Deletes the unreachable duplicate defaults in createVideoThumbnailRequest.
* style(studio): format filmstrip changes
Implements the fixes from the 2026-09-21 cutover dashboard read
(plans/sdk/2026-09-21-cutover-dashboard-fixes-plan.md):
- sdk_cutover_failed now carries `family` and `error_kind`
(network/conflict/http/sdk). The one production gate event was a raw
`TypeError: Failed to fetch` from an unwrapped fetch (readProjectFile /
writeProjectFile's preflight), which an instanceof-only classifier would
still file as "sdk" and trip the rollback gate — added a message-based
fallback for the browser's fetch-rejection strings.
- studio:sdk_session_unavailable's `network` reason now carries `elapsed_ms`
and `hidden` (document.visibilityState at rejection), separating a
near-instant policy block (CSP/PNA/extension) from a dropped connection
the tab or server going away mid-flight.
- The ownership-race branch in useSdkSession's open effect now emits
sdk_session_superseded instead of sdk_session_unavailable{stage:ownership}
— it's a benign race (identity moved on mid-open), not a broken
precondition, and was being counted as one on the dashboard.
- Root-caused and fixed the read-404 "storm": /api/events (CLI host) and
the vite-dev hot-reload channel are one connection per SERVER, not per
project. A stale tab left open after `hyperframes preview` restarts on a
different folder (same port) received every save the CURRENT project
made, reloading its preview (which then 404s) and re-reading its own
composition on each one. Both transports now carry `projectId`; the
client-side coordinator drops a delivery whose projectId doesn't match
the tab's (absent field still passes, for one release of skew).
- studio-server's file routes report `why` on 403/404: `project_dir_missing`
(new — the CLI host's static resolveProject kept answering for a project
whose folder was renamed/deleted under a running server, which used to
fail closed as an indistinguishable path-traversal 403) vs `nul` vs
`outside_project`.
- absent_or_empty reads now carry `path_in_tree` (against the caller's
loaded file tree). Instrumentation only, per the graveyard page
(clearing-activecomppath-on-tree-absence) that refuted the "clear
activeCompPath" fix this was the proposed next step for.
Tests: unit coverage added for every change (sdkCutover.test.ts,
useSdkSession.lifecycle.test.tsx, useExternalFileChangeCoordinator.test.tsx,
studioServer.test.ts, files.pathSafety.test.ts). oxlint/oxfmt clean,
tsc --noEmit clean on studio/studio-server/cli.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* feat(registry): add colorama-wipe, a colour-mapped feathered wipe reveal for headlines
A hyperframes:component recreating the After Effects "shiny wipe": masked text +
Compound Blur + Colorama over a moving gradient, with a crisp duplicate fading on top.
One call per text element:
attachColoramaWipe("#headline", tl, { at, in, hold, out, cycle, feather, tilt, blur })
adds a single linear clock tween to the host timeline and paints the effect on two
canvases over the element's box; the element is hidden (visibility) with its layout
kept. All sizes scale from the element's font size. Deterministic: hash noise keyed by
frame, no Math.random, no clocks; seeks outside the window paint an empty stage.
registry/registry.json, docs/docs.json and docs/public/catalog-index.json were
regenerated with the two generator scripts as the contributing guide asks; they also
pick up drift already present on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* feat(registry): publish live colorama wipe payload
* style(registry): format colorama wipe demo
* fix(registry): tear down colorama wipe on timeline kill
* fix(catalog): refresh colorama wipe payload
* fix(catalog): preserve merged catalog entries
* fix(catalog): regenerate catalog index
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* feat(studio): timeline clips take the designed material
Picture clips use an 8px radius and audio clips a 21px pill.
Clips sit one step above the lane. The label is a chip that
drops under 60px. Selection keeps the inset outline plus a
1px inner highlight. Hover background is the only transition.
* style(studio): format timeline clip shadow
* fix(studio): restore handle-bar transition and de-pin box-shadow test
* feat(studio): menus and popovers paint instantly
Menus and popovers open and close with no fade. Hover colour is unchanged.
* fix(studio): fade menus in from a visible shape
Menus, popovers, and dropdowns start at opacity 0.8 and scale 0.97, then settle in 120ms. They close in 90ms. Tooltips use the same shape in 100ms.
* ci: select the remaining orphan tests in their owning runners
* ci: provide FFmpeg for the catalog preview witness
* ci: pass the installed Chrome path to catalog rendering
* test: align creator capability checks with speed ramp support
#4153 added supported speed ramps as rate lanes in data-automation. The
previously unselected capability tests still required preprocessing.
Require the documented rate-lane representation and its precedence over
the constant playback rate, matching the runtime resolver.
* feat(registry): add a week-in-merges leaderboard block
Authors race across the row at a speed set by their merge count, bouncing at both edges.
* chore(registry): cover week-in-merges in the catalog search index
* style(registry): format the week-in-merges block
Wrap the lines oxfmt rejects so the format check passes.
* fix(registry): regenerate the week-in-merges catalog payload
The format pass left the committed payload off the generator output.
* feat(registry): cap week-in-merges at twelve rows
The block races at most rows_max authors, highest merge count first, and writes one line for everyone left out. The default is 12, so a long list cannot shrink the rows.
* fix(registry): make week-in-merges speed follow merge count
A row now travels the leader's pace times its merges over the leader's count. Only a zero stays on the floor, so a near-tie no longer looks like last place.
* feat(studio): audio meter faders for groups and master
* fix(studio): meter's mid band uses amber-500, the theme's actual token
yellow-500 was reset to nothing by palette-reset.css (--color-*: initial)
and never redefined in theme.css, so the mask never revealed any color
for -6 to -3dB; the token gate test already catches this and was red.
* refactor(studio): rename YELLOW_AT to AMBER_AT to match the amber token
* fix(studio): fader nudge and aria-valuenow match the visual dB position
* refactor(studio): use core StereoLevel for meter channel shape
* fix(core): emit levelTap in the packed core build
* fix(studio): name the preview fader Monitor and share clampNumber
* fix(studio): split the meter rAF tick and cover its branches
* style(studio): oxfmt AudioMeterStrip tests
* fix(studio): preserve authored gain in audio meter faders
* fix(studio): make timeline audio meters opt-in
* feat(studio): typed audio values, clip fades, audible video drops
Audio rows in the media inspector (Volume, Speed, Media start) take a
typed value: click the readout, type "-6 dB", "1.5x" or "2.5s", Enter.
Speed presets are hidden on <audio> clips.
Clip-edge fades: data-fade-in / data-fade-out in seconds, honoured by the
preview runtime (per-tick gain), the render mixer (afade after volume) and
Studio (Fade in / Fade out rows, and drag dots at the top corners of audio
clips and audible videos). Linear on both sides so preview and render
agree; fades that outrun the clip scale to meet inside it.
Dropped videos with an audio stream now land with data-has-audio="true"
instead of muted, so the mixer keeps their sound. The media metadata
endpoint reports hasAudio from the ffprobe call it already makes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(audio): one owner for fades, dB, and afade-after-volume
Illegal fade values become no-fade at fadeGain; typed inspector input
still returns null so the readout stays red. Master and group mixes
share one filter chain with volume then afade. Fade-out stays anchored
to the trimmed clip end.
* fix(studio): share audio helpers and bound typed media values
* fix(studio): reserve sibling fade duration when editing
---------
Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* feat(cli): move media-use engine into the CLI
* fix(cli): replace media-use compatibility asset
* fix(cli): replace moved media-use tree
* fix(cli): copy moved media-use files cleanly
* fix(cli): copy media-use engine files individually
* fix(cli): keep audio helper compatibility import
* fix(cli): clear copied skill destinations
* test(cli): preserve moved media-use test paths
* fix(cli): preserve media-use script compatibility imports
* fix(cli): preserve media-use telemetry shim
* fix(cli): resolve packaged media-use LUT assets
* fix(cli): resolve packaged bundled SFX assets
* chore(skills): regenerate media-use manifest
* fix(cli): preserve media-use parity coverage
* fix(cli): cover relocated media-use tests
* chore(skills): regenerate media-use manifest
* fix(cli): format bundled media provider
* fix(ci): run relocated media-use tests with node
* fix(ci): preserve fallow script exemptions
* fix(ci): scope media-use fallow exemption
* refactor(cli): trim media-use essay comments
* feat(cli): index media-use assets for catalog search
* feat(catalog): commit media-use search vectors
* chore(catalog): track media vector metadata
* fix(catalog): validate media vector row order
* fix(catalog): validate media artifact freshness
* fix(catalog): cover media metadata and credits
* fix(cli): reject malformed media rows
* fix(catalog): compare complete media rows
* feat(cli): route media search through media-use
* refactor(cli): trim relocated media-use comments
* fix(cli): make media search runtime-safe
* fix(cli): preserve sfx fallback diagnostics
* fix(cli): probe sfx fallback provider explicitly
* chore(skills): regenerate manifest
* refactor(cli): keep typed media ranker source
* fix(cli): align media search checks with local-first flow
* fix(cli): scope vector audit exceptions
* fix(cli): ship resolver registry beside skill
* fix(cli): keep media-use skill scripts standalone
* test(cli): cover local vector validation
* test(cli): correct media vector fixtures
* fix(cli): prefer exact media keys
* fix(cli): stem exact media keys
* test(cli): skip media lint for snippets without media
* fix(cli): align skills test discovery
Keep the CI skills job's bash discovery as the single source of truth so its empty-list guard covers the exact files Node executes. Make hasMediaVectors private because it has no external callers and needs no Fallow suppression.
* fix(cli): close media-use review gaps
Prioritize literal ids before stemmed ids and cover the collision alongside the plural regression. Add command seam tests for engine discovery, actionable failure, and argv passthrough. Add a deterministic parity check for standalone media-use copies, with documented allowlists for the standalone media-fetch shim and self-contained npx resolver. Keep the bash-discovered skills test list as the CI source of truth.
* fix(cli): lower parity check complexity
Split missing-copy and unallowlisted-drift checks so the parity guard keeps its behavior without a threshold CRAP score.
* fix(cli): close media-use review seams
Run the parity test in the skills job so skills-only edits exercise the same check. Export the verb mapping and inject command dispatch for direct coverage. Keep missing-copy diagnostics separate from drift reads.
* feat(studio): audio level meters at the timeline's right edge
* test(core): cover metering started before the audio context finishes init
* fix(studio): keep meter follow after a dead preview and start bars at silence
* fix(studio): keep the 0 dB peak tick inside the meter
* fix(studio): keep the 0 dB peak tick visible without calc()
* fix(core): tap meters on the program bus, mute on a monitor gain
* style(core): wrap applyMasterGain to satisfy oxfmt
* docs(changelog): weekly digest 2026-09-14 to 2026-09-21
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(changelog): embed weekly video for 2026-09-14 to 2026-09-21
Adds the rendered weekly changelog video to the Sep 14-21 digest entry,
using the file's existing DocsVideo component and CDN path convention.
Video: 52.4s, square 1080, six themed scenes cut from the digest's eight.
Gates green before upload: hyperframes check 0 errors (layout 0 issues
across 33 samples, contrast 67/67), seam gate 0 fail / 0 warn across 7
seams, captions present across all 1572 frames, all scene midpoints and
payoff frames read individually.
CDN object verified byte-for-byte after invalidation (sha256 match,
x-cache Miss).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(studio): report a rejected read as a network reason, not an open failure
`stage: "open"` is documented as `openComposition` threw — an unparseable
composition, or OOM. Every event in that bucket on 0.8.56 and 0.8.57 carries a
fetch-rejection message instead: "Failed to fetch" (28 users / 11 users), "Load
failed", "NetworkError when attempting to fetch resource.".
The cause is one missing catch. `readProjectFileOptional` awaits `fetch` with no
try/catch, so a rejected request escapes the function and is caught by the
effect's outer `.catch` — the handler written for `openComposition`. The read
path has a five-member reason union precisely so this class of failure is
diagnosable, and the one case that never produces a response was the case
outside it.
That made the largest SDK-session failure class in Studio (2.9% of session users
on 0.8.56, 2.3% on 0.8.57) unaddressable: you cannot fix a network failure while
it is reported as a composition that failed to parse.
Now `readProjectFileOptional` catches its own rejection and returns
`reason: "network"`, so it lands in `stage: read` with everything else, and
`stage: open` means only what its comment says.
Telemetry only. Nothing branches on the stage and no user-facing behaviour
changes. Fifth split of this shape, after `ineligible_operation` (v0.8.49),
`stage: read` (v0.8.51) and `block_detail` (#4214).
Co-Authored-By: Claude Code <noreply@anthropic.com>
* test(docs): point the catalog tab-row test at the page gutter #4238 moved the inset to
`catalog-detail.test.ts` asserted the exact strings that #4238 deleted:
`.hf-ve-tabs-row { margin: 20px 0 0 16px; }` and `.hf-ve-body { padding: 2rem 0 0 16px; }`.
That PR removed both insets deliberately — its own comment says "the page gutter is
the page's job, and a second 16px here put everything below the stage on a
different left edge from the title, install block and stage above it" — and
restored the gutter on `#content-area` in `docs/custom.css` instead. The test was
left asserting the old mechanism, so main has been red on it since, unnoticed
because the `Test` workflow does not run for a docs-only change.
The invariant did not change: this content is still inset from the page edge. Only
the mechanism did, so the test now asserts both halves of it — the two rules carry
no inset of their own, and the gutter standing in for them is still present, at both
its values. Asserting only the first would keep passing if the gutter were deleted
outright, which is the regression the test exists to catch; both mutations were
checked and both fail.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Bump @puppeteer/browsers to ^3.2.2 and puppeteer-core to ^25.10.0 so Chrome spawns detached only off Windows (upstream puppeteer #15415). Lock regenerated on top of main. Fixes#4028.