Commit Graph
5348 Commits
Author SHA1 Message Date
Miguel Ángel 8dae5ef89b fix(core): make grading writes previewable and honor computed properties (#5157)
* fix(core): write only authored hue curves, so a single hue curve passes lint

* fix(cli): apply over a grading saved with empty hue curves instead of refusing it

* fix(core): read grading properties from computed style, and stop advising an inline start

* fix(cli): preserve validation during legacy hue repair

* fix(cli): preview grading writes without apply
2026-10-08 15:21:47 +00:00
Monster-GM 202d5b5300 fix(engine): tag GPU H.265 streams as hvc1 (#5029) 2026-10-08 15:02:33 +00:00
Miguel Ángel 188475aaf2 fix(cli): group sampled check findings by element and rule (#5174)
* fix(cli): group sampled check findings by element and rule

* fix(cli): preserve unique elements when grouping audit samples

* fix(cli): retain geometry sample times until report grouping

* fix(cli): keep grouped finding evidence time paired with geometry
2026-10-08 10:38:45 +00:00
Miguel Ángel de4b31a262 fix(studio): update unreferenced generated media names on replace (#5169)
* fix(core): derive generated media labels from current source

* fix(core): decode generated media filenames

* fix(core): share filename decoding across URL forms

* fix(core): strip filename suffixes without unsafe indexing

* fix(studio): rename unreferenced generated media ids on replace

* fix(core): register media ID exports and trim names in linear time
2026-10-08 10:38:12 +00:00
Miguel Ángel cf1b04bb71 feat(cli): accept host-managed HeyGen access tokens (#5177)
* fix(auth): accept host-managed HeyGen access tokens

* fix(auth): share environment header validation

* docs(auth): condense credential resolver guidance

* fix(auth): report the account a browser login just signed in

Identify the account from the tokens the login issued, not a fresh credential
lookup that an environment token or API key outranks.

* docs(auth): list the host access token in the CLI credential order

* fix(cli): name a rejected host access token on publish

Publish told a host-token user to log in again, which cannot help while the token outranks the login.

* docs(auth): show the media credential order main resolves

* fix(cli): warn at logout that a host access token still signs commands

One map of environment credential variables, owned by the resolver, now feeds the logout warning and both publish messages.

* docs(auth): scope the media credential order to the audio scripts

* refactor(cli): read every env credential name from the resolver's map
2026-10-08 10:33:57 +00:00
Miguel Ángel 042ec2ebd0 fix(studio): restore overscan and publish the zoom viewport (#5151)
* fix(studio): restore the timeline overscan budget

* test(studio): align window witnesses with the restored budget

* fix(studio): publish the viewport when a zoom anchor lands

* fix(studio): publish the viewport when a zoom-out ends at its laid-out scale

An eased zoom-out that ends at the scale already laid out only moves the scroll position,
so the timeline drew from the old position until the next scroll event. The zoom input
now publishes that scroll through the same callback the zoom anchor uses.

* fix(studio): a pan at the laid-out scale publishes its scroll before paint

The same-scale ending of a zoom ran outside React, so its scroll publish rendered on the
next task, after the browser painted the old window. It now flushes like the scale change
beside it, and its test pans at the laid-out scale instead of changing it.

* test(studio): a same-scale pan must render its scroll before paint

Reads the render window outside act, so unwrapping the flush fails it, and records each
published scroll at call time instead of reading the live element afterwards.
2026-10-08 08:37:55 +00:00
Xuanru LiandClaude Opus 5.5 4a335aa6a1 chore: release v0.8.141 (#5193)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v0.8.141
2026-10-08 05:56:58 +00:00
Jerrai 0c76e52446 feat(fonts): record resolver stage diagnostics on fail-closed font errors (#5189)
* feat(core): per-compile font fetch diagnostics on the fail-closed Unresolved error

Attach a stage record to the per-compile fail-closed Unresolved FontFetchError
so callers can log why a family did not resolve. The record is an ordered list
of families, each with required/resolved flags and its fetch attempts (path,
authored/default URL, text param, CSS cache fresh/hit, real CSS HTTP status,
block and regex-match counts, asset totals and non-ok HTTP statuses). It holds
enums, counts and HTTP statuses only: no family names, URLs, CSS or page text.
The error class, name, code, message, familyName and unresolvedFamilies are
unchanged, and shared rejected errors are never mutated.

- Jerrai

* test(fonts): diagnostics for the authored-stylesheet fallback; fix the cssStatus comment

Add generic tests (invented family names) for an authored stylesheet that is rejected or
yields zero faces and falls back to the default request, an authored success with no fallback
attempt, an unchanged transient UNAVAILABLE, and a failed case that records both attempts with
the error contract unchanged. cssStatus doc now reads: no HTTP response was recorded for that
attempt.

- Jerrai

* test(fonts): pin cache labels of a repeated fallback compile and the Unresolved error's url and cause

A second compile of the same failing authored-plus-default page must label both attempts as
cache hits while the first compile's are fresh. The Unresolved error keeps url "" and no
cause, as at main.

- Jerrai

* fix(fonts): label a CSS lookup's cache hit before awaiting it

An optional family can join an in-flight CSS lookup that then rejects as UNAVAILABLE. The
rejection is swallowed, but the attempt kept its default cssCache "fresh" because the label was
written only after the await. The label is now recorded on the caller's own attempt before the
await. The shared result and error objects are not touched; retry, fallback and selection are
unchanged.

- Jerrai

* test(fonts): make the concurrent-join synchronization explicit

The second compile now passes its own AbortSignal; a lookup that awaits a shared promise
registers a listener on it, so the test waits (bounded) until the joiner has reached the optional
family's lookup and asserts the optional URL still has exactly one request before releasing the
deferred 503. The first compile's optional attempt must be fresh and the joiner's a hit, in that
order.

- Jerrai
2026-10-08 02:50:31 +00:00
Miguel Ángel b66e8a195e perf(studio): preview decodes hard-to-play videos at the size they are shown (#5184)
* perf(studio): preview decodes hard-to-play videos at the size they are shown

Studio's copy of an HEVC or ProRes clip was made at the source's full size, so a 4K clip was decoded at 4K inside a preview a few hundred pixels wide, and back-to-back 4K clips flashed the background at their cuts. The player now reports how large it shows the composition, the runtime asks for a copy sized to the shown video (on a ladder of sizes, never above the source), and Studio stops pre-warming source-size copies. CLI play, static serving, publish and renders keep the source-size copy.

* fix(studio): sized preview copies follow zoom, hover previews and the newest size

Studio reports the preview's on-screen scale when a zoom settles and for composition hover previews, which no player measures. The runtime loads only the newest size asked for, regrows a first copy made while the frame grew, and never replaces a copy with one smaller on either side. The player's scale is measured against the frame's own layout width.

* fix(studio): keep comment shares and file sizes within the repo ratchets

* test(studio-server): end the sizing test's ffprobe options before its input

* fix(studio): hover previews report their size once their runtime is ready
2026-10-08 00:26:08 +00:00
Xuanru LiandClaude Opus 5.5 09f359c33e fix(core): embed the weight-list font when the page's Google link gives none (#5186)
* fix(core): embed the weight-list font when the page's Google link gives none

Since #4569 the renderer fetches the Google Fonts URL the page wrote. When
that link is malformed (e.g. Bricolage Grotesque with wght@100..900, past the
family's 200..800), Google answers 400, the family stays unresolved, and a
distributed render fails closed. Before #4569 the renderer's own weight-list
request resolved it.

If the page's links yield no usable faces, fall back to that weight-list
request and warn with each link's status. Fail-closed transient errors still
throw before the fallback, and a working link is used unchanged. Preview and
render share this resolver, so both embed the same faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(core): cover a multi-family Google link that silently drops a family

Production pages link Bricolage Grotesque with opsz@7..96 alongside a second
family; Google answers 200 and returns only the second family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(core): log the font-link fallback as fields, without the link URL

An authored link can carry page text in `text=`, so the warning now records
only the family, each link's outcome (http_<status> or no_faces) and how many
faces the weight-list fallback embedded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(core): import the font resolver at module scope in the Google subset tests

Also drops the two comments the fallback added, so the file's comment share does not rise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 00:05:31 +00:00
Wenbo ZhuandClaude Opus 5.5 21d14b25b3 feat(media-use): reach HeyGen through a host app's gateway (HEYGEN_API_BASE) (#5181)
* feat(media-use): reach HeyGen through a host app's gateway (HEYGEN_API_BASE)

- The audio engine sends every HeyGen call to $HEYGEN_API_BASE when set, as the heygen CLI does; plain HTTP needs
  $HEYGEN_ALLOW_HTTP=1, the CLI's own rule. A host gateway (that base with its own key) wins over a host OAuth token.
- The skill says: inside a host that gives HeyGen access, no CLI install or sign-in, prefer the host's own HeyGen
  tools, relay a refused call's message as written and do not switch providers on your own.

HyperFrames Desktop sets these variables when a HeyGen API key is saved in its Settings, so media-use's TTS, catalog
search and avatar calls are paid by that key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(media-use): format the edited tables and regenerate the skills manifest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(media-use): host access applies only when HEYGEN_API_BASE is set; HeyGen credentials stay on HeyGen hosts

- The "use the host's own tools first / no CLI sign-in" guidance is now conditional on HEYGEN_API_BASE alone, so an
  agent outside such a host (Claude Code with a HeyGen MCP connector, say) keeps today's media-use journey. The
  compressed copies in README, CLAUDE.md, the prompting overview, general-video and the capability menu are back to
  main's wording.
- A base outside heygen.com gets no stored or host OAuth credential, only a key set for it (review on #5181).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(media-use): a project's .env cannot set the HeyGen base or allow HTTP

loadEnvFromDir copied HEYGEN_API_BASE from a project file into process.env, and the host gateway path then sent the
person's shell HEYGEN_API_KEY to that host. A host app sets HEYGEN_API_BASE and HEYGEN_ALLOW_HTTP in the environment
it spawns, never in a project file, so the .env loader skips both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:30:56 +00:00
Miguel Ángel d2a3c076d0 fix(player): load compositions from document duration (#5176)
* docs(player): map composition probe readiness contract

* fix(player): resolve preview readiness from document duration

* fix(player): preserve timeline grace and terminal probe failure

* fix(player): scope probe rejection to the failed document

* test(player): share terminal readiness fixtures
2026-10-07 18:08:20 +00:00
Miguel Ángel 5c7f6316d3 chore: release v0.8.140 (#5175) v0.8.140 2026-10-07 10:04:56 +00:00
Miguel Ángel 1e711b087d feat(studio): a host can show the hover box and give the stage its starting size (#5116)
* test(studio): pin a host's hover box and a starting stage size

* feat(studio): a host can show the hover box and give the stage its starting size

* fix(studio): keep the comment share flat and let a measured size win over the hint
2026-10-07 09:13:07 +00:00
Miguel Ángel 17c699db4a fix(studio): replacing an image's fill on an img changes its picture (#5170)
* fix(studio): replace image fill through its source attribute

* fix(studio): preserve literal percent image sources
2026-10-07 09:11:58 +00:00
Miguel Ángel 9701f0da61 fix(core): keep rebound root timelines clock-driven (#5165)
* fix(core): keep rebound root timelines clock-driven

* test(core): verify transport advances the rebound root
2026-10-07 08:44:52 +00:00
Miguel Ángel da6259896b fix(core): settle slow documents against their own frame cadence (#5168)
* docs(core): map document-relative readiness contract

* fix(core): measure idle against document frame gaps

* fix(core): preserve fast-page readiness while calibrating slow frames

* docs(core): clarify readiness coverage and measurement provenance
2026-10-07 08:40:57 +00:00
Miguel Ángel 87387268a7 fix(core): preserve fresh host-driven timeline updates (#5159)
* perf(core): render a playing frame once when the host also ticks it

* fix(core): reset host tick dedup on playback start

* fix(core): deduplicate host seeks by rendered timeline time

* test(core): advance host tick clocks deterministically
2026-10-07 08:01:27 +00:00
Miguel Ángel e21ab020da fix(cli): preserve transcribed words before speech onset (#5164) 2026-10-07 07:46:58 +00:00
Miguel Ángel 9f09861d2e perf(studio): zooming a long timeline re-decodes and redraws less (#5142)
* perf(studio): reuse filmstrip frames when a zoom changes a strip's width

A zoom that changed a strip's frame count decoded the whole strip again
on the main thread, and showed the clip's single poster frame until it
landed. Frames are now sampled at each slice's left edge, so a strip
twice as long holds every frame of the shorter one; decoded frames are
shared per source and time, so zooming out decodes nothing and zooming in
decodes only the new half. The strip on screen stays until the new width
is ready.

* perf(studio): redraw only the waveforms on or near the screen after a zoom

A zoom resizes every mounted clip, and every mounted waveform redrew in
that same frame, on screen or not. A waveform now draws only while it is
on or near the screen (one shared observer, a screen of margin each side)
and draws when it comes near if a zoom changed it meanwhile.

* perf(studio): never start a beat-analysis stage while the timeline zooms

Analyzing a long music track runs for over a second on the main thread,
and when it started late it landed inside a zoom. The analysis now awaits
an optional pause before each long stage; Studio's waits for the timeline
to rest and the browser to go idle. Results are unchanged. The tempo
detector's stage is one call, so a zoom that starts inside it still waits
for it.

* fix(studio): reuse a filmstrip frame only where its own strip would show it

A shared frame was keyed by the time a strip asked for, not the time it
was decoded at, so a coarse strip's keyframe-early frame could land in a
finer strip and run its frames backwards. A frame now carries its decoded
time and is reused only when the strip would have accepted that keyframe
itself. The frame-count ladder now tops out at a power of two (32), so
its top step shares frames with the one below instead of none.

* feat(studio): show the clip's last frame at the end of every filmstrip

Left-edge sampling never showed a clip's final slice, so an end card or a
fade-out was missing from its strip. Each strip now also decodes the
clip's last frame (at its own time, never an earlier keyframe), shared
across zoom steps like the others, and the last tile shows it.

The strip on screen is now held per clip and range, not per props: a
clip crossing between on and off screen keeps it, and a clip narrowed to
one frame lets it go, so widening again never re-decodes a dropped width.

* docs(studio): keep the filmstrip files at or under main's comment share

The comment ratchet allows no file a higher share of comment lines than on
main; the added explanations move to the PR description.

* fix(studio): keep a held filmstrip leased so a full cache cannot evict it mid-zoom

The hold leased the strip on screen only once a new width was requested,
so for one commit it had no lease; with the cache over budget it was
evicted, the poster showed, and the old width decoded again. The strip on
screen is now leased for as long as it is the clip's, and shown only while
held. A frame that fails to decode takes the frame before it, so every
slot keeps its time and the last tile still shows the end.

* refactor(studio): one owner for which clip a held filmstrip belongs to

The hold checked the clip twice, in its lease and again before showing
it; the lease alone decides now, and the cross-clip test pins it. A test
now also pins that a filled frame is not charged to the cache.

* fix(core): hold the tempo detector until idle after a slow import

* fix(studio): scope shared video frames to timeline sessions

* chore(core): keep beat analysis comments within the ratchet
2026-10-07 07:38:13 +00:00
Miguel Ángel dbb8222c8d fix(studio): catch up the preview after missed file changes (#5126)
* fix(studio): recover previews after missed file changes

* fix(studio): keep queued reloads with the current owner

* fix(studio): keep a queued outside change when switching compositions

* test(cli): the project watcher closes its parent folder's watch too

* fix(studio): keep the project-folder change out of the shared signature check

* fix(studio): keep the edited file's draft and reload every session after a reconnect

A reconnect or replaced folder is a project-wide change: every open SDK session reloads, a failed save is recorded against the file the draft belongs to, and two queued changes to different files merge into one project-wide reload.

* fix(studio): refresh the preview cache when the .hyperframes folder is replaced

* fix(cli): report the files of a folder moved into the project

Replacing a populated folder such as .hyperframes/ now reaches the signature cache through its files, so the shared signature rule stays as on main and Studio's own writes inside .hyperframes/ still invalidate nothing.

* refactor(studio): name the merged file-change payload and share the test sender

* fix(studio): keep a failed draft's recovery copy until that file itself saves

* style(studio): rewrap the merged payload line after the rename

* fix(studio): deliver a held change's full reload when its block clears

A clean save, Use external and Keep Studio now all deliver the merged scope a blocked change owes, and a moved-in .hyperframes folder reports its manifests so a dropped one still clears the cached signature.

* fix(studio): keep restored drafts out of the reload scope and clear the lint gates

* fix(studio): retain held reload scope before React renders
2026-10-07 07:32:16 +00:00
Miguel Ángel 4fcad1ed74 fix(media-use): use a host app's music tools; fail loudly without the heygen CLI (#5154)
* fix(media-use): use a host app's music tools; fail loudly without the heygen CLI

* fix(media-use): name the heygen CLI only for music and sound effects; keep the sfx test offline

* fix(skills): workflow skills use a host app's own music tools first

* fix(skills): say how a host app's music track reaches the narrated workflows

* fix(skills): set a host app's music track after the last audio pass

* fix(skills): a host-music film with no narration still fetches its sound effects

* fix(media-use): keep the stub-failure test off the machine's heygen CLI; sync the skills manifest

* fix(skills): audio passes keep a host app's music and sounds; silent needs no sound cues

* fix(skills): host audio keeps string frames, stops on a broken audio_meta.json, survives a re-run

* fix(skills): report a host audio entry with no path and a broken sidecar instead of crashing

* fix(skills): keep host audio written while an audio pass runs

* test(cli): serve the telemetry capture while the resolve child waits on it

* test(cli): fail the telemetry capture when the resolve child exits non-zero

* style(skills): wrap the host audio comment to the 120-column limit
2026-10-07 07:16:51 +00:00
Miguel Ángel 80c2547eac feat(cli): send missing features and host-app friction as ratingless feedback (#5140)
* docs(skills): ask agents inside a host app to report its friction as feedback

* feat(cli): send missing features and host-app friction as ratingless feedback

* fix(cli): keep the rating error, refuse blank reports and document ratingless feedback

* docs(skills): leave names and figures out of missing-feature reports

* docs(skills): keep anything private out of missing-feature reports
2026-10-07 06:40:37 +00:00
Miguel Ángel 9e0d05d450 fix(engine): auto worker count stays within the heap budget (#5149)
* fix(engine): automatic worker count never exceeds what the V8 heap can feed

A 24 GB Mac exporting a 43 s portrait project with 22 videos picked 5 capture workers against a 4192 MB heap that feeds about 4, and the render died at 62% with JavaScript heap out of memory. The heap budget was only advisory; it now caps the automatic count (bound 'heap'). An explicit --workers N is untouched.

* test(engine): heap-cap test restores its mocks even when an assertion fails

Review findings on #5149: the os and v8 mocks were restored only if every assertion passed, and two comments still called the heap cap advisory.

* docs(engine): heap budget comments say it caps auto sizing
2026-10-07 06:11:30 +00:00
Miguel Ángel 9fbec22596 fix(cli): load telemetry without the core and engine barrels, and stop the history test racing (#5153)
* fix(cli): load telemetry without the core and engine barrels, and stop the history test racing

* fix(cli): load history from its leaf subpath, and fail the history test fast on a crashed CLI
2026-10-07 06:05:41 +00:00
Miguel Ángel 3b89bfe8a7 fix(engine): motion blur works over video by holding each video frame across samples (#5150)
* fix(engine): motion blur works over video by holding each video frame across samples

Each output frame's injected video frame is now held for every sub-frame
sample and probe of that frame, so footage stays as shot and only the
layers over it blur. The refusal for sessions with a before-capture hook
is removed.

Fixes #5144

* fix(engine): trim held-video comments, pin the new test-classification hash

* fix(engine): a blurred frame at a sub-composition cut keeps its video

Each sample now decides which videos show at its own time, like every other
layer, and only a video on screen at the output frame's time keeps that
frame's picture. Before, the visible set was taken at the frame's time while
sub-composition hosts were hidden at the sample's time, so the samples just
before a cut showed no video and the cut frame dipped toward black. The
before-capture hook takes the held time as an optional third argument, and
the early injection is gone: the GPU re-render now always runs at the page's
own time.

* style(engine): one doc comment for the before-capture hook

* fix(engine): a video animating its own style stays in step when blurred

The injector copies a video's own style only when its frame changes, so the
first injection of each blurred frame now runs at the frame's own time
before the samples. A video sliding by its own transform no longer sits a
quarter frame behind with blur on.

* test(producer): a two-line header for the motion blur over video test

* test(producer): reach the cut fixture by path, not a URL fallow reads as an import
2026-10-07 05:46:10 +00:00
Miguel Ángel 2d98d5c15f fix(check): stop false sweep_static and text-overlap alarms on compositions that are fine (#5147)
* fix(check): see color, filter and media time under seek, and name data-no-timeline for stills

* fix(check): warn instead of fail when only audio advances, and judge text overlap on glyphs

* fix(check): judge glyph overlap by the glyph box, and see capitalized words and one-side borders

* fix(check): capitalize past leading punctuation, test outline color, trim a comment

* fix(check): capitalize words the way Chromium does, so digit-led words stay lowercase
2026-10-07 05:38:45 +00:00
James RussoandClaude Opus 5.5 f74f260286 feat(core): bound imported scene timing to source intervals (#5132)
* feat(core): support deterministic async frame sources

* feat(core): bridge preserved film HTML to native capture

* feat(core): bound imported scene timing to source intervals

* test(producer): verify film bridge pixels in real browser

* fix(runtime): retain queued draws and disable unsafe dedup

* fix(runtime): fail promptly on runner reloads and send errors

* fix(runtime): align film draws with export windows

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(core): require data-no-timeline on imported film hosts

Imported film wrappers have no GSAP timeline. Without data-no-timeline
on the scene hosts and root, lint fails with missing_timeline_registry
and each render waits 45 s for timeline registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

* test: set explicit film fixture canvas background

* test: diagnose film capture timeout phase

* test: foreground film pages before screenshot capture

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:25:09 +00:00
Miguel Ángel cffc3aa4e1 feat(render): report encoding and assembling progress (#5137)
* feat(render): report encoding and assembling progress

* fix(render): close every progress stage and fail a render whose progress callback throws

* fix(render): send a repeated progress update once

* fix(render): keep Docker terminal output as before and drop the unneeded callback ending

* fix(render): keep one failure stage code per stage whatever its frame counts
2026-10-07 04:29:35 +00:00
James RussoandClaude Opus 5.5 891a18af76 feat(core): bridge preserved film HTML to native capture (#5131)
* feat(core): support deterministic async frame sources

* feat(core): bridge preserved film HTML to native capture

* fix(runtime): retain queued draws and disable unsafe dedup

* fix(runtime): fail promptly on runner reloads and send errors

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(core): require data-no-timeline on imported film hosts

Imported film wrappers have no GSAP timeline. Without data-no-timeline
on the scene hosts and root, lint fails with missing_timeline_registry
and each render waits 45 s for timeline registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:55:42 +00:00
Miguel Ángel f6b3821626 fix(catalog): run block scripts inline in the docs player, and pages for six shader blocks (#5139) 2026-10-07 03:11:49 +00:00
Miguel Ángel 7cf042d74f feat(registry): six WebGPU shader backgrounds from the Shaders library (#5138) 2026-10-07 03:00:53 +00:00
James RussoandClaude Opus 5.5 a17d0bfc00 feat(core): support deterministic async frame sources (#5130)
* feat(core): support deterministic async frame sources

* fix(runtime): retain queued draws and disable unsafe dedup

* docs(core): mark frame-source hosts with data-no-timeline

A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(runtime): avoid transport redraws after render seeks

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 01:59:55 +00:00
Miguel Ángel 0ca99b30da fix(ci): skills sync publishes hyperframes-creative past its hidden 1.0.13 and checks it went live (#5081)
* fix(ci): skills sync publishes past a version the registry holds unpublished

* fix(ci): skills sync fails unless a republished skill is the registry's latest

* fix(ci): skills sync republishes one patch past a hidden version and checks it as owner

* test(ci): skills sync fails the job on an error and stops after a clean sync

Two mutants survived review: dropping the line that sets the exit code (the job would print its ::error:: lines and still pass) and dropping the clean-sync return. One test runs the script with a failing fake clawhub on PATH and expects exit 1; another pins that a clean sync makes no further call.

* test(ci): the exit-code test can only reach its fake clawhub

The child's PATH was the fake's folder plus the inherited PATH, so on a machine where the fake cannot execute, a real logged-in clawhub further down would run sync for real. PATH is now the fake's folder alone.

* fix(ci): skills sync skips only hyperframes-creative's hidden 1.0.13

Republishing one patch past any taken version would publish past every future hidden version with no one looking. Only the acknowledged case (hyperframes-creative, 1.0.13 taken) is published at 1.0.14; any other taken version fails the sync as before.

* test(ci): the skip allowlist matches the skill, not just the version

A taken 1.0.13 on any other skill must fail the sync too; without this case, accepting 1.0.13 for every skill passed all tests.

* refactor(ci): skills sync picks the republish version in its own helper

The allowlist check took republishTaken to cyclomatic 5, which the Fallow audit scores at its 30.0 CRAP threshold; deciding the version in versionPastSkippable keeps each function under it. Behaviour unchanged.
2026-10-07 01:46:15 +00:00
Miguel Ángel 4cf5cf9b53 feat(engine): render WebGPU compositions on SwiftShader behind an opt-in (#5133)
* feat(engine): render WebGPU compositions on SwiftShader behind an opt-in

PRODUCER_ALLOW_SOFTWARE_WEBGPU=true lets a host without a GPU render a
data-requires-webgpu composition instead of refusing it. SwiftShader can
draw canvas WebGPU only through its Vulkan backend with GPU compositing
on, so an opted-in software launch adds the Vulkan feature and
--use-vulkan=swiftshader, keeps compositing on, and the adapter check
accepts the fallback adapter. Off by default; every other launch is
unchanged.

* test(engine): compare the opted-out launch without a cast

* test(engine): pin which adapter a capture session accepts for WebGPU

* test(engine): assert the adapter check's arguments with the call matcher

* docs(engine): fit the software WebGPU note in three lines
2026-10-07 01:25:10 +00:00
Miguel Ángel 04736006b1 test(producer): unit tests never reach the network (#5135)
* test(producer): unit tests never reach the network

* test(producer): the unit lane refuses sockets to other hosts, not only fetch

* test(producer): refuse outside hosts at the lookup, not by parsing socket calls

* test(producer): keep dns.lookup promisify-compatible under the no-network guard
2026-10-07 00:52:01 +00:00
Miguel Ángel eee0c5101c perf(studio): smooth timeline zoom on long projects (#5109)
* perf(studio): coalesce timeline zoom to one write per frame and zoom to a range

* perf(studio): coalesce timeline zoom to one write per frame and zoom to a range

Every zoom input goes through one owner that writes the store once per frame.

zoomTimelineToRange eases to a time range; the zoom buttons use it.

* feat(studio): zoom-to-range reports done or cancelled and is not a person zoom

* perf(studio): place clips in percent of a one-second layer so zoom restyles nothing

A zoom step rewrites one width per row; clip thumbnails start decoding at rest.

* perf(studio): a zoom step re-renders only clips whose drawing it changes

Clip handlers read the lane callbacks when pressed, so a skipped clip never acts on a stale zoom.

* refactor(studio): move at-rest waveform and thumbnail work to its own change

* perf(studio): draw waveforms and decode thumbnails when the timeline rests

Waveforms draw only the visible span of a clip.

A zoom or scroll step stretches what is drawn until the timeline rests.

* test(studio): fade handles measure the clip only while they show

* test(studio): waveforms draw their visible span and redraw at rest

* feat(studio): export currentTimelineRange, the inverse of zoomTimelineToRange

* perf(studio): zoom gestures scale the drawn timeline and lay it out once at rest

No layout or render per gesture frame; past 0.67x or 2x the preview is laid out and continues.

* perf(studio): rebase a zoom preview only past 0.5x or 4x

Clips stay mounted half a viewport each side, so a zoom-out preview to 0.5x shows no gaps.

* fix(studio): keep the zoom readout, edges and Fit right during a preview

The slider and its percent follow a zoom while it is previewed. A zoom in
from below Fit no longer jumps when it lands: the preview now uses the
width the timeline will really have. A zoom-out lays out as soon as it
would show time with no clips mounted, so edges never show empty strips.
Fit, a press or a scroll on the timeline end a pending zoom first. An
older abort no longer cancels a newer zoom-to-range, and an instant one
lands on the next frame instead of inside its caller. Range selection and
gap tints scale with the preview.

* fix(studio): keep a pending zoom when the timeline re-registers its view

The timeline registers its scroll view again whenever clips are added or
removed, which used to drop a zoom still waiting to be laid out. Now only
a real unmount drops it, and unmounting one timeline leaves a newer
timeline's registration alone.

* refactor(studio): share the zoom geometry helpers the timeline already has

Zoom scales come from getTimelinePixelsPerSecond, the range margins from one
helper, and a zoom step starts from the same playhead anchor every zoom uses.
The clip minimum width is named and shared with the drag ghost.

* fix(studio): keep a zoom preview through scrolls and past the content end

A scroll during a zoom preview, including the one a laid-out zoom makes
itself, used to lay the next preview out at once, so a pinch or eased zoom
fell back to a full layout every frame. A scroll now only rescales what it
mounted. Zooming out also lays out before the preview shows past where the
ruler and beat lines are drawn, not only past the mounted clips.

* fix(studio): keep a moved audio clip's waveform drawn where it shows

A waveform now draws the stretch of its clip that the shared strip tracker
reports near the screen, the tracker the filmstrip already uses. It follows
scrolls, resizes and moves (a drag or ripple that shifts the clip), so a
moved clip no longer shows a blank stretch, and a zoom redraws it once.

* perf(studio): lay a zoom-out step out once, at its start

An eased zoom-out (a Zoom out click, or zooming out to a range) used to
lay out partway, once the shrinking preview reached time with nothing
mounted, and again at the end. It now lays the target out first and eases
by scaling that layout down from the old view, so each click lays out
once. Each eased frame also draws its preview in that frame.

* fix(studio): keep previewing a zoom at the content's end and over new rows

The preview now lays out only once it would show past the window the
timeline draws clips, ruler ticks and beat lines in, with two pixels of
slack, so rounding at the content's end no longer lays out every frame.
Rows mounted into the timeline during a preview are scaled before they
paint.

* fix(studio): redraw a waveform when its clip's width changes or it moves

A waveform drawn whole kept its old bars, stretched, after a zoom changed
its clip's width; it now redraws on any resize. A long clip also marks
its undrawn ends for the strip tracker, so a move without a scroll (a
ripple, a drop, an undo) re-measures it instead of leaving a blank
stretch.

* perf(studio): skip the layout when a zoom preview ends on its own scale

An eased zoom-out lays its target out first, so its preview ends on the
scale already laid out. Landing it now only sets the scroll, instead of
writing the same zoom again and counting the person's zoom twice.

* fix(studio): measure filmstrips and waveforms after a zoom preview ends

The strip tracker measured clip boxes while a zoom preview scaled them,
so after a Zoom out click a long clip's filmstrip or waveform could stay
blank across part of the screen. Reads during a preview now wait, and the
tracker re-measures every strip once the preview ends. A zoom-out lays its
target out in its first frame rather than inside the caller, and only when
the new view holds the old one.

* perf(studio): watch a waveform's undrawn ends only when it has some

A clip drawn whole has no undrawn ends, so it no longer mounts the two
empty markers the strip tracker watches; with hundreds of short audio
clips that saved an observer entry each.

* fix(studio): measure strips each time a zoom preview is laid out

A zoom-out lays its target out partway into the ease, after which the
strip tracker kept the stretch it measured before the zoom, so a long
filmstrip or waveform went partly blank near the end of the ease. Every
layout of a preview now re-measures the strips.

* docs(studio): keep the zoom-to-range doc comment within four lines

* docs(studio): keep the zoom files' comment share at or under main's

The comment ratchet failed on ten files this branch touches. Comments are
cut or folded into one line; no code changes.
2026-10-07 00:46:17 +00:00
Miguel Ángel 1825c37268 test(studio): launch real-Chrome tests like the e2e launcher, bounded by the test budget (#5134)
* test(studio): launch real-Chrome tests like the e2e launcher, bounded by the test budget

* test(studio): share the real-Chrome page setup between the two runtime tests

* test(studio): keep the real-Chrome test helper with the other test helpers
2026-10-07 00:25:18 +00:00
Miguel Ángel 11e684d305 fix: the preview preloads the coming scene's images, and check stops failing on image swaps (#5128)
* fix(core): the preview loads the next scene's images before it starts

* fix(cli): check stops reporting image loads that its own scrubbing cancelled

* fix: preload only a coming clip's own images, and judge aborted images by whether they still decode

* fix(cli): count an aborted image as broken only when its decode fails

* style(cli): keep the aborted-image comment within the line limit

* test(cli): pin that a reload still pending at the cap is not reported

* test(cli): run the cap test on whatever timers are pending, and always restore real timers
2026-10-07 00:10:29 +00:00
Miguel Ángel e6daed3df0 chore: release v0.8.139 (#5136)
* chore: release v0.8.139

* chore: sharpen the v0.8.139 release summary
v0.8.139
2026-10-07 00:06:22 +00:00
Miguel Ángel e8cfd6b73e perf(studio): parse saved HTML once when syncing timeline length (#5092)
* perf(studio): parse the saved file once to sync the composition length after a timeline edit

* perf(studio): keep empty duration updates free of parsing

* test(studio): pin one parse for rootless moves and the delete length readout

* test(studio): check the delete's length readout before its save lands
2026-10-06 23:16:21 +00:00
Miguel Ángel f7bba2288d fix(cli): read the WAV in JS so Built-in transcription runs inside Electron (#5129)
* fix(cli): read the WAV in JS so Built-in transcription runs inside Electron

* fix(cli): read an empty WAV as silence
2026-10-06 23:15:50 +00:00
Miguel Ángel d94708e531 fix(core): keep nested scenes in place during a drag when the root has no timeline (#5115)
* fix(core): keep nested scenes in place during a drag when the root has no timeline

* fix(core): reuse the missing-root composite so a late root timeline still binds

* fix(core): count a nested scene in its composite length so a rebind keeps it at the playhead

* fix(core): rebuild a held composite whose length went stale so the player length stays right

* test(core): reuse the no-root-timeline loader for the stale-length case
2026-10-06 21:58:19 +00:00
Miguel Ángel b50dead1da feat(registry): gooey split block with metaball goo and directional motion blur (#5123)
* feat(registry): gooey split block with metaball goo and directional motion blur

A dot splits into a three-dot menu through a gooey neck, spins into a
swirl mark under shutter motion blur, breaks back into three dots and
merges into one. Timing, spacing and eases are measured off a reference
recording frame by frame.

The motion-blur snippet gains an opt-in `sharp: 0` option that hides the
crisp frame-time copy while an element moves, leaving only the shutter
average. The default is unchanged; the inlined copies carry the same text.

* feat(registry): grow gooey-split blades out of swelling dots

* refactor(registry): tidy gooey-split pose code and document sharp 0 limits

* fix(registry): hide a sharp 0 motion-blur target by marker, not its visibility

Hiding through the element's own visibility fought timelines that
write it (autoAlpha): a target faded in after attach stayed hidden at
rest. A marker attribute and one !important rule now do the hiding, the
default path writes nothing, and a declarative sharp other than 0 or 1
is refused like any other bad option.

* fix(registry): smear a motion-blur target that was hidden when blur attached

Copies froze the target's computed visibility at attach, so a target
entering with autoAlpha never smeared (and vanished entirely under
sharp 0). The group now carries the target's current visibility each
frame, read with the sharp 0 marker lifted, and a copy keeps a
descendant's visibility only where it differs from its parent's.

* test(registry): pin a copy keeping a descendant's own visibility

* fix(registry): keep a child's own hiding in the smear of a target hidden at attach

* refactor(registry): restore a lifted visibility in one call and lift any hidden form

* fix(registry): restore a lifted visibility even when copying styles throws

* fix(registry): let motion-blur copies hide with a clip that has ended

The smear group set an explicit visibility on every moving frame, and
copies used to freeze an explicit visible, so an ancestor clip hiding
after the last update could not hide them: copies from a 0-4 s
sub-composition stayed on screen at 16.4 s. The group now sets a
visibility only where the element's differs from its parent's.
Also regenerates skills-manifest.json for the motion-blur skill doc.
2026-10-06 21:24:38 +00:00
Miguel Ángel 156e87766c feat(cli): stream heard words on the transcribe --json progress stream (#5127)
* feat(cli): stream heard words on the transcribe --json progress stream

* test(cli): share the streaming transcribe helper

* fix(cli): keep install and download stoppable and whisper errors short while streaming
2026-10-06 20:44:45 +00:00
Miguel Ángel 755ab7d165 fix(core): renders keep a fromTo's from-only values from the frame it starts on (#5125)
* fix(core): renders keep a fromTo's from-only values from the frame it starts on

* refactor(core): prime a tween at its start in one helper

* fix(core): a keyframed fromTo also keeps its from-only values at its start

* fix(core): each staggered target keeps its from-only values from the frame it starts on
2026-10-06 20:22:57 +00:00
Miguel Ángel dc244d765c fix(studio): drags after a resize, undo or reload land where they were pressed (#5088)
* test(studio): bench moves pressed right after Add keyframe

* fix(studio): an undo that reloads the preview keeps the selection, so a drag after it moves

* fix(studio): a drag started while a resize saves keeps its animations paused

* fix(parsers): an edit pins no position on a keyframed tween it did not touch

* fix(studio): a move pressed while the preview changes lands on the new preview, at the time pressed

* test(studio): bench waiting presses, a held box resized twice, and reloads at the last playhead

* refactor(studio): tidy the waiting press, bound its wait, and freeze edit stamps after early exits

* test(studio): load the property panel once so its first test stops timing out

* fix(studio): presses queued on a held box keep their release, hover and newest outline

* test(studio): judge bench undo by the history groups Studio claims, not by file writes

* refactor(studio): one waiting-box lookup in the bench and one raf-pause write per release

Behaviour-identical tidy of the waiting-press work: the bench reads the waiting
box through one helper, releaseActiveGesture sets the raf pause once, clearPointerState
writes the false it always computed, and previewGestureStarts calls the opts setters
directly instead of through aliases named like the gesture hook's gated setters.

* fix(studio): a queued press lands on the element it pressed and survives a window switch

* fix(studio): undo cancels a waiting press, a stuck save stops holding presses, scene swaps hold them

* refactor(studio): keep the complexity waiver on clearPointerState itself

* fix(studio): undo of a queued press leaves nothing waiting, and hosts keep their own undo

* fix(studio): a waiting press keeps the box still after a blur, and undo reports what it took back

* style(studio): bring the PR's comments within the comment limits

* fix(studio): bank the four bench cases this fixes and meet the comment, complexity and duplication gates

* test(studio): pin that a waiting press is never a pending edit, and name the new duplication waivers
2026-10-06 20:22:37 +00:00
Miguel Ángel 34552a21d2 fix(cli): let parakeet-mlx detect the language and name the Parakeet install under auto (#5124)
* fix(cli): let parakeet-mlx detect the language and name the Parakeet install under auto

* docs(cli): say what --language does under each engine

* docs(cli): whisper transcribes as the --language, it does not filter

* chore(cli): drop a comment the install-hint condition already says
2026-10-06 19:04:28 +00:00
Miguel Ángel b9e511a768 fix(studio): a timeline jump no longer starts thumbnail loads it is about to cancel (#5085)
* fix(studio): a timeline jump no longer starts thumbnail loads it is about to cancel

* fix(studio): pump inline after the release batch, and pin the synchronous slot free
2026-10-06 18:04:50 +00:00
Miguel Ángel 0ca28db4f8 chore: release v0.8.138 (#5121) v0.8.138 2026-10-06 17:53:18 +00:00