Commit Graph
235 Commits
Author SHA1 Message Date
Miguel Ángel 5eec513281 fix(studio): the timeline takes m4v video and flac audio (#4649)
* fix(studio): the timeline takes m4v video and flac audio

* fix(studio-server): check and label flac uploads as audio
2026-09-28 08:49:57 +00:00
Miguel Ángel 00ac4ef571 feat(studio-server): list the projects the history cache knows, with their folders (#4639) 2026-09-28 08:20:35 +00:00
Miguel Ángel bee6a8e17d fix(studio-server): remove the history of a project that no longer exists (#4634)
* fix(studio-server): remove the history of a project that no longer exists

* fix(studio-server): keep a project's history while its drive is disconnected

* fix(studio-server): keep an older history whose drive may only be unmounted
2026-09-28 07:17:46 +00:00
Miguel Ángel e7a0b6be2c fix(studio): a resize after a drag writes its keyframe below the timeline it uses (#4638)
A canvas drag stores the element position as a global gsap.set above the
timeline declaration. A resize that followed it inserted its tl.set right
after that line, so the saved script referenced the timeline before it was
declared and every reload and render threw a ReferenceError.

Both GSAP writers now bound the "after the last statement" anchor by the
timeline declaration, whatever the timeline variable is named. A drag or a
resize on its own writes the same bytes as before.
2026-09-28 06:48:38 +00:00
Miguel Ángel 7fedc689b8 fix: run the configured ffmpeg and ffprobe everywhere and say when an upload was not checked (#4611)
* fix(studio-server): ffmpeg and ffprobe run from the configured binary; unchecked uploads say so

* fix(studio-server): the upload result type lists unchecked files

* fix(media-use): spawn the configured ffmpeg and ffprobe, and name a missing configured ffprobe

* style: format the ffprobe path test, doctor spawns and upload check

* fix(studio): show the not-checked upload notice as info, not an error

* fix: a configured ffmpeg or ffprobe path that cannot run fails loudly

* fix: refuse a folder as the ffprobe path, and report broken paths through --json

* test: reuse the file manager test setup and split the upload stream check

* fix(media-use): a configured ffmpeg or ffprobe must run -version, and a relative one runs itself

* fix(media-use): a configured ffprobe path must name ffprobe, not another tool
2026-09-28 06:09:00 +00:00
Miguel Ángel 078c493448 perf: the preview loads and decodes only the images of scenes on screen or due next (#4578)
* perf(core): skip hidden clips' content so the browser stops decoding their images

Hidden timed clips get content-visibility:hidden while their inline visibility is hidden,
from script evaluation on. A clip due within the look-ahead (2 s by default) keeps its
content live, so its images load before it shows.

* perf(studio-server): load preview images lazily so hidden scenes fetch nothing

* fix(core): stop the readiness scan waiting on and decoding later scenes' images

* perf(studio-server): keep first-frame images eager so the preview opens as fast as before

Only images inside a clip with a numeric start after 0 go lazy; a start the route
cannot read stays eager.

* perf(core): keep clips that start at 0 laid out before the first pass so the first frame is on time

* perf(core): open the preview on time by hiding only lazy images before the first pass

GSAP reads the computed style of the elements it animates while timelines build. Inside a
content-skipped clip every read forces a style update, which cost about 65 ms at open on the
bench composition. Before the first pass the rule now takes only lazy images out of layout, so
hidden later scenes still do not load theirs; after it, the skip rule keeps their content skipped.

* fix(core): hold a paused jump on the previous picture until the next scene's images decode

* fix(studio): swap in a reloaded preview only after its restore seek has painted

* fix(core): apply a held paused jump after 1 s when an image never decodes

* fix(core): keep hidden-scene image skipping and held jumps to Studio's preview, never renders

* fix(studio): reapply manual edits once a held paused jump lands

* fix(studio): copy a clip without the preview's lazy-loading marks

* fix(studio-server): mark preview images lazy without re-encoding the document's entities

* fix(core): share one preview-mark constant between the runtime and the preview route

* fix(studio): reapply manual edits a second time only after a seek that was held

* refactor(core): share the hidden-ancestor walk between clip visibility and image holds

* fix(core): import the studio preview mark from its own subpath

* fix(studio-server): find preview images with the linear html scanner

* chore(core): drop two doc comments the scanner's names already carry

* fix(studio-server): serve the preview mark as its own meta, apart from the GSAP fallback script

* fix(studio-server): capture thumbnails and frames from the preview without lazy images or the mark

* fix(core): let the full readiness scan wait on every image except lazy ones off the first frame

* fix(studio-server): keep an authored loading attribute in any letter case on preview images

* fix(core): leave template content out of findStartTags so preview images match the DOM

* fix(studio): send Ask agent the live markup without the preview loading marks

* chore(core): drop the template note from findStartTags, its test carries it

* perf(studio-server): splice lazy attributes into the preview in one pass

* fix(studio): keep authored image loading on copy and Ask agent by marking preview-made lazy images

* test(studio-server): follow the capture preview URL back to its sub-composition for every project id

* refactor: name the upcoming-clip and GSAP fallback marks once

* fix(studio-server): keep capture builds out of the on-disk preview store
2026-09-28 04:03:36 +00:00
Miguel Ángel 2ebfcc04a3 fix(studio): read and write a clip's in-point the way playback does (#4624)
* fix(studio): read and write a clip's in-point the way playback does

* fix(studio): a group head trim keeps the minimum clip length off the grid

* fix(studio): a group head trim stops where a single-clip trim does and never goes below zero

* fix(studio): group members on the grid move together when a trim stops at its limit
2026-09-28 03:08:13 +00:00
Miguel Ángel a845c322b9 test(studio-server): a write landing between Studio's read and its patch survives undo (#4622) 2026-09-28 03:07:33 +00:00
Miguel Ángel 03e3755aaa fix(studio-server): background removal writes only where it checked, even through planted links (#4540)
* fix(studio-server): background removal writes only where it checked, even through planted links

* test(cli): read the cutout through a no-follow handle instead of checking then reading
2026-09-28 02:07:30 +00:00
Miguel Ángel b165b64eda feat(studio-server): a host can make Cmd+Z undo the newest change, whoever made it (#4603)
* feat(studio-server): a host can make Cmd+Z undo the newest change whoever made it

* fix(studio-server): Cmd+Z for everyone leaves an agent turn that is still open alone

* style(studio-server): format the undo scope check

* refactor(studio-server): the undo scope option names itself

* fix(studio-server): an open turn's parts are known by id, and a new change ends redo for everyone

* style(studio-server): format the open turn check

* fix(studio-server): an open turn's part is known before its entry is announced
2026-09-27 16:14:53 +00:00
Miguel Ángel 3db3da7bb2 fix(studio-server): history saved by 0.8.78 survives the update with every entry (#4601)
* fix(studio-server): history saved by 0.8.78 opens with every entry, and an unreadable id is never overwritten

* test(studio-server): a malformed history id is refused and kept, not replaced

* refactor(studio-server): name the 0.8.78 record check instead of commenting it

* docs(studio-server): hosts keep project histories in historyCache, which reopens a replaced folder

* fix(studio-server): an unreadable 0.8.78 record keeps its history, and another path to the folder is not a copy

* style(studio-server): format the 0.8.78 history tests

* fix(studio-server): name the history id error, and a recorded path that cannot be read is not a copy

* style(studio-server): format the 0.8.78 history fixture
2026-09-27 15:38:12 +00:00
Miguel Ángel 59e3850f6a fix(studio-server): a write through a link lands on the file the system resolves (#4589)
* fix(studio-server): a write through a link lands on the file the system resolves

* fix(cli): resolve catalog install paths as the system does, matching the install root

* fix(cli): list installed files relative to the real project folder
2026-09-27 12:30:17 +00:00
Miguel Ángel b3ab28aa44 fix(studio): keep a clip in sync when its head is trimmed at non-1x speed (#4581)
* fix(studio): keep a clip in sync when its head is trimmed at non-1x speed

* fix(studio): keep head-trim sync at the media start, on snaps and through splits

* fix(studio): keep group head trims rigid and read in-points like the runtime
2026-09-27 11:10:44 +00:00
Miguel Ángel e00baefb5c fix(studio): reload the preview only when a file it loaded changes (#4570)
* fix(studio): reload the preview only when a file it loaded changes

* test(studio): read the owner id from the new change-owner shape

* refactor(studio): drop a comment the parser name already says

* fix(studio): keep reloading until the preview is built, and count what its document names

* style(studio): format the reference pattern and name the kept reload

* fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload

* fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it

* test(studio): name the preview ownership test for what it checks

* chore(ci): allow the watchIgnore removals and trim the new comments

* fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out

* perf(studio-server): check a changed folder in one lookup and cover every reference form

* fix(studio-server): record read folders for a project at the filesystem root

* fix(studio-server): reload the preview when the file a symlinked asset points at changes

* fix(studio-server): resolve symlinks on every preview read and key projects by their real folder

* fix(studio-server): record every link a preview read passes through, resolved as the system does
2026-09-27 10:05:53 +00:00
Miguel Ángel 1262485a01 fix(studio-server): a thumbnail survives files vanishing mid-request (#4571)
* fix(studio-server): a thumbnail no longer fails when a file it reads is removed mid-request

* fix(studio-server): a thumbnail survives its cache folder being removed mid-request

* fix(studio-server): a stray file where the manifests folder goes no longer fails a thumbnail

* fix(studio-server): drop a comment the code already says
2026-09-27 09:06:03 +00:00
Miguel Ángel c83e958576 fix(studio): the dev server leaves catalog installs to hyperframes preview (#4574)
* fix(studio): the dev server leaves catalog installs to hyperframes preview

* test(studio): name the dev server's install test for what it checks

* fix(studio): a finished install replaces its 'Adding' toast instead of sitting beside it

* test(studio): pin that the Adding toast goes after the install's own toast
2026-09-27 08:16:11 +00:00
Miguel Ángel 950cccdac0 test: cli and studio-server tests remove the temp folders they create (#4566)
* test: cli and studio-server tests remove the temp folders they create

* test(cli): each temp folder is removed where it is made, even when a test fails

* test(cli): the one-liner helper relies on its project's own cleanup
2026-09-27 04:06:05 +00:00
James Russo 31c00d2f0e fix(studio): avoid preview reloads for temporary and excluded files (#4553)
* fix(studio): avoid preview reloads for temporary and excluded files

* docs: capture public preview reload reproduction

* fix(studio-server): normalize watch paths in linear time

* fix(studio): register dynamically opened preview projects
2026-09-27 02:23:25 +00:00
James Russo 91b21ae06f perf(studio): serve bounded JPEG timeline thumbnails (#4560) 2026-09-27 02:16:18 +00:00
Miguel Ángel 9c56240bf5 fix(studio-server): probe each proxied clip once instead of on every request (#4557)
* fix(studio-server): probe each proxied clip once instead of on every request

* fix(studio-server): retry a failed codec probe and use the server probe cache

* fix(studio-server): probe again when a clip symlink is repointed

* test(studio-server): pin equal mtimes in the symlink retarget test
2026-09-27 02:06:00 +00:00
Miguel Ángel 984ad28c7c fix(core): clips that meet within float rounding count as meeting in edits and playback (#4535)
* fix(core): clips that meet within float rounding count as meeting in edits and playback

* fix(engine): hand a shared clip boundary to the incoming clip in export, as preview does
2026-09-26 22:14:14 +00:00
Miguel Ángel dce0acd6d2 perf(studio): tag each scene's parts in the preview so one scene can be swapped (#4503)
* perf(studio): tag each scene's parts in the preview so one can be swapped

With sceneParts (Studio preview only) the bundler tags each top-level scene's host, styles and
inline scripts with data-hf-scene; nested scenes belong to their top-level scene. The preview
route then adds a manifest: a hash of everything outside the scenes and one hash per scene,
so the preview can tell when a rebuilt document differs only inside some scenes. Renders are
unchanged.

* fix(core): keep scene order in the preview manifest and mark unswappable scenes

A moved or reordered scene now changes the shared hash, a nested scene
keeps its @import first, and a scene whose script leaves work running
is marked so edits to it take the full reload.

* refactor(core): keep the scene swap side-effect check in the bundler

* fix(core): refuse swapping a scene that runs its own script file

A swap never re-runs external scripts, so a scene file keeps driving the
replaced nodes. Library URLs stay swappable.

* fix(core): refuse swaps for async scene setup and keep the shared @import first

Scenes that finish building after an await or a .then, animate elements
directly or touch the page head and body now take the full reload, and
splitting out scene styles no longer leaves the shared import late.

* fix(core): refuse swaps for scene scripts that throw when run again

Custom element and CSS property registration, sockets, draggables and
infinite tweens now refuse too, and ticker or animate inside a name no
longer does.

* fix(core): refuse swapping a scene that runs a module script or import map

* fix(core): run a scene's local script file in source order in the preview

* fix(core): refuse a scene swap only for defineProperty on a global

* refactor(core): cut scene swap comments to what names and types do not say

* fix(core): find the head tag for the scene manifest in linear time

* fix(core): leave a document with an unclosed head tag without a scene manifest

* fix(core): keep preview scene parts in render order and refuse scene-only script URLs

* fix(core): keep render @import precedence in the preview and swap only known libraries

* fix(core): keep preview scene styles where a render merges them and resolve library URLs

* fix(core): scene swap refuses standalone gsap tweens and root scripts that hold scene nodes

* fix(core): refuse to swap exactly the scenes a script outside them selects

* perf(core): the preview scene scan looks up ids and classes once instead of querying per string

* docs(core): trim three scene-parts comments to the facts a reader needs

* fix(core): refuse to swap a scene a script outside it reaches by tag name

* fix(core): keep scenes swappable when a script outside them only creates elements by tag

* test(core): check scene tags on elements, not in the inlined runtime's text

* fix(core): refuse swaps for chained scene timelines and any-case or universal root selectors

* fix(core): refuse swaps for root selectors through tags above the scenes

* fix(core): put the scene manifest inside the head when a head attribute holds a quoted >

* fix(core): place the scene manifest with the shared head scanner instead of a regex
2026-09-26 20:23:13 +00:00
Miguel Ángel 7eb6430e4e fix(studio-server): history refuses once closed and reopens when a new project takes its folder (#4537) 2026-09-26 19:01:05 +00:00
Miguel Ángel 89fe898cd5 fix(core): an added sub-composition keeps its declared variable defaults when bundled (#4539)
* fix(core): an added sub-composition keeps its declared variable defaults when bundled

parseHTMLContent only treated input as a full document when it started with
a doctype or <html>. Files installed by `hyperframes add` start with a marker
comment, so they were wrapped as a fragment in a new <html> and lost their own
<html data-composition-variables>. Leading comments are now skipped first.

* fix(studio-server): an installed block's preview keeps its <html> variables

isFullHtmlDocument moves to core beside parseHTMLContent and both callers use
it, so the standalone preview of a file that `hyperframes add` installed (a
marker comment above the doctype) is treated as the full document it is. The
leading-comment skip also handles an empty `<!-->` comment, and `<html-card>`
style elements no longer count as a document.

* fix(core): a doctype written without a space still reads as a full document
2026-09-26 14:16:42 +00:00
Miguel Ángel 162de1a6ab fix(studio-server): a gesture sent without an idle time holds until another key (#4513) 2026-09-26 10:35:42 +00:00
Miguel Ángel 9faade09e7 fix(studio-server): undo of a studio edit keeps a save that landed just before it (#4524)
* fix(studio-server): undo of a studio edit keeps a save that landed just before it

* fix(studio-server): undo keeps a save across several studio writes and a whole-file follow-up

* fix(studio-server): undo claims survive studio writes that loop back and stale write receipts

* fix(studio-server): a claim forgets only the write receipts it walked through

* chore(studio-server): drop a claim comment the comment ratchet counts against the stacked base

* docs(studio-server): keep the claim walk comment under the line limit

* refactor(studio-server): pull the pending-hash keep set out of the budget fold
2026-09-26 10:00:34 +00:00
Miguel Ángel 7c017a08de feat(studio-server): history can copy the files before or after any change into a folder (#4493)
* feat(studio-server): history can copy the files before or after any change into a folder

ProjectHistory.checkout(entryId, side, emptyDir) writes the project as entry entryId found it (before) or left it (after) into an empty folder, leaving the project and its history untouched, so a host can play a change's before and after side by side. The entry's own files come from its recorded change and the rest from the entries logged before it, so a change stays exact when entries were logged out of write order.

* fix(studio-server): checkout refuses an unreadable folder and every replay names its side

* fix(studio-server): checkout lays out only trees that existed and never writes into the project

An entry's tree now includes later-logged entries whose writes it continues (an outside change that turned a file into a folder before an agent turn edited inside it), each file replayed in the order its versions chain. Checkout refuses a folder inside the project (symlinks included), refuses a tree with a path that is both a file and a folder before writing anything, and empties the folder again if a copy fails. manifestAt is back to main's replay.

* fix(studio-server): history logs every change in the order it was written

A window now settles pending writes, the held claim and the outside group before it opens, and commits a held claim before its own entry when it closes, so the log order is the write order and every replay (checkout, restore, peek, reopen) sees files as they were. Removes the dependency replay added for checkout. Visible change: an outside burst that spans an agent turn is two entries, and a drag held across a turn ends at the turn.

* fix(studio-server): checkout recognises its project folder whatever letter case the path uses

* fix(studio-server): overlapping turns, operations and claims log every pending change in write order

settle() (a window opening, an undo, restore or step) and every claim commit the held claim, the outside group and each open window's pending changes as a part, oldest first; the windows stay open. A held drag ends when another write is pending. One commitPending owner replaces the claimed-paths-only cut.

* feat(studio-server): undo is per author, and a turn part undoes alone keeping later edits

* test(studio-server): pin per-author redo and name a person's open window on Cmd+Z

* refactor(studio-server): split Cmd+Z target lookup and claim steps under the audit's thresholds

* fix(studio-server): a file replaced by a folder logs its removal before the folder's files

* fix(studio-server): undo across a file/folder swap works, and a later swap stays the person's

* fix(studio-server): an undo blocked by a file added since refuses before touching any file

* fix(studio-server): an undo never writes through or replaces a link standing in its way

* fix(studio-server): undo and redo across a letter-case file/folder swap work in every direction

* fix(studio-server): a letter-case file/folder swap after an idle turn stays one checkable entry

* fix(studio-server): decide letter-case folding from the project's own disk

* docs(studio-server): keep three history comments under the line limit

* fix(studio-server): checkout checks and fills one resolved folder, so it never overwrites a nonempty one
2026-09-26 09:11:32 +00:00
Miguel Ángel 0f14691c7b fix(studio-server): studio element edits keep a save that lands while they apply (#4510)
* fix(studio-server): a Studio element edit keeps a change that lands while it is applied

applyFileMutations refuses a file that changed since the caller read it even without a version (FileChangedError). The element-patch routes, including the patch-element route every style, text and attribute save uses, re-read and re-fold their patches when another writer lands mid-edit, so both changes are kept, and answer 409 after three raced tries. Every other route that writes through writeMutationResult answers 409 instead of overwriting. The batch route now writes atomically.

* fix(studio-server): element edits re-check after their backup and insert-composition refuses a changed file

* fix(studio-server): an element edit refolds when another save lands during its backup

* test(studio-server): the insert race test uses a page with a composition to insert into

* fix(studio-server): an edit to a linked file updates the link's target and keeps the link

* fix(studio-server): id stamping never writes through a link, and link writes keep their receipt

* fix(studio-server): receipts match across a linked project folder, before and after a delete

* fix(studio-server): a restore into a folder deleted outside the app keeps its receipt

* docs(studio-server): say the real-path fallback climbs to the nearest existing folder

* fix(studio-server): edits pin their target when checked, so a retargeted link can't redirect the write

* fix(studio-server): split cuts pin their target and duplicates never follow a planted link

* docs(studio-server): keep the real-path helper's comment under the line limit
2026-09-26 08:23:45 +00:00
Miguel Ángel 3746c9821a fix(cli): timeline ids never writes over a save that lands while it stamps (#4494)
* fix(cli): timeline ids never writes over a save that lands while it stamps

runIds now passes each file's version to applyFileMutations, like every other timeline command, so a save that lands while ids are minted makes the command stop and roll back instead of being replaced by the stamped old content.

* fix(cli): timeline ids refuses a conflicting save with the same JSON refusal as other timeline edits

* fix(studio-server): a refused file batch never rolls back over another writer's save

applyFileMutations checks every file's version before its first write, so a batch that is already stale writes nothing, and a rollback restores only files that still hold this batch's write; a file whose own write failed is still restored.
2026-09-25 18:37:09 +00:00
Miguel Ángel b2a449df8e fix(studio-server): stamping ids at open never reverts a write that lands meanwhile (#4491)
* fix(studio-server): stamping ids at open never reverts a write that lands meanwhile

stampFileHfIds replaces the file only if it still holds the bytes it stamped, so a save or agent write landing while ids are minted is kept. walkDir skips a subfolder that is unreadable or removed mid-walk instead of aborting the whole project walk; an unreadable project folder still throws.

* fix(studio-server): a stamp never recreates a deleted file and a walk only skips unreadable folders

* fix(studio-server): a stamp keeps a permission change made while ids are minted
2026-09-25 16:38:34 +00:00
Miguel Ángel 2734ee2c02 feat(cli): hyperframes history lists, undoes and restores a project's recorded changes (#4449)
* feat(cli): hyperframes history lists, undoes and restores a project's recorded changes

* fix(cli): an agent's history turn ends after 10 idle minutes on every path

A turn left open no longer takes the person's later edits: the engine
files a CLI turn's writes by the same idle rule as a preview window.
Studio claims take their paths back from an agent's window; refusals,
--json shapes, --limit and time parsing follow the review.

* refactor(cli): the project-history opener lives in its own module

The history command and other commands open a project's history the same
way: through a running preview, or directly under the engine's lock.

* docs(skills): the history loop lines sit in one marked trial block per skill

* fix(cli): a turn marker without its last write time has ended

Idle-limit tests set file times instead of racing the clock, and the
restore refusal test picks a ref no entry id starts with.

* docs(cli): the history entry shape comment names no other app

* refactor(cli): history reuses the busy error class and its who helper

* fix(cli): undo --who reverts the agent's whole last turn and nothing older

When the open turn recorded nothing, undo fell back to the agent's
earlier turn; when a mid-turn command split the turn, it reverted only
the newest part. The turn now keeps the entries it filed, and undo --who
reverts exactly those or refuses. Engine tests pin the write timing
rules, and a date that rolls over (Feb 31) is refused.

* fix(cli): an undo --who that meets a conflict leaves the whole turn as it was

It redoes the parts it already undid, lists the newer entries as they
are now, and offers --just-this and --back-to-before for the whole turn.
The last turn is recorded atomically before its marker is cleared.

* chore(cli): history keeps each file's comment share at or under main's

* fix(cli): history keeps log order past a turn's idle limit and undoes a turn's cut parts

* fix(cli): history drops unused imports and splits undo and reopen steps
2026-09-25 14:26:09 +00:00
Miguel Ángel f1f370e96c fix(studio-server): inject page tags at the document's own head and body, not inside a script (#4484) 2026-09-25 12:05:06 +00:00
Miguel Ángel 31441701fc fix(studio-server): one process at a time keeps a project's history open (#4448)
* fix(studio-server): one process at a time keeps a project's history open

* fix(studio-server): taking over a dead history owner never removes a live owner's lock

Two processes that both found the owner dead could each take the lock,
the second removing the first's. Eviction now happens under its own lock
with a re-read, a close removes the lock only while it names the closer,
and a lock file holding no pid counts as dead.

* fix(studio-server): a history lock that cannot be read fails the open instead of spinning
2026-09-25 11:52:46 +00:00
Miguel Ángel 828c434f76 refactor(studio): drop recordEdit's kind, which the project history never read (#4474) 2026-09-25 10:48:46 +00:00
Miguel Ángel 11ac9bce69 fix(studio-server): previewing a project never rewrites its files (#4480)
* fix(studio-server): previewing a project never rewrites its files

* fix(studio-server): a GSAP save on a served id writes that id to the file too
2026-09-25 10:04:38 +00:00
Miguel Ángel 36f8bd0e91 feat(studio): undo and redo step through the project history, whoever made the change (#4456)
* feat(studio-server): an undo taken from Studio is not mistaken for an outside edit

* feat(studio): undo and redo step through the project history, whoever made the change

* feat(studio): the dev server keeps each project history, in the same folder as preview

* fix(studio-server): an undo that deletes a file reads as Studio's, and dev histories close

* fix(studio-server): a claim keeps an agent's earlier write the agent's and takes back Studio's own

* chore: allow the removal of Studio's browser edit history files

* test(studio): mock the project history in the runtime smoke

* fix(studio): step past a jammed undo and say why a step fails

* fix(studio-server): an agent turn cut by a claim commits its part before the claim

* fix(studio-server): a claim's cut ends a held drag and takes only the cut files out of a turn

* fix(studio-server): a held drag ends when someone else changed a file it already holds
2026-09-25 08:43:04 +00:00
Miguel Ángel f873734cc4 perf(studio): open a large film ready to play in about 4 s instead of 32 s (#4401)
* perf(studio): keep lint off the preview open path

The bundler linted every build only to print a StaticGuard warning; a preview bundle now
skips it through a staticGuard option, since Studio lints on its own route. That route now
lints in the CLI child-process worker the render path already uses, so seconds of parsing
no longer stall the preview and every other Studio request on the server event loop.

* perf(parsers): parse identical GSAP source once

Studio re-requests the parsed animations of unchanged source on every open, and several lint
rules parse the same script in one run. A small memo keyed by the script text serves repeats;
each caller gets its own copy.

* perf(studio): build each preview document once per content version

The preview route rebuilt the document for every browser without a cached copy. It now serves
one build per ETag from memory, and the CLI keeps the last build on disk salted with its own
file, so a restarted server serves the previous document without stale code.

* perf(studio): build the opening film's preview while the browser starts

The preview server built the document only when the player asked for it, after
Studio had booted, so a first open waited about a second of idle server time.
The CLI now requests the opening project's preview right after listening, and
requests that arrive while a build runs share it instead of starting another.

* perf(studio): request the preview document at page load

Studio asked for the preview only once the player mounted, about 640 ms into a
first open. It now requests it the moment the page loads, and again when the
hash names another project, so the build or cache read overlaps Studio's boot.

* feat(studio): show the cached frame 0 while the live preview boots

Studio covers the live preview with frame 0 as the thumbnail route last rendered it, sized
from the image so the stage does not shift, and removes it when the live frame is ready to
show. The request is cache-only, so a miss never starts a render on the open path; the
missing poster is rendered after the live frame shows, for the next open.

* perf(cli): start Studio before the startup lint finishes

hyperframes preview linted the whole project before it listened, 4 to 6 s on a large film, and
did so in --json mode where the result is discarded. The lint now runs in the CLI worker while
Studio starts and prints its summary when done; --json skips it.

* perf(studio): render thumbnails from one loaded page, after the preview boots

Every thumbnail opened a page and loaded the whole composition to shoot one frame, and the
Compositions card asked for one while the live preview was still booting. The server now keeps
one loaded page per preview document and seeks it per frame, reloading only when the project
changes; card thumbnails wait for the project first ready preview.

* perf(studio): parse GSAP animations after the preview boots

Studio asked the server to parse the composition animations while the preview was opening; on
a fresh server that cold parse ran on the event loop ahead of the preview build. The request
now waits for the live preview to boot, or to fail loading, since only keyframes need it.

* perf(cli): find orphaned browsers with one process-table read

preview ran pgrep three times and then ps once per matching browser, all
synchronously before listening: about half a second on a machine running
other headless Chrome. One ps listing now finds every orphan in ~20 ms.

* perf(studio): keep lint off the boot

Opening a project started two lint processes while Studio booted: the
terminal's startup lint and Studio's background lint, ~4 s of CPU next to the
boot. Studio's background lint now waits until the live preview has booted,
and lint processes run at the lowest CPU priority so they only take idle CPU.

* perf(studio): open the editing session after the preview boots

Opening a project read the composition source and parsed it for the editing
session (linkedom plus hf-id assignment) on the main thread while the live
preview was booting on the same thread: ~300 ms on a large film. The session
now opens once the preview has booted; nothing the first frame shows needs it.

* perf(studio): park the editor overlays until the preview boots

The overlay frame loop polled preview geometry and off-canvas indicators from
the first frame, forcing layout on a preview document that was still loading,
on the thread its boot runs on: ~200 ms before the first frame on a large film.
The loop now skips its subscribers until the preview has booted, then wakes.

* perf(studio): treat the preview as booted once it can play

Work deferred until the boot (editing session, lint, overlays, GSAP parse,
card thumbnails) started at timeline-ready, which fires before the asset gate,
so the editing-session parse ran inside the wait for play. The Player now
latches the boot when the preview can show and play, or fails to load.

* fix(studio): keep a left project's deferred work out of the one now open

Deferred open-time work (source parse, background lint, editing session) waited on one global
boot flag, so leaving a project before its preview booted let the next project's boot run the
old project's work and write its results. The wait is now per project and resolves false once
another project opens. A preview stuck without an error stops deferring that work after 5 s.

* perf(cli): let the play page revalidate instead of refetching

hyperframes play served the composition, its assets, the player and the runtime with no
validator, so every reload refetched everything, the mp4 in full three times. They now carry
an ETag and stay no-cache, so unchanged bytes cost a 304 and an edit is still seen.

* fix(cli): serve an edited text asset even when its size and mtime match

The play page tagged every asset by mtime and size, so a same-size rewrite within one
filesystem tick kept the old tag and the browser kept stale bytes. Text assets now get a
content-hash tag; media keeps the mtime and size tag Studio already uses.

* fix(studio): close idle thumbnail pages so an idle Studio runs no composition

Kept-loaded thumbnail pages stayed open for the life of the server, each running a live composition,
so an idle Studio cost server CPU and memory that main does not. A page now closes after 10 s unused,
and a page is closed only after the frame still running on it finishes.

* fix(studio): stop a first open from reloading the preview once per scene file

Studio stamped hf-ids into a composition the first time that composition was served, so opening a
film of external scenes wrote to index.html and every scene file mid-session. Each write reached
Studio as an outside edit and reloaded the preview, which failed for a large film. The server now
stamps every composition once before it starts watching the project.

* fix(studio): ignore play until the preview can play, so it is never undone

Space (and any play() caller) could start playback before the preview frame finished loading. Its
load step then paused and rewound to the restore point, so the play was silently lost. play() now
waits for timelineReady, the same gate the Play button already uses.

* fix(studio): hold play on a composition switch until the new preview loads

Readiness was reset only when the project changed. Drilling into another composition kept the
old ready flag while the new preview loaded, so Play or Space could start a play that its load
step then undid.

* refactor(studio): split background lint and thumbnail page reuse into single-purpose steps

Keeps the boot wait in the background lint only, moves page lookup and idle close out of
withPage, and drops a suppression the startup worker no longer needs. Behaviour unchanged.

* chore(cli): declare the render-setup worker as a fallow entry

It is spawned by path from cancellableProcess.ts, so it has no import-graph referrer; the entry list
is where the repo records that, replacing the per-file ignore removed in the previous commit.

* fix(cli): close a thumbnail page whose load fails

The page never leaves the promise that loads it, so when the load throws (a WebGPU composition with
no adapter, or the navigation timeout on a big film) nothing could close it.

* fix(cli): match text assets by exact type and keep Range on them

The pattern anchored only its first alternative, and text assets skipped buildRangeResponse, so a
ranged request for a stylesheet or SVG got a full 200.

* test: open the next project as Studio does, and read the stamped file once

The absent-read guard test switched project without beginning its timeline session, so the
per-project boot wait held the read. The stamp test checked then read the same file.

* test(cli): rewrite the scene file without opening it first

Opening the file by path counted as a check before the later read of the same path.

* perf(cli): take a thumbnail's version from the cached project signature

Each thumbnail walked the project and hashed every text file for its page version; the adapter
already caches that signature and clears it from the watcher. An aborted request now also stops
before its screenshot.

* fix(studio): keep Play off until the preview's load step has run

Play could enable before the preview document's load event, from the runtime's timeline message
or from the blank page's load step settling on the preview's first message. The load step then
paused and rewound the preview, so a Play pressed in between did nothing; a film with large
images loads late enough for this to hit a cold open. Readiness now waits for the document's load
step, whichever path asks for it.

* refactor(studio): move the preview poster's state into usePreviewPoster

NLEPreview.tsx passed the 600-line limit once main grew it; the poster's state and callbacks belong
beside PreviewPoster. No behaviour change.

* fix(cli): take an earlier thumbnail on a fresh page

A composition need not draw the same frame when seeked back, so a kept page that had shown 20 s
drew a different 10 s thumbnail than a fresh load. A kept page now only seeks forward, as a render
does; an earlier time loads a fresh page.

* perf(cli): close a kept thumbnail page a second after its last frame

Held for 10 s, the page went on running its composition after the open's thumbnail
burst, about 5 % of a core in the thumbnail browser while Studio sat idle. A burst's
requests arrive back to back, so it still loads the page once.

* fix(studio): count the preview as booted at its first frame

The boot waited for every video and audio in the preview to finish buffering, which on a
project with media took until the 5 s fallback. The thumbnails, editing session and lint
that wait for the boot then started seconds late and kept an idle Studio busy. They now
start once the first frame is up; media finishes buffering alongside.

* perf(studio): render one thumbnail per composition for its card and clip

A composition's sidebar card asked for its frame at 3.00 s and its timeline clip for the
clip's midpoint, so opening a project rendered every scene twice. The clip now asks for the
card's frame (the midpoint still, for compositions under 3 s), and the thumbnail cache
serves both from one render: 20 scene renders drop to 12 on a 12-scene project.

* chore: keep each changed file's comment share at or under main's

The comment ratchet counts the comment lines this branch added. Each is cut, or kept as one
line where it states an invariant (the GSAP parse cache returns copies; the boot starts waiting
work at the first frame). The orphan-cleanup docblock is condensed and now names the process-table
read it uses instead of the pgrep scan it replaced. No code line changed.

* fix(studio): keep a 3 s clip off its end frame and give card and clip one encoded URL

A composition exactly 3 s long asked for its thumbnail at 3.00 s, its end instant, which after a
fade-out is an empty frame; compositions of 3 s or less now use their midpoint. The card built its
thumbnail URL from the raw path while the clip encoded it, so names with & + [ or % asked for two
different URLs, and a % in the name made the card's request fail on the server; both now encode.
The readiness comment says the boot latches at the first frame, as it has since the boot moved.

* fix(studio): encode the card's live preview URL too, and document the bundler's static guard

The card's hover preview built its URL from the raw composition path, so a % in the file name made
the preview route fail to decode it. The card now builds its composition URL in one place, encoded,
for both its thumbnail and its hover preview. The static-guard bundle option keeps a one-line doc
(its default and what it warns about); a neighbouring docblock is rewrapped to keep the comment
share.

* test(cli): mock studio-server once so the signature spy and the history hook both apply

The main merge brought a second vi.mock of @hyperframes/studio-server into studioServer.test.ts;
the later one replaced the first, so createProjectSignature was no longer a spy and the cached
signature test read .mock of undefined. One mock now carries both overrides.
2026-09-25 04:56:55 +00:00
Miguel Ángel 177464b4c1 feat(cli): preview keeps project history, and Studio can claim its own edits (#4437)
* feat(studio-server): preview keeps a project history and names the next undo and redo

The history engine now reaches `hyperframes preview`, and its routes tell
Studio what Cmd+Z and Cmd+Shift+Z would revert next.

- The CLI preview server gives the shared API a project history, opened
  on first use under ~/.cache/hyperframes/history (overridable with
  historyRoot), and its file watcher feeds every change it sees to it.
  Shutdown closes it, so open windows are committed, within the same
  bounded wait as renders, so a history still opening cannot hold exit.
- GET /projects/:id/history also returns `back` and `forward`: the entry
  each step would revert, with its label, for the Undo and Redo buttons.
- POST /projects/:id/history/window takes an optional idleMs, so a drag's
  burst of writes can share one window that ends by itself (capped at
  10 minutes, below where a timer overflows).

Nothing in Studio calls these yet.

* feat(studio-server): a writer that records after writing can claim its writes

Studio records an edit after writing it, and many of its writes are
server mutations deep in its save paths, so opening a window before each
write would restructure them. claim() fits that order instead.

- ProjectHistory.claim(who, label, paths, { coalesceKey?, idleMs? })
  sweeps now and moves the not yet committed outside changes to `paths`
  into one entry of who's, under the edit's label. Other outside writes
  stay outside.
- Claims with one coalesceKey merge into one entry (a drag's burst) until
  a claim with another key, idleMs (default quietMs) without one, or any
  operation; Cmd+Z right after a drag undoes all of it.
- The ceiling is named where it lives: another writer's write to a
  claimed file between Studio's write and its claim folds into Studio's
  entry, and a write filed to another writer's open window stays there.
- Route: POST /projects/:id/history/claim { label, paths, coalesceKey?,
  idleMs? } -> { claimed: { id } | null }.

* fix(studio-server): a drag back to its start claims nothing, and next steps name their files
2026-09-24 20:55:58 -04:00
Miguel Ángel 56c48a79be feat(studio-server): every write to a project is kept in a history that can be undone or restored (#4431)
* feat(studio-server): every write to a project is kept in a history that can be undone or restored

* fix(studio-server): rewrite a history removed while open and report damaged log lines

* fix(studio-server): history ignores foreign ids, ends idle windows and budgets only past history
2026-09-24 16:19:00 -04:00
Miguel Ángel 6a5fcd4cf2 fix(studio-server): preview keeps its base URL when a script mentions a <base> tag (#4426)
* fix(studio-server): preview keeps its base URL when a script mentions a <base> tag

* test(studio-server): pin the sub-composition preview's base check

* fix(studio-server): find a <base> with the HTML parser, not a regex
2026-09-24 13:40:52 -04:00
Miguel Ángel fed45bac7c fix: serve projects and assets whose names contain @ % # & or ? (#4415)
* fix(studio-server): serve projects whose names contain @ % # & or ?

Five routes cut the decoded project id out of Hono's c.req.path, which leaves %40 %25 %23 %26 %3F
encoded, so the cut missed and files, preview assets, sub-compositions, thumbnails and waveforms
404ed or 403ed for those names. One helper now takes the sub-path by segment from the raw URL.

* fix(studio-server): thumbnails and waveforms stay inside the project for any name

The thumbnail route built its preview URL from the raw project id and path, so # ? and % broke it,
and neither route checked that the decoded path stays inside the project. Thumbnails now use the
composition guard and an encoded URL; waveforms use the read-only asset guard.

* fix(cli): play and present serve assets whose names contain @ % # & or ?

Both /composition/* routes cut a prefix out of Hono's c.req.path with no decode. They now share
studio-server's requestSubPath (renamed from projectSubPath and generalised to any route) with the
five studio-server routes, so one helper owns how a request path becomes a file path.

* fix(studio-server): a thumbnail request for the project folder answers 404

An empty thumbnail path resolved to the project directory and crashed reading it (500).

* fix(studio-server): read the thumbnail's composition with one open

Checking the path with stat and then reading it let the file change in between (CodeQL
js/file-system-race). One open now serves the type check, the mtime and the read; a missing file
still thumbnails from the preview as before, anything that is not a file answers 404.
2026-09-24 10:38:56 -04:00
Vance IngallsandClaude Sonnet 5 add6783775 fix(studio-server): stop mislabeling two more read-route failure shapes (#4363)
* fix(studio-server): stop mislabeling two more read-route failure shapes

Follow-up to the stale-tree investigation this week — two more cases
where the read route answers with the wrong shape for what happened.

A path a listing (`walkDir`) can show but this route cannot serve, now
correctly reported:

- **Replaced by a directory.** `existsSync` passes, `readFileSync` throws
  `EISDIR`, and Hono answers plain-text "Internal Server Error" — not
  JSON, and not a reason. `statSync(...).isFile()` after the exists check
  turns this into `404 { why: "not_a_file" }`, same shape as any other
  missing-file 404 the client already handles.

- **A dangling symlink.** `isSafePath`'s `realpathSync` throws on a link
  whose target does not exist, and (by design — see its comment) that
  fails closed as unsafe, same as a real traversal attempt. Correct for
  containment, wrong for the response: a broken link scans as an attack
  when it is broken plumbing. Added `isDanglingSymlink`, checked only
  after `resolveWithinProject` has already refused the path, so nothing
  about what is permitted changes — only which `why` a refusal carries.
  A symlink resolving to something real outside the project is still
  `outside_project` 403, unchanged; pinned by test so this can't
  regress into loosening containment.

Tests: dir-in-place → `not_a_file`; dangling symlink → `dangling_symlink`;
symlink-to-outside-project still → `outside_project` 403 (the guard the
new branch must not weaken). 102/102 in the touched suite, 247 green
across studio-server routes overall. oxlint, oxfmt, tsc clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(studio-server): stop dangling_symlink from leaking outside-project existence

Review on #4363: `isDanglingSymlink` ran on the *lexical* path even when
`resolveWithinProject` had already failed — so a path reached via `../` that
happened to be a dangling symlink out there, or a leaf reached through an
escaping directory symlink, came back `404 dangling_symlink` instead of `403
outside_project`. Worse: an in-project symlink pointing outside the project
got `404` if the outside target was missing and `403` if it existed — a
small oracle for "does this path outside the sandbox exist."

`isDanglingSymlinkInProject` now requires containment on both ends: the
link's own directory must resolve inside the project (`isSafePath(dir,
dirname(lexicalPath))`), and its target, read via `readlinkSync` and
resolved against the link's directory, must also resolve inside the project.
Only then does "target exists nowhere" get the new label — anything that
touches outside the project keeps the plain `outside_project` 403, with no
distinction based on what's actually out there.

4 new regression tests: a dangling symlink reached by `..` (403, not 404),
a dangling leaf behind an escaping directory symlink (403, not 404), and a
same-status check across a missing vs. existing outside target (both
`outside_project`, not one of each). files.pathSafety.test.ts 25/25,
files.test.ts 80/80, oxlint/oxfmt/tsc clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(studio-server): close a TOCTOU CodeQL flagged on the not_a_file check

CodeQL (js/file-system-race, high severity) on #4363: the new `not_a_file`
check stat'd `res.absPath` by path, then a separate `readFileSync` reopened
the same path — a directory-for-file swap between those two calls could
land a stale answer. Opens the file once instead and checks/reads through
that single descriptor (`fstatSync(fd)` then `readFileSync(fd)`), so the
check and the read are provably on the same inode; a swap after open can't
affect either. `openSync` on a directory still succeeds (POSIX allows
O_RDONLY there), so the `not_a_file` 404 still fires the same way; opening
a genuinely missing path still throws and hits the existing "optional" vs.
plain 404 branch unchanged.

105/105 in files.pathSafety.test.ts + files.test.ts, oxlint/oxfmt/tsc clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 20:53:10 -07:00
Miguel Ángel 86fa10ced4 perf(studio): show a film's first frame sooner by loading preview assets by URL (#4357)
* fix(studio): serve preview assets by URL instead of inlining them as base64

The Studio preview route serves project files under a base href, but both preview adapters
called the bundler with its self-contained defaults, so every image and font was inlined
once per reference. One exported option set now owns how a preview bundle is built.

* test(studio): cover poster, srcset and sub-composition preview asset URLs

Also types the Vite adapter bundler with BundleOptions, so the shared preview options are
checked against the real option set.

* fix(studio): tag a preview asset for revalidation only once it has settled

Preview assets are now served by URL, so the asset route serves every image and video. It tagged
them by mtime and size; two same-size writes inside one filesystem tick share an mtime, and copy
tools (cp -p, rsync) set mtime back, so a browser could keep stale bytes after an edit. The route now
tags a file by inode, change time and size, and only once its last change is three seconds old; a
younger file is served without a tag and refetched.
2026-09-23 21:08:56 -04:00
Miguel Ángel 8cb7d06db6 perf(studio): re-render only the edited scene's thumbnails after an edit (#4315)
* perf(studio): refresh only the edited scene's thumbnails and let the preview load first

One scene edit regenerated every sidebar and timeline thumbnail, and those renders starved the new preview's requests on the Studio server. The thumbnail cache now keys on what a composition renders from, the server names the compositions a write affects, only those thumbnails refresh, and composition thumbnails wait while a shadow preview reload is in flight.

* test(studio): pin scene-scoped thumbnail reuse and the preview-reload hold

Memoizes the scoped thumbnail signature on the project signature so requests keep reusing the adapter's cached one, keeps the fresh-signature check that catches a write the watcher has not reported yet, and adds tests for sibling-scene reuse, root-head invalidation, the affected-composition set, per-path revisions and the scheduler hold.

* refactor(studio): share the file-change payload guard

* fix(studio): match thumbnail revisions for scenes mounted with a ./ path

* perf(studio): keep signature cache keys constant-size

* style(studio): format thumbnail-scope changes

* docs(studio): trim thumbnail input comments
2026-09-23 10:29:01 -04:00
Vance IngallsandClaude Opus 5 6e5f460a0c fix(studio): split the empty-read reason and stop blaming the composition (#4325)
Two residuals from the cutover-dashboard work, both found in the data the
first pass shipped (0.8.61/0.8.62).

`absent_or_empty` was one label over two different failures: the route
answers `content: ""` both from the `optional=1` shim (nothing resolved at
that path) and for a real 0-byte file. The route now marks the shim with
`missing: true`, and the read path with `missing: false` so the field's
PRESENCE is what tells the client this server draws the distinction — a new
server's empty file would otherwise look exactly like either case from an
old one. Client splits them into `absent` and `empty_file`; a server without
the field still reports `absent_or_empty`, so the existing series stays
honest instead of silently folding into one of the new ones.

Why it is worth splitting: on 0.8.62, 75 tabs hit this class and not one of
them ever landed an SDK edit afterwards — it is terminal for the tab, while
the tab keeps playing back and navigating, so nothing surfaces to the user.
Every measured case with a loaded tree carries `path_in_tree: true`: a file
the tree lists and this read cannot get. Which of the two causes that is
decides the fix, and until now the event could not say.

`res.json()` at the end of the read was unguarded, so a 200 carrying HTML —
an SPA fallback or a proxy answering in the route's place — rejected out of
the function entirely and the effect's outer catch filed it as
`stage: "open"`, which asserts the COMPOSITION failed to parse. Observed in
production on 0.8.62: `Unexpected token '<', "<!-- /*!"... is not valid
JSON`. That is the third label to make the same wrong claim, after the
fetch-rejection split in #4240 and #4269. Now `reason: "invalid_json"` with
the response's content-type.

`path_in_tree` now rides all three empty-read reasons, not just the combined
one, so the split keeps the signal that justified it.

Tests: 4 new (absent / empty_file / combined-label fallback / non-JSON 200)
plus a server test pinning `missing` on both answers. 1172 tests green
across studio hooks + studio-server routes; oxlint, oxfmt, tsc clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 03:44:04 -07:00
Miguel Ángel 9f19d9d325 fix(studio-server): write served files atomically (#4285)
* fix(studio-server): write project files atomically

* fix(studio-server): isolate atomic write temporary files

* test(studio-server): document Windows atomic replacement

* ci: allow change detection checkout to finish

* test(studio-server): assert atomic stamp cleanup without ESM spy
2026-09-22 08:55:45 -04:00
Vance IngallsandClaude Sonnet 5 24c3e197ef fix(studio): cutover gate classification, SSE cross-project bleed, benign races (#4269)
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>
2026-09-22 01:40:47 -07: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 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 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