4607 Commits
Author SHA1 Message Date
Miguel Ángel 9603fe16e0 Merge pull request #4279 from heygen-com/release/v0.8.60
chore: release v0.8.60
v0.8.60
2026-09-22 03:45:16 -04:00
Miguel Ángel c51a0d7069 chore: retrigger release checks 2026-09-22 03:27:39 -04:00
Miguel Ángel b12e767e5a chore: release v0.8.60 2026-09-22 03:11:40 -04:00
Miguel Ángel 5203312c34 feat(studio): timeline clips take the designed material (#4270)
* 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
2026-09-22 02:15:55 -04:00
Miguel Ángel c029bf4087 ci: require zero orphan tests without a baseline (#4271)
* ci: require zero orphan tests without a baseline

* fix: keep reachability reports diagnostic-only
2026-09-22 01:30:24 -04:00
Miguel Ángel 867787b2f9 feat(studio): menus and popovers paint instantly (#4268)
* 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.
2026-09-22 01:05:00 -04:00
Miguel Ángel cff467a352 ci: select the remaining orphan tests in their owning runners (#4260)
* 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.
2026-09-22 00:54:41 -04:00
Miguel Ángel c27bdda7d9 fix(engine): read extensible PCM and float WAV headers (#4267) 2026-09-22 00:15:42 -04:00
Miguel Ángel d5e00c0e66 feat(registry): week in merges leaderboard block (#4264)
* 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.
2026-09-21 23:35:55 -04:00
Miguel Ángel a80d733219 ci: run media-use tests when media-use changes (#4259) 2026-09-21 22:05:44 -04:00
Miguel Ángel 86e27b00f6 ci(studio): ratchet colour contrast of timeline tokens (#4257)
* ci(studio): ratchet colour contrast of timeline tokens

* refactor(studio): clarify contrast ratchet branches and state citations

* fix(studio): resolve contrast baseline from the repository root

* test: parse contrast channel tokens without color wrappers

* ci: keep contrast gate artifacts outside runtime sources

* test: honor strict indexed access in contrast scripts

* test: require removal of zero contrast baseline entries
2026-09-21 21:35:23 -04:00
Miguel Ángel 0be08f34a2 ci: ratchet test reachability across workflow filters (#4253)
* ci: ratchet test reachability across workflow filters

* fix: fail closed on skipped detection and quoted selectors

* refactor: separate reachability parsing decisions

* fix: keep comments and runner prefixes out of reachability

* refactor: bound reachability parser complexity

* fix: reject conditional shell runner blocks

* fix: reject conditional shell constructs in test routing
2026-09-21 21:14:49 -04:00
Miguel Ángel 2cc384f1fe feat(studio): draw icons that say what the feature does (#4262) 2026-09-21 21:05:20 -04:00
Miguel Ángel 3e63de4778 fix(studio): keep timeline selection ring inside clip (#4251)
* fix(studio): keep timeline selection ring inside clip

* test(studio): cover active dragging selection ring

* fix(studio): keep selection rings above thumbnails
2026-09-21 21:03:56 -04:00
e5689782c0 fix(cli): require non-empty feedback repro commands (#4263)
Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
Co-authored-by: miguel.sierra <miguel.sierra@heygen.com>

Co-authored-by: heygengenesis[bot] <262951085+heygengenesis[bot]@users.noreply.github.com>
2026-09-22 01:02:19 +00:00
Miguel Ángel 1efe02a346 feat(studio): draw the audio meters toggle as a level meter (#4254) 2026-09-21 20:06:54 -04:00
Miguel Ángel eeabbe839f fix(studio): timeline colours all resolve from tokens (#4249)
* fix(studio): brighten unselected timeline clips

* test(studio): pin timeline clip contrast colors

* fix(studio): keep timeline clip contrast themeable

* chore(studio): ratchet hex color baseline

* fix(studio): route timeline colours through theme tokens

* fix(studio): route timeline colours through theme tokens

* test(studio): assert waveform token lookup

* test(studio): bank timeline colour reductions

* fix(studio): preserve timeline token defaults

* fix(studio): preserve active label theme value

* style(studio): format timeline token test

* fix(studio): bank colour literal reductions

* fix(studio): remove zero colour baseline
2026-09-21 20:03:18 -04:00
Miguel Ángel 6f6d24248c feat(studio): audio meter faders for groups and master (#4203)
* 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
2026-09-21 19:25:04 -04:00
Miguel Ángel bf6cc7e32b chore: release v0.8.59 (#4252) v0.8.59 2026-09-21 19:07:41 -04:00
839ae426dd feat(studio): typed audio values, clip fades, audible video drops (#4250)
* 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>
2026-09-21 18:45:08 -04:00
Miguel Ángel 3091dcb787 feat(cli): route media search through media-use (#4230)
* 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.
2026-09-21 18:35:47 -04:00
Miguel Ángel 16fab47a8e perf(player): scope preview readiness to first frame (#4239)
* perf(player): scope preview readiness to first frame

* refactor(core): share runtime visibility timing

* refactor(core): share readiness timing owner

* test(core): pin readiness caller wiring

* test(core): cover fonts in first-frame gate

* fix(core): restore readiness test contract
2026-09-21 17:15:05 -04:00
Miguel Ángel 4e9059ad59 feat(cli): add agent timeline workflows (#4241)
* feat(cli): add timeline agent mutation commands

* fix(cli): type timeline a2 mutations

* style(cli): format timeline a2 commands

* test(cli): cover timeline a2 verbs

* test(cli): assert ids keep named refs

* refactor: centralize timeline duplicate mutation

* fix(cli): narrow set assignment field

* style(cli): format timeline mutation exports

* refactor(cli): split apply plan decisions

* fix(cli): tag apply plan refusals

* refactor(cli): move A2 runners beside timeline helpers

* fix(cli): remove stale A2 imports

* style(cli): format A2 command module

* fix(cli): avoid A2 command import cycle

* refactor(cli): isolate shared timeline mutation logic

* fix(cli): restore timeline mutation command factory

* fix(cli): remove stale command-surface remnants

* fix(cli): complete shared timeline support extraction

* fix(cli): close shared conflict helper

* fix(cli): import shared timeline formatter

* style(cli): format shared mutation runner

* refactor(cli): split mutation command orchestration

* fix(cli): keep shared mutation primitives in support module

* fix(cli): use shared mutation support helpers

* refactor(cli): centralize A2 refusals

* style(cli): format A2 refusal helpers

* fix(cli): clean shared A2 imports

* fix(cli): make A2 runner imports explicit

* refactor(cli): isolate apply plan target validation

* fix(cli): tag prepared plan edits

* style(cli): format plan target helper

* fix(cli): validate sequential plans and duplicate ids

* test(cli): account for stamped composition id

* fix(studio): stamp ids on patched html

* fix(studio): preserve no-op patch bytes

* test(studio): expect stamped patch markup

* fix(studio-server): duplicate elements in nested templates

* refactor(cli): share timeline mutation receipts

* fix(studio-server): harden duplicate and no-op mutations

* fix(studio-server): reduce duplicate helper complexity
2026-09-21 17:14:47 -04:00
Miguel Ángel 97420736d5 feat(studio): audio level meters at the timeline's right edge (#4193)
* 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
2026-09-21 16:19:30 -04:00
James RussoandClaude Opus 5 badf49b551 docs(changelog): weekly digest 2026-09-14 to 2026-09-21 (#4248)
* 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>
2026-09-21 18:57:46 +01:00
Miguel Ángel 0641921558 feat(studio): expose composable timeline parts (#4229)
* feat(studio): expose composable timeline parts

* refactor(studio): split timeline overlay parts

* fix(studio): resolve timeline fallow findings

* fix(studio): clarify timeline part contracts

* style(studio): trim timeline drag comment
2026-09-21 09:03:50 -04:00
Miguel Ángel 952e9228b5 fix(cli): bound capture runs and degrade WebGL navigation (#4215)
* fix(cli): bound capture runs and degrade WebGL navigation

* fix(cli): keep partial capture on deadline

* fix(cli): persist partial capture artifacts

* fix(cli): reduce capture phase complexity

* chore(cli): tighten capture comments

* fix(cli): restore post-capture phase calls

* fix(cli): parse extracted scripts before filtering

* fix(cli): remove next data through dom filter

* refactor(cli): move capture attempt into module

* refactor(cli): extract capture phases into modules

* refactor(cli): split navigation checks

* refactor(cli): deduplicate capture phase modules

* fix(cli): preserve bounded capture output on deadline

* fix(cli): secure capture artifact writes

* fix(cli): close remaining capture file alerts

* fix(cli): mark deadline captures unsuccessful

* refactor(cli): centralize capture phase schema

* style(cli): format capture watchdog test

* docs(cli): condense capture attempt header
2026-09-21 02:52:54 -04:00
Vance Ingalls c797f54542 chore: release v0.8.58 (#4242) v0.8.58 2026-09-20 20:14:03 -07:00
Vance IngallsandClaude Code 5a8ee663f7 fix(studio): report a rejected read as a network reason, not an open failure (#4240)
* 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>
2026-09-20 19:40:06 -07:00
Miguel Ángel c09594fef8 fix(docs): restore the sidebar gutter on catalog pages (#4238)
* fix(docs): restore the sidebar gutter on catalog detail pages

* fix(docs): align the catalog tab row and panes with the page gutter
2026-09-20 21:17:09 -04:00
Miguel Ángel b4787082b2 fix(studio): keep narrow timeline headers readable (#4232)
* fix(studio): hide narrow timeline header labels

* test(studio): cover narrow automation headers

* test(studio): cover narrow automation headers

* test(studio): assert narrow lane tooltip

* style(studio): format narrow header changes

* test(studio): cover narrow speed lane labels

* fix(studio): truncate timeline header names

* test(studio): remove stale header room plumbing
2026-09-20 19:49:29 -04:00
Miguel Ángel e86a530f0b fix(cli): update Puppeteer browser dependencies (#4233)
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.
2026-09-20 18:08:16 -04:00
Miguel Ángel f92d017b99 feat(cli): add timeline editing verbs (#4221)
* feat(cli): add timeline editing verbs

* chore: satisfy fallow complexity gate

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* test(cli): cover snap rounding at known fps

* fix(cli): derive split ids from stable refs

* fix(cli): remove timeline gate blockers

* fix(cli): close freshness and output gate gaps

* fix(cli): close remaining timeline review findings

* refactor(cli): simplify mutation decision flow

* refactor(cli): split trim and apply paths

* refactor(cli): isolate trim validation

* fix(cli): satisfy timeline lint gate

* style: format timeline mutation files

* fix(cli): preserve mutation result types

* fix(cli): close timeline receipt and nested bounds gaps

* fix(studio): rollback writes that fail during write

* fix(cli): bound moved clips by end time
2026-09-20 18:04:13 -04:00
Miguel Ángel 4ff4435e1d feat(studio): export player handle for hosts (#4227) 2026-09-20 17:47:35 -04:00
Miguel Ángel 2fb6f6987e test(studio): close shadow reload regression gaps (#4223)
* test(studio): close shadow reload regression gaps

* chore: refresh follow-up checks

* test: keep follow-up free of unused exports

* test(studio): distinguish root timeline selection
2026-09-20 17:15:29 -04:00
Vance Ingalls f09ea7af3b chore: release v0.8.57 (#4231) v0.8.57 2026-09-20 14:13:17 -07:00
Miguel Ángel e48eac7f5e feat(studio): track preview time to first frame (#4226) 2026-09-20 17:10:41 -04:00
Vance IngallsandClaude Code f476e78cc8 feat(studio): say which project this Studio is serving when a read 404s (#4228)
A tab whose project the running Studio cannot resolve failed every composition
read with a 404 and said nothing. `useSdkSession` recorded the failure to
telemetry and returned, so the editor looked normal while every edit silently
took the server path instead of the SDK — nothing landed, and nothing said
why. The `out-fb1` timeline sat in that state for an hour and a half, through
a reload that passed mount validation.

The banner names both sides: "This Studio is serving X. This tab was opened
for Y." That is the whole truth in the host that produces these 404s. The
CLI-embedded Studio serves exactly one project, and `hyperframes preview`
reuses port 3002 when the folder matches, so starting it on a different
folder takes the port out from under an open tab — and the project the old
tab was pointed at is untouched on disk. "No longer available" would be
false: nothing was lost, and the same timeline shows the project return.

With several projects served (the Vite host) a rename cannot be told from a
deletion, so that wording claims no cause.

`reportReadFailure` answers which project a failed read identifies. Only a 404
does; a 5xx, a dropped request, an unexpected body or an empty file say nothing
about which project the server serves, so none of them claim it.

Also fixes a smaller lie on the same path: `useFileTree` parsed the 404's JSON
body without checking `r.ok`, so the failure took the success branch and
recorded an empty tree — and an empty provenance snapshot for a project that
was never read — which then travelled out in bug reports.

The 404s continue; this ends the silent part, not the failure.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:53:52 -07:00
Vance IngallsandClaude Code 74af8b4279 fix(studio): validate the hash on hashchange, and stop pushing dead ids (#4225)
The mount path has validated its hash since v0.8.52. The hashchange path did
not — it adopted whatever id the URL offered, so changing projects walked
straight around the mount fix and re-armed the same failure: every later
/api/projects/<id>/... request 404s for the life of the tab, including the
composition read that opens the SDK session.

Same tri-state as mount: ok adopts, unknown adopts (a transient check failure
must not rewrite a hash that is probably fine), missing falls back.

Three details that are not obvious from the diff:

- **The race guard reads the hash, not a counter.** This effect re-registers on
  every projectId change, so a `generation` in that closure resets and an
  in-flight verdict from the previous registration would read as current —
  adopting a project the user already navigated away from. The hash cannot go
  stale, so it is the ground truth to compare against after the await.

- **The fallback re-fetches the project list.** tryConnect has `data.projects`
  in scope; this path does not, and caching the mount value would be stale in
  exactly the long-lived-tab case this exists for.

- **Both rewrites use replaceState.** `location.hash =` pushes, leaving the dead
  id one Back away — and Back would then re-validate, fall back, and push again,
  forever. The mount fallback had this bug too; fixed here as well.

Also emits the hashchange verdict so the `unknown` branch — which deliberately
keeps the hash and is invisible from outside — is countable.

Verified: 7 mutants, 7 caught, including adopting without validating, collapsing
unknown into missing, trusting the verdict over the live hash, memoizing the
fallback list, and pushing on either rewrite. Full studio suite 5,323 passed.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:53:31 -07:00
Vance IngallsandClaude Code 7abbe49638 feat(studio): a per-page tab id, and the hash-validation verdict (#4224)
Two telemetry gaps, both pure instrumentation with no behaviour change.

`tab_id` — distinct_id identifies a browser, not a page. Several
investigations stalled on "two tabs, or one tab switching?", because a burst
of events alternating between two project hashes is indistinguishable from a
user moving between projects, and `session_start` fires once per page load so
counting it cannot tell them apart either. One id per page load, from one
shared module, attached by BOTH transports (`studio:*` and `studio_*`) — those
two families already drifted once on how `agent_runtime` is encoded.

Deliberately not persisted. Storage would survive a reload and quietly turn
this into a tab-session id, changing what every query means, and blocked or
cleared storage would make the property intermittently absent — which reads as
"old client" rather than "storage unavailable".

`project_hash_validated` — a tab that keeps its hash because the check itself
failed ("unknown") is currently indistinguishable in production from one that
never ran the check. Different causes, different fixes, one silent bucket.
Emits `stage` (mount | hashchange), `outcome` (ok | missing | unknown), and
the HTTP status when the server answered.

Verified: 8 mutants, 8 caught — including dropping tab_id from either
transport, regenerating it per read, persisting it to storage, emptying it in
the no-DOM fallback, and collapsing "unknown" into "missing". Full studio
suite 5,314 passed.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-20 13:30:35 -07:00
Miguel Ángel ab6cbb383e refactor(studio): compose timeline parts through TimelineProvider (#4222)
* refactor(studio): add timeline provider boundary

* fix(studio): preserve timeline context schema

* refactor(studio): own timeline composition context

* fix(studio): complete timeline context ownership

* refactor(studio): keep timeline helper exports in Timeline

* refactor(studio): extract timeline provider state builders

* refactor(studio): isolate timeline provider branches

* refactor(studio): extract timeline overlay state

* refactor(studio): extract timeline overlay state hook

* fix(studio): keep overlay internals out of context
2026-09-20 15:38:27 -04:00
Miguel Ángel 243ecd1d10 feat(studio): compose preview overlay guides (#4220)
* feat(studio): compose preview overlay guides

* fix(studio): sync late preview iframe mounts

* fix(studio): make preview iframe ownership explicit
2026-09-20 14:37:50 -04:00
Miguel Ángel 85a1a8f789 fix(studio): swap the preview via a shadow iframe instead of blanking on reload (#4207)
* fix(studio): reloading the preview swaps in a ready copy instead of blanking

* fix(player): report painted once assets are ready and the loader has faded out

assetsready fires when the loader fade starts, so a consumer promoting on it can
expose a raised or fading panel. painted fires once per document load after the
fade completes, with a getter for late listeners; it resets on each load.

* fix(studio): promote the shadow preview only when painted, under one wait budget

Studio waited for ready plus two frames, which can land while the player's loader
panel is still up or fading. It now also waits for the player's painted event.
The shadow no longer arms the 5s adapter give-up, so a slow but live load fails
only at the single 15s budget. Set-destination availability follows the live
iframe across a swap.
2026-09-20 13:04:23 -04:00
Miguel Ángel 0c158be165 feat(studio): edit a clip's speed ramp with a rate lane and presets (#4153)
* feat(studio): edit a clip's speed ramp with a rate lane and presets

* fix(studio): keep the live preview attribute in sync with what gets persisted

* fix(cli): snapshot picks the source frame through a rate lane and up to 10x

hyperframes snapshot clamped the constant playback rate at 5 and ignored a
rate lane in data-automation, so a sped-up or ramped clip showed the wrong
frame. It now reads the rate through the runtime's own reader and maps time
with the shared source-time function.

* docs(skills): teach speed ramps as a rate lane in one reference page

Adds the speed-ramps reference, corrects the 0.1 to 5 rate range to 0.1 to 10
and the 'speed ramps are not supported' lines in the skills, and regenerates
the skills manifest.

* docs(skills): say what fixes a ramped clip's length and how the render approximates pitch

* fix(studio): hide the speed preset until the clip has a known duration

A preset on a clip with no data-duration was stretched over a made-up one
second. The panel now offers presets only when the duration is known.

* docs(skills): fix the last stale ramp line in the editing recipes and the slice-cap wording
2026-09-20 12:21:47 -04:00
Miguel Ángel fa699b7d9b refactor(studio): move playback adapter resolution into its own module (#4199) 2026-09-20 12:07:53 -04:00
Miguel Ángel e45483aa0a chore: release v0.8.56 (#4218) v0.8.56 2026-09-20 10:57:46 -04:00
Miguel Ángel 41b7202bca refactor(studio): make timeline rows use manifest elements (#4213)
* refactor(studio): make timeline rows use manifest elements

* fix(studio): preserve timeline consumer prop names

* test(studio): update asset row expectation

* fix(studio): clear preflight lint nit

* chore: allow intentional timeline hook removal

* chore: refresh PR capture checks

* test(studio): align visibility gate with store rows

* test(studio): preserve canEdit literal type
2026-09-20 10:18:03 -04:00
Miguel Ángel 84e6531668 feat(studio): a host can make the preview read-only (#4200)
* feat(studio): a host can make the preview read-only

Gates the shared geometry commit funnel (offset/size/rotation), so the
inspector's manual X/Y/W/H/rotation fields refuse the same as canvas
drag/resize/rotate. The flat inspector's fields render disabled with a
host-supplied reason. Fixes a hooks-order crash on deselect found in review.

* refactor(studio): derive the read-only preview flag from context, not a synced store

EditorShell synced its readOnlyPreview prop into a zustand store via a raw
useEffect behind an eslint-disable of no-restricted-syntax. Replace it with
a PreviewReadOnlyProvider context computed fresh from the prop every render:
Tier-1 consumers (genuine EditorShell descendants) read it via useContext;
Tier-2 consumers (useAppHotkeys, useGestureCommit, the
useDomEditSession/useDomEditCommits/useDomGeometryCommits chain), which run
in App.tsx above EditorShell, take it as an explicit parameter threaded from
StudioApp's own prop instead. Adds a host-mount test proving EditorShell
gates its descendants from its own prop with no StudioApp/App.tsx involved.

* fix(studio): keep preview hotkeys context explicit

* fix(studio): close read-only interaction escapes

* fix(studio): preserve timeline ownership in read-only preview

* fix(studio): rollback read-only preview work

* fix(studio): keep hotkey ownership on the active surface

* fix(studio): format preview overlay and strengthen split test

* fix(studio): clear preview gate findings

* refactor(studio): share preview test fixtures
2026-09-20 10:10:57 -04:00
Miguel Ángel 9bd6da848f feat(studio): theme timeline colours with CSS tokens (#4211)
* feat(studio): the timeline's colours read CSS custom properties instead of literals

The timeline's surfaces, hairlines, ruler text and clip colours were hard-coded
hex/rgba, so a host with a light chrome saw a black slab. The 24 values move
into new --timeline-* tokens in theme.css (the same mechanism Button and Menu
already use), holding today's exact values, so a host themes the timeline by
overriding a token instead of the object staying opaque.

* fix(studio): route timeline clip colours through theme tokens

* chore(studio): bank the colour-literal baseline

* fix(studio): preserve timeline clip token defaults

* fix(studio): increase default timeline clip contrast

* fix(studio): keep timeline hover brighter

* test(studio): stabilize property panel flag coverage
2026-09-20 09:38:38 -04:00
Vance Ingalls 96369c8acd chore: release v0.8.55 (#4216) v0.8.55 2026-09-20 00:45:18 -07:00