mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
30ed3cd1242d0fd18c8d204bd8826671b1315884
223
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84898fbe85 |
fix(ci): skill docs may name the project files a workflow writes (#4902)
* fix(ci): skill docs may name the project files a workflow writes * style(ci): wrap the project-file citation test to the format width |
||
|
|
29ded9b0fa |
chore(deps): update github-actions (#3934)
* chore(deps): update github-actions * ci: re-pin producer test job hash after the checkout action bump --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
74e6df4370 |
fix(release): use portable Git paths in release guards (#3766)
* fix(release): use portable Git paths in release guards * fix(release): drop the comment on a path git never compares --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
d4756f597c |
fix(studio): edits on GSAP-animated layers keep their keyframes and file positions (#4869)
* fix(studio): resizing an element GSAP only positions saves its CSS width and height * fix(studio): a resize never saves GSAP's scale mask as the element's CSS * fix(studio): keep the resize size writer inside the file and comment budgets * fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders * fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it * test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview * fix(studio): a move keeps an authored xPercent centring set in the saved file * fix(studio): a drag on a centred element no longer flashes off-centre on release * fix(studio): a font pick saves the edit and its @font-face in one write * fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit * test(scripts): pin that a slash after i-- before a quoted slash is division * fix(studio): an inspector edit saved while a preview reload loads stays on screen * fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it * fix(studio-server): a font name or path with braces in quotes still saves its @font-face * fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule * test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass |
||
|
|
37f30b1d06 |
feat: keep a video's sound on the clip by default (1/8) (#4813)
* feat(cli): one owner for init media placeholders, audio stays on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(cli): init keeps a video's sound on the clip; silent files scaffold muted Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com> Co-Authored-By: kapelame <kapelame@users.noreply.github.com> * feat(registry): example templates keep the a-roll's sound on the video Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): video audio rules recommend keeping sound on the video Update video_missing_muted and video_audio_double_source message and fixHint text to recommend the new default: keep audio on the video with data-has-audio="true" (not muted). Tests verify the new wording is in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): a video is either audible (data-has-audio) or muted, never both Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): follow the compiler's audio rule in the audio-track toggle Confirm before background removal drops a video's sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): hyperframes-core teaches sound on the video by default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): workflows and scaffold guidance keep a video's sound on the clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: footage with sound keeps it on the video clip Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): init from-file a-roll is timed, muted strip is attribute-exact The from-file a-roll video gets data-start="0" so a scaffold lints clean, and the orphaned audio-only slot comment is dropped. setVideoAudio now walks the tag attribute by attribute, so a class value containing the word muted survives. The ffprobe-missing warning says the video is assumed to have sound and how to switch to muted, and DEFAULT_META notes the failure surfaces at render until lint support lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep has-audio in step with the muted toggle, test cutout confirm Muting a video now clears has-audio and unmuting sets it to true, in both the Design and flat media panels, matching the Has audio track toggle. Audio elements are unchanged. Adds coverage for both panels plus the cutout window.confirm guard: declined leaves src alone, accepted swaps src and mutes, a silent video never prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false An audio element or an undefined hasAudio no longer picks up a muted attribute; only an explicit false on a video writes muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: video_missing_muted rule text, silent-footage notes, consistent audible examples lint.mdx and cli.mdx describe the current video_missing_muted rule instead of the retired unmuted-video wording. The editing recipes say how to adapt for silent footage and require the outgoing clip's sound to end at a J-cut. The Remotion media example shows the audible mapping consistently, data-attributes states that has-audio is required unless muted, and the hairline frame note no longer claims the video is muted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): encode init media filenames and probe --audio duration Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded. --audio now passes the probed track length as the composition duration in both init paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(lint): audio-state-aware media rules and time-aware double-source check video_missing_muted honors any explicit data-has-audio. video_audio_double_source only considers audible videos and only fires when the same-src clips overlap in time. duplicate_audio_track now covers audible videos. audio_file_without_element no longer claims silence when an audible video carries the sound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(skills): audio-stream check for remotion ports, followable J-cut recipe Remotion Video maps to data-has-audio only when the file has an audio stream. The J/L-cut recipe now shows both shots with muted pictures and separate audio. embedded-captions removes the audio.mp3 scratch file it extracts. skills-manifest.json regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(lint): trim comments to satisfy the comment ratchet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): atomic muted edits, cutout keeps sound on a linked audio Muted and Has audio track commit one batched write (one persist, one undo entry) and remove data-has-audio instead of writing an empty marker. Unmuting a video probes its src: audible -> true, silent -> false, probe failed -> absent. Remove background on an audible video inserts a linked sibling <audio> with the original sound in the same write, replacing the drop-the-sound confirm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(producer): skip hidden videos when auto-adding video audio A video hidden in Studio is dropped by the mixer, so its src never reached existingAudioSrcs and the render re-added it at full volume. The video parser now records the shared hidden predicate and the auto-detect step skips hidden videos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): keep useDomEditSession under the 600-line cap Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com> Co-authored-by: kapelame <kapelame@users.noreply.github.com> |
||
|
|
d218cec091 |
ci(studio): gates read the base the run measured, and drag frames on a quiet runner (#4872)
* ci(studio): measure drag smoothness on a quiet runner, with a blank-page control * ci(studio): gates read the base the run measured, not the base branch tip * style(ci): format the base-file test |
||
|
|
fd105807c4 |
fix(scripts): count comments after regex literals in the comment ratchet (#4879)
* fix(scripts): count comments after regex literals in the comment ratchet * fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet |
||
|
|
e666a8a2a3 |
feat(studio): the code editor follows studio's light and dark theme (#4862)
* feat(studio): the code editor follows studio's light and dark theme Replaces One Dark with a Studio editor theme whose colours are theme.css variables: eight syntax role tokens (One Light and One Dark hues held at 4.5:1), editor surfaces from Studio's roles, all measured by the contrast ratchet. * test(studio): the code editor theme takes every colour from a theme.css token * fix(studio): code stays at 4.5:1 when a selection or search match sits on the active line Syntax tokens are tuned for the selection over the active line, which the ratchet now measures; matches are outlined rather than stacking another wash; the search panel's pressed button and special characters follow the theme; unused tooltip rules go. * fix(studio): code matches carry no background, and types read apart from constants Search and selection matches are outlined only, so CodeMirror's lime match wash and stacked washes are gone; type and constant tokens split by hue in both themes. * fix(studio): the current search match reads apart by width, not colour alone A 2px accent outline on the current match and a muted 1px outline on other copies of the selected word. |
||
|
|
658f22bb7f |
feat(studio): graphite colour system with a light theme that passes contrast (#4836)
* feat(studio): graphite colour system with a light theme that passes contrast Studio's tokens take the Graphite values: a lifted dark, a light set behind data-theme="paper", the player dark in both. Every text role clears 4.5:1 and every icon and ring 3:1 on the fills it sits on, held by the contrast ratchet. * fix(studio): every studio surface follows the light theme and passes contrast The dock no longer inherits dockview's forced dark scheme, popovers sit on the raised surface, stock white, red and amber text map to roles, the keyframe diamonds and track headers use roles, and the contrast ratchet measures every pair in both themes (150 pairs). * fix(studio): stock white, red and amber stay colours; studio text and fills use roles Round-two review: white, red and amber are colour content as well as text, so they keep Tailwind's values and hosts keep their palettes. Text, toasts, the feedback card, the FX popover, destructive buttons, category labels and the dock take Graphite roles instead; the contrast gate rejects an unparseable colour. * fix(studio): every light-theme ink, icon, dot and focus ring holds its contrast Stock white, red, amber and file-type hex left in Studio chrome now use roles or a light-dark pair; the file tree icons carry a contrast test. * chore(studio): drop two comments the names already carry * fix(studio): red marks, teal outlines and colour curves hold contrast in both themes Danger graphics use the danger ink, file tree outlines use the accent ink, the colour-curve well stays dark like the scopes, and six timeline token pins come back. * fix(studio): invalid readouts, the curve focus ring and editor dots stay distinct in both themes The invalid readout border is dashed as well as red, the curve graph's ring sits inside its dark well, the radius dots and ease handle centre follow the theme. * refactor(studio): ease and radius editors use colour classes; the dashed invalid border has a test Simplify pass: the ease editor's inline accent styles become stroke and fill classes, the radius dot class is computed once, the ratio check is one expression, and the alias table is named for what it holds. |
||
|
|
0b417cb57a | refactor(studio): delete the move writer no Studio path calls (#4837) | ||
|
|
567525a8c2 |
fix: readers never see an empty or half-written project file (#4777)
* fix: readers never see an empty or half-written project file * test: pin the create routes and block install to whole-file writes * fix(sdk): a write through a link to a missing file creates that file * fix: saves through a linked folder reach the real file, atomic writes handle long names * test(sdk): check for the stray file, not the folder listing * fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does * fix: agree the helper move in the deletion check, and match temp paths without a path regex * test: check POSIX file modes only off Windows |
||
|
|
590c192498 |
feat(catalog): add 3D objects, cursors, snow, starfield and paper transitions (#4349)
* feat(catalog): shelve 3D objects and cursors in their own catalog sections Carries the generator grouping rules from #4310 and #4308. * feat(registry): add a starfield ambient overlay From #4313. * feat(registry): add a falling snow ambient overlay From #4331. * feat(registry): add a torn-paper-rip transition From #4321. * feat(registry): add a paper-collage-reveal transition From #4322. * feat(registry): add a curtain-reveal transition From #4323. * feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo From #4310. * feat(registry): add eight cursor animations and the dart cursor From #4308. * fix(catalog): find items by everyday editor words and accept a bare search word From #4311. * chore(registry): remove the simulated-cursor component The Cursors section's new pointer animations replace it (from #4308). * fix(registry): take cursor-zoom-follow's screenshot from runtime variables only The declared default is empty, so page markup never becomes an image URL. Clears the code-scanning alert raised on #4308. * docs(cli): show a catalog search example that finds items "smoke" matches nothing in the catalog; "crossfade" finds fade-through. * chore(ci): allow the simulated-cursor removal in the deletion guard * fix(ci): skip tracked files missing from disk in the player CDN pin check Catalog regeneration removes a deleted item's pages before the publish PR untracks them, so the check crashed on every catalog deletion PR. * chore(cli): drop comments that narrate catalog search history * fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering * fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s * fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value * fix(registry): show real file thumbnails and end the context menu demo sooner * fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop * fix(registry): centre the text-select toolbar on the selection and tighten its timing * fix(registry): selection cursors default to the macOS system blue * style(registry): format the agent and annotate cursor sources * fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing * fix(registry): frame the product turntable fully and light it with two studio softboxes * fix(registry): touch-indicator taps like a real phone and the post it taps gets liked * fix(registry): press-ripple confirms with a check and small click rings at the cursor * fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled * fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim * fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press * style(registry): format the exploded stack source * fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen * fix(registry): make glass-hero a clear lens that shows the headline through it * fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card * fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on * fix(registry): starfield reads as a drifting night sky over a real title card * style(registry): format the glass hero source * fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away * fix(registry): curtain reveal draws velvet folds that gather onto a title card * fix(registry): make every zoom-follow click change the screen and read at zoom * fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc * refactor(registry): drop dead code and correct stale comments in reworked items * fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names * docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple * docs(registry): plain lowercase comments in oversized-cursor and touch-indicator * docs(registry): oversized-cursor names CLICK_AT as the click cue |
||
|
|
f7ef18dca8 |
fix(producer): renders leave no captured frames behind when interrupted (#4694)
Ctrl+C during a render left its whole work dir of captured frames (and a staging dir) in the renders folder. Puppeteer's default SIGINT handler kills Chrome and exits the process before the render's own cancel path can remove them. The engine keeps that default unless the host owns Ctrl+C; the CLI's render cancellation scope claims it while a render runs, so the render aborts and cleans up after itself. A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and staging dir now records its owner process (pid, hostname, boot, and on Linux the pid namespace), and the next render into the same folder removes the ones whose owner has exited. Cancelling a render while its browsers start no longer leaves anything running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up loop, the probe browser and file server, or a capture browser whose launch finished after the cancel (now closed before the worker settles, since Puppeteer's exit hook skips every other live browser). |
||
|
|
a31738f115 |
fix(docs): restore examples with the HyperFrames Player (#4675)
* fix(docs): restore visible example video cards * fix(docs): use catalog player for example films * fix(docs): fit player posters before playback * fix(docs): preserve the original examples grid |
||
|
|
5383e486bb |
fix(producer): gif renders survive rgb frames among transparent ones (#4610)
* fix(producer): gif renders survive rgb frames among transparent ones * test(producer): run the gif mixed-frame encode test in the ffmpeg lane * fix(producer): gif encode test passes the argv, reachability and comment checks * fix(producer): transparent gifs clear every frame so an opaque frame never lingers * fix(producer): only gifs that turn translucent after an opaque frame get whole cleared frames * fix(core): plain text keeps its own gif control block; gif encode failures share one shape |
||
|
|
b41ef425cc |
fix(release): name Release once in a changelog entry's tags (#4567)
* fix(release): name Release once in a changelog entry's tags * fix(release): the release tag appears once in any case; four past entries lose the duplicate |
||
|
|
04d56d06aa |
fix(producer): renders use the font named in a var() fallback (#4619)
* fix(producer): renders use the font named in a var() fallback When a font-family starts with var(--x, A, B) and --x is not defined, the render now reads A, B the way the browser does. A system font at the head of that fallback gets Inter placed in front of it, the same rule a direct system-font stack already follows. The rewrite stays inside the var() fallback, so a defined --x still wins. A defined property whose value is itself a var() is followed too. The distributed plan check reads the same answer. Fonts named only in such a fallback are best-effort: they are embedded when they resolve, and one that does not resolve is logged instead of failing a distributed render. Fonts named directly, or through a defined property, still fail closed as before. CSS-wide keywords such as inherit are no longer treated as font names to fetch. Compiled the way a distributed render compiles them, 28 registry items now embed a font they name this way, and none newly fails. * refactor(producer): fold var() fallback resolution into one branch * fix(producer): fonts behind a var() chain or empty fallback add no new render failures A family reached through a chain of defined var()s is optional like a fallback-only one: fetched fail-closed, skipped only when no source serves it. An empty var() fallback makes the declaration invalid, so the browser inherits and the plan check no longer rejects the families after it. * test(producer): a family named after a leading var() stays required * fix(producer): a transient error on an optional font warns and renders the fallback * chore(catalog): record the font requests var() fallbacks now make * fix(producer): required fonts fetch before optional ones can spend the time budget |
||
|
|
693c385508 |
fix(cli): commands exit right away even when the network or DNS is broken (#4604)
* fix(cli): commands exit without waiting on update checks or telemetry The update and skills checks run in a detached child that refreshes the caches for the next run; the parent only reads the caches. finalizeCli no longer awaits the telemetry flush: the exit handler flushSync() delivers what is queued. The events beacon, feedback and figma error paths stop awaiting flush() too. * fix(cli): retry a failed background check at most once an hour Before starting the background worker, the CLI stamps a per-check attempt time and skips a check it attempted within the hour, so a dead network no longer starts a worker on every command. The stale-pin notice writes onto a fresh read, detached spawns set windowsHide, and a test ties the worker path to its build entry. * fix(cli): a failed-check stamp dated in the future no longer silences update checks |
||
|
|
0a5e3c7fc4 |
feat(plugins): ship portable agent bundles and installation guides (#4608)
* feat(plugins): ship portable agent bundles and installation guides * fix(plugins): align new manifests with current release * test(plugins): split caption fixture setup and cleanup * test(plugins): pass render fixture path as an argument |
||
|
|
2b1bb8b731 |
test(cli): CLI tests run with a home folder of their own, never the user's (#4587)
* test(cli): CLI tests run with a home folder of their own, never the user's * style(cli): format the test home check * test(cli): the skills test lane also runs with a home folder of its own * style(cli): format the test home check * ci: the test reachability map follows the skills lane's new command |
||
|
|
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 |
||
|
|
1a53681de1 |
fix(studio): an automation lane edit is refused on a locked clip and reports how it saved (#4533)
* fix(studio): an automation lane edit is refused on a locked clip and reports how it saved Lane drags, Delete and paste save through the timeline's gated clip or group save instead of the property panel's commit, so the host's canEdit applies and the commit resolves saved, refused or failed. A landed save refreshes the lane store and the panel's selection, as the panel's commit did. * fix(studio): a refused automation save puts the preview and the lane back A save that does not land (recording, no id, a failed write) reverts the live preview it was given, the lane drops a draft whose save came back refused or failed, the no-id and no-project failures toast like the others, and the panel refresh reads the current selection. The panel commit's unused coalesce option goes, and the lane value reuses automationAttrValue. * fix(studio): a failed save restores the preview it was given, not an older one Each timeline save claims the preview's before-value when it starts, so a gesture begun while it is in flight records its own and a later failed save cannot put back a value from before an earlier save landed. * chore(repo): allow the automation gesture key files this branch removes * fix(studio): a refused automation save leaves the lane store and paste mark as saved Reverting a live value re-syncs the lane store from the restored preview, so an overlapping save that landed first cannot leave the store holding a later value that failed. A paste whose save is refused or fails gives back the selection and paste chain it replaced. * fix(studio): a failed save settles on what the file holds and a refused paste marks nothing A save that does not land settles the preview and store on the file's value (the claimed before-value only when the file cannot be read), and leaves a gesture begun meanwhile alone apart from correcting its before-value, so two overlapping failed saves cannot leave either on a value that never landed. A paste selects and marks its span only once it lands and only over the selection it was pasted at. * fix(studio): a failed save reads the file only after writes queued to it land The settle read goes through the same per-path queue as the writes, so a save that fails while another is queued settles the preview, the store and any newer gesture's before-value on what that queued save wrote. * fix(studio): an older failed save never settles over a newer edit on the same lane Live-preview bookkeeping moves to createLiveLanes, one owner for both timeline saves: each lane key carries a generation that every live write and save start advances, and a save settles the preview and store (on its landed value, or on the file after a failure) only while it is still the newest. A paste marks its span only if it is still the latest paste. * test(studio): a newer live drag keeps its preview when an older save recovers * fix(studio): every finished save records what the file holds, even under a newer edit A save's settle now always writes the file's value (landed, or read back after a failure) into the lane store and a live gesture's before-value, and moves the preview only while nothing newer has touched the lane. The clip store is set from that value rather than copied from the preview. A paste marks its span only while its clip is still the selected one. * fix(studio): an older automation save never overrides a newer gesture on its lane A failed save's recovery read could settle after a newer save started, and the save's own optimistic preview patch and failure unwind ran over a newer drag. Every preview write a save makes now goes through the lane's ownership check, only the newest save records its value, and a refused drag hands the lane back to the newest pending save. * fix(studio): timeline attribute saves land in the order they start An attribute save read the file before joining its write queue, so an older save with a slow read could land after a newer one, overwrite it on disk and leave the lane showing the newer value. The save now joins the queue first and reads, patches and writes inside it. A save moves the preview only while it is the newest save and no drag is live, which replaces the per-lane generation. * refactor(studio): name the in-queue history write for the queue it needs * test(studio): pin save start order and a newer release's preview mid-flight * fix(studio): a drag keeps its points when an older save lands under it An older save that landed mid-drag wrote the lane store, the lane dropped its draft, and the release committed the older points. The lane now keeps its draft while a gesture is live. A save that cannot read the file back settles on the last value read or saved on its lane, never on an unsaved one, and an older save that lands records its value there even when a newer save is pending. * fix(studio): a lane's first failed save still puts its drag back With the claimed-value fallback gone, a first save on a lane that failed and could not read the file back settled on nothing, leaving the drag playing. A save begun with no other save pending now records the value under its drag as verified, since that is what the file last settled on. * fix(studio): only a before-value read with no save pending counts as saved The value under a drag was trusted when its save started with nothing pending, but it was read when the drag began, possibly over a paste that later failed. A lane now marks a before-value clean only when no save was pending as the drag began. A group lane's preview carries its store mirror, as its live writes do. * fix(studio): a lane's saved value never crosses into another project The lane bookkeeping outlives a project switch and its keys had no project, so a value saved in one project could settle into another project's clip with the same key when a save there failed and could not read the file back. Lane keys now carry the open project, and a save moves the preview or store only while the project it was made in is still open. * fix(studio): a failed save reads its file back from its own project A save that failed after a project switch read its recovery from the newly opened project and recorded that project's value as its own. Each save now captures its project when it starts and reads back from it. * fix(studio): a failed save never brings back a value an undo removed A lane kept its last verified value after its saves settled, so after an undo a save that failed and could not read the file back settled on the removed value. The value now lives only while a save on the lane is in flight, and each save records what its own in-queue file read found. The group preview's store mirror is no longer needed and is gone. * test(studio): pass the file-read callback in the direct attribute-save tests * fix(studio): a drag whose before-value a save settled counts as clean When a settle rewrites a live drag's before-value to a verified value, that value is known saved, so the drag's own save can fall back on it even when its read and read-back both fail. Adds a test for a failed save reading back from its own project while another save is in flight across a switch. * fix(studio): a save started in one composition never lands in another of the same project Live lanes were scoped to the project only, so a save held across a switch to another composition of that project settled its value into the new composition's preview (clip and group lanes) and store (group lane). Lanes are now scoped to the project and the composition open when the save starts. createLiveLanes moves to its own module for the 600-line cap. * fix(studio): an undo during or after a lane gesture stays undone when the next save fails A live gesture records the lane's value before it starts, and a later save that cannot read the file settles on that value. An undo mid-drag, or after a gesture that ended without saving, left that value stale, so the save brought back what the undo removed. Every undo and redo now re-reads each open lane's value from the files it restored. * fix(studio): undoing group automation clears it from the members' lanes at once The group-info cache only noticed a change when its MutationObserver callback ran, so the soft restore's store sync, which reads it synchronously, mirrored the pre-undo group automation onto the members. A read now counts mutations the observer has not delivered yet. * fix(studio): an undo reaches every lane and pending save it rewrote, in any composition The undo re-read only covered lanes in the open composition and live gestures. History is project-wide, so undoing from another composition left the rewritten composition's lanes on the pre-undo value, and a save released while the undo was in flight kept that value as its fallback. The re-read now covers every lane of the project on a restored path, including saves still pending, and an undone file it cannot read makes the value untrusted instead of dropped. A missing element reads as unreadable, the same as a save's read-back. * fix(studio): a cancelled fade, stretch or preset audition leaves no stale lane value behind A cancel put the gesture's start back through a live write, which left the lane's before-value in place until the next save trusted it, even after the property panel, the source editor or an agent had changed the file. The fade cancel, the edge-stretch cancel and leaving a preset audition now end through the lanes' revert, so a before-value exists only during a live gesture. * test(studio): pin that a group write is read back fresh in the same task * style(studio): drop two comments the names already carry * fix(studio): a failed group save reads its own file back; fade cancels work without revert - The group save's failure read-back used the file of whatever composition was loaded when the write failed. It now reads the file captured when the save started, as the clip save does. - A fade cancel writes its start back live before it reverts, like the other cancel sites, so a host without the optional revert callback still puts the fade back. - The edge-stretch cancel comment says what the code now does. - The save test harness passes the revert callbacks, as the app does; without them the cancelled-gesture tests exercised a host that lacks them. * refactor(studio): trim three long comments on live automation saves |
||
|
|
d290c05b09 |
fix(cli): --yes in an unattended run no longer overrides a no saved by another process (#4547)
* fix(cli): --yes in an unattended run no longer overrides a no saved by another process * fix(cli): every settings writer takes the lock, and --yes needs a real terminal to reverse a no * fix(cli): the thin-results download offer also needs a real terminal * fix(cli): one owner-checked settings lock, and an unreadable settings file never counts as a yes * fix(cli): settings lock removals go through a reaper, and a non-boolean answer counts as a no * fix(cli): a settings lock that cannot be released no longer fails a write that landed * fix(cli): a leftover settings lock is never taken over, and doctor names it * fix(cli): a background update that cannot be recorded is not launched again every run * fix(cli): every on-device install re-reads the saved answer first, and an unsaved yes installs nothing * fix(cli): the thin-results offer stops when a yes cannot be saved * fix(cli): only the consent-checked step installs the on-device runtime, and a leftover lock warns once * fix(cli): the leftover-lock warning follows the attended-terminal rule |
||
|
|
8798e40da8 |
fix(registry): carousel cards fall back to their sample image inside the assets folder (#4543)
* fix(registry): carousel cards fall back to their sample image inside the assets folder When an image variable resolves to nothing, the 25 carousel blocks loaded the sample card by bare file name, which 404s beside the block and draws a white card. The fallback now keeps assets/carousel-images/, the folder every card image is installed into. A guard test fails on any block with a bare fallback. * test(registry): the carousel fallback guard covers every block that uses it The guard now picks each block that reads an image variable and fails when its fallback no longer matches, instead of silently skipping it, and ignores stray files in the blocks folder. |
||
|
|
c238abfe6f |
fix(media-use): tests never write into the person's media library (#4459)
* fix(media-use): tests never write into the person's media library * fix(media-use): guard the media home only in this repo's test runs * test(media-use): probe the media home from a plain child process * fix(media-use): keep miss logging best-effort when home is unresolvable * test(media-use): miss logging survives an unresolvable media home * docs(media-use): describe the media library without naming an app * chore(media-use): refresh the skills manifest hash |
||
|
|
81d8a03d2e | fix(repo): keep one fallow audit base cache per checkout instead of one per main commit (#4525) | ||
|
|
fbaa9f9205 |
ci: stop running CodeQL on merge queue groups, where its upload races the merge (#4492)
* ci: stop running CodeQL on merge queue groups, where its upload races the merge The code scanning rule does not apply to merge queue groups, and the queue deletes a group ref when it merges, so the upload failed on about half the groups. Pull requests and pushes to main keep scanning. * ci: catch merge_group in any trigger form and say the upload race is intermittent |
||
|
|
828c434f76 | refactor(studio): drop recordEdit's kind, which the project history never read (#4474) | ||
|
|
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 |
||
|
|
fc8cb86ef6 |
ci: check TODO owners, public URLs and source links in comments (#4453)
* ci: check TODO owners, public URLs and source links in comments * fix(ci): a TODO's issue link must be a github.com issue URL |
||
|
|
7c634426fd |
docs(cli): explain how catalog search works and which model it downloads (#4462)
* docs(cli): explain how catalog search works and which model it downloads * docs(cli): name the model catalog search was chosen against * docs(cli): keep only live rules in the word-search header comment * docs(cli): correct the hosted vector size in the local vector build comment |
||
|
|
53e2e20a15 |
fix(registry): six blocks load their own files and variables when mounted (#4452)
* feat(core): a composition's script can load its own files with __hyperframes.assetUrl * test(core): a template composition inside a mounted file resolves assets from that file * docs: assetUrl is for paths beside the composition, not project-root paths * docs: say assetUrl misses a project-root path * fix(core): a mounted composition's import map and module scripts run as such * refactor(core): scope a mounted module script under the same condition as a classic one * fix(core): a mounted module script loaded by src stays a module * fix(core): a mounted import map keeps the trailing slash of a prefix address * fix(registry): six blocks load their own files and variables when mounted * fix(registry): keep frost's variable declarations on its html element * fix(registry): orbit-card uses its own variables; frost font and catalog maps follow * chore(core): keep the module-script helpers' comment share at the base * fix(catalog): frost's payload runs its driving script, not the asset-base one * test(catalog): check import-map keys by membership, not a built regex |
||
|
|
0e34d69641 | ci: comment checks grade branches cut before their scripts existed (#4466) | ||
|
|
3520c8225c | ci: a package file's comment share may not rise (#4445) | ||
|
|
3194c2ae87 | ci: grade the comments each PR touches (#4444) | ||
|
|
8dabc8af63 |
fix(registry): glass-shard-title renders when mounted, and 4 skipped blocks install again (#4423)
* fix(registry): glass-shard-title renders when mounted, and 4 skipped blocks install again * fix(registry): glass-shard-title loads in Studio and ?play, and its docs payload fails loudly * test(scripts): pin the glass payload's hdr guard for any quote style |
||
|
|
096a586b41 |
ci: editing a PR's text no longer cancels its running checks (#4420)
* ci: editing a PR's text no longer cancels its running checks
CI, regression and Windows render re-run on pull_request edited (for base changes) with cancel-in-progress, so a body or title edit cancelled the running checks and the fail-closed Test aggregator went red on the cancelled run. Only a push or a base change cancels now; a text edit queues behind the running run.
* test(ci): read the workflows folder by path, not as a module URL
Fallow treated new URL('../.github/workflows/', import.meta.url) as an unresolvable import. Use import.meta.dirname and join, as scripts/check-skill-mirror.mjs does.
|
||
|
|
d1048cf98f |
fix(studio): ship component styles as their own stylesheet for embedders (#4393)
* fix(studio): ship component styles as their own stylesheet for embedders Studio's component rules (timeline clips, loaders, range inputs, toasts and the rest) lived only in the app sheet, so a page that embeds Studio's components with theme.css got unstyled clips and dark-on-dark labels. They move unchanged into components.css, exported as @hyperframes/studio/components.css; studio.css keeps the app shell and imports it, so Studio's compiled CSS has the same rules. * test(studio): read moved component rules from components.css * test(studio): catch class rules nested in the app sheet's layers and media blocks |
||
|
|
8d8ea810b6 |
fix(scripts): retry a fresh GitHub attachment's transient 404 in the captures gate (#4362)
* fix(scripts): retry a fresh GitHub attachment's transient 404 in the captures gate Reproduced this week: the same attachment URL 404'd, then 200'd minutes later with no edit to the PR in between, and whichever asset had been attached most recently was always the one that failed. The gate already retries 429/5xx with backoff, but treats every other status as final — so a fresh upload lagging GitHub's own read-path failed the check on attempt 1. 404 is now retryable, scoped to a GitHub attachment host (isAttachmentUrl) so a real 404 elsewhere (a typo'd link, a deleted gist) still fails fast. 403 stays non-retryable everywhere, including on an attachment host — it means something else (auth, a private-repo asset another workflow can't see), not propagation lag. Widened the retry window to match: 5 attempts, delays out to 60s, 3-minute deadline (was 3 attempts / 45s) — the observed lag was up to ~3 minutes. Tests: split the old combined 404-and-403 test; added the attachment-host 404 retry case and the non-attachment-host 404 still-final case; updated the backoff test's attempt/delay assertions for the new constants. 73/73 green, oxlint/oxfmt clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(scripts): stop the captures-gate retry tests from sitting through the real backoff Review on #4362: `runCli()`'s no-network preload stubs `fetch` but not time, so the two CLI tests exercising a genuinely-undownloadable capture now sit through the full 5-attempt/110s backoff per asset (two assets each) — 220s+ per test, pushing `bun run test:scripts` from 15s to 446s and `Test (support)` (timeout-minutes: 10) to 10m0s with zero margin. Preload now also collapses every `setTimeout` delay to the next tick, so the retry logic itself still runs for real (same attempt count, same terminal "could not download" outcome) — it just doesn't wait. 73/73 tests, 1.1s total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(scripts): split isRetryableStatus to clear fallow's CRAP-score gate CI's fallow audit flags `isRetryableStatus` at CRAP 30.0 (cyclomatic 5, threshold 30.0) — pre-existing since the original commit, not something the prior test-timing fix touched. Split into `isServerRetryable` (429/5xx) and `isRetryableAttachment404` (404 scoped to the attachment host), each at cyclomatic 2-3, composed by the still-exported `isRetryableStatus`. Same retry decision, no behavior change. 73/73 tests, oxlint/oxfmt clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
29fc95395d |
fix(skills): keep script imports inside installed skill directories (#4356)
* fix(skills): keep script imports inside installed skill directories * refactor(skills): simplify standalone module checks * fix(skills): carry the brand-logo lookup into the media-use copy of media-fetch |
||
|
|
922be55e55 | fix(skills): preserve captured font subsets (#4358) | ||
|
|
d49e7b95e0 |
feat(media-use): resolve brand logos from theSVG (#4291)
* feat(media-use): resolve brand logos from theSVG Replace the svgl and simple-icons tiers of the `logo` cascade with a single theSVG (thesvg.org) tier. theSVG ships 7,400+ official brand SVGs with color, mono, light/dark and wordmark variants. The tier fetches one manifest pinned to a glincker/thesvg commit on jsDelivr, memoizes it per process, and matches entities locally by normalized slug, title, or alias. That drops the hand-kept svgl alias and simple-icons slug maps. GitHub org avatar and favicon stay as the fallbacks for brands theSVG lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(media-use): sanitize fetched SVGs and gate theSVG by license freezeUrl/freezeLocalFile wrote every fetched SVG to disk unsanitized; the theSVG tier is now the sole SVG-returning logo provider, so an inline script tag or on-event handler would land verbatim in a user project. Route .svg destinations through the existing Figma sanitizer (new @hyperframes/core/sanitize-svg subpath, synced via package-subpaths). thesvgMatch never checked theSVG's own license field: entries theSVG itself marks unresolved (Proprietary, Unknown, Custom, TODO, or an explicit no-redistribution-license string) could still resolve and freeze into a project. Gate matches to the ten SPDX ids theSVG's submission form accepts; a rejected entry falls through to a lower ranked theSVG match, then to the github/favicon tiers. Also caps the theSVG manifest fetch at 20MB, streamed, matching freeze.mjs's existing cap discipline. It had a timeout but no bound. * chore(skills): regenerate skills-manifest for media-use/motion-graphics The original submission edited skills/media-use and skills/motion-graphics docs without regenerating skills-manifest.json; its own CI never ran (fork approval gate), so the drift went unnoticed until this pass's first CI run caught it. * fix(media-use): drop the sanitizer's workspace import, add parity test freeze.mjs importing @hyperframes/core/sanitize-svg broke the test-skills CI lane: packages/cli/src/media-use tests run with node builtins only, zero install, by design (media-use is vendored standalone into other projects, e.g. packages/desktop). Port the sanitizer into media-use/lib/svg-sanitize.mjs instead, and drop the now-unused core export. Parity between the port and packages/core/src/figma/sanitizeSvg.ts is enforced by a new test (scripts/check-svg-sanitize-parity.test.mjs, run in the test:scripts lane, which does have an install) over a hostile-SVG corpus, so the two cannot drift silently. * fix(media-use): close three sanitizer bypasses, add fetch timeout Independent review of the maintainer pass found the SVG sanitizer (packages/core/src/figma/sanitizeSvg.ts, reused by media-use's own port) was itself bypassable: an unquoted href/xlink:href survived, inline style="..." attributes were never checked, and a href bound through a renamed XML namespace prefix (xmlns:x=".../1999/xlink", x:href=...) slipped past the literal "xlink:href" match. Rewrote sanitizeSvg as a tag/attribute tokenizer instead of a set of literal-spelling regexes: parse each tag's attributes in any quoting form, resolve the local name after stripping any namespace prefix, then drop by local name (any on* handler, any *href whose value is not a #fragment or data:image/ embed, any style attribute containing url(, @import, expression( or javascript:). Ported the fix into media-use's svg-sanitize.mjs identically. Legitimate url(#id) references on non-style attributes (clip-path, fill) are untouched. Also adds a 10s AbortSignal.timeout to freezeUrl's fetch, matching every other fetchMedia call in this package - it had a size cap but no deadline. New cases for all three bypasses in both sanitizeSvg.test.ts and the parity corpus, each reproducibly failing against the prior head. * fix(media-use): replace regex SVG sanitizer with a DOM allowlist Two rounds of regex/tokenizer patches to sanitizeSvg found new bypasses in the same shape each time (unquoted href, style= url(), aliased xlink prefix; then presentation-attribute url(), unquoted '=' handling, SMIL animate/set href rewrite, self-closing-slash tag corruption, data:image/ navigation via <a>). Denylisting attribute names against a hand-rolled tokenizer was the wrong approach regardless of how careful the regex. Rewrote sanitizeSvg (packages/core/src/figma/sanitizeSvg.ts) as a real DOM walk (linkedom, already a dependency of core and cli) with an allowlist: known-safe elements and attributes only, everything else dropped by construction rather than pattern-matched as dangerous. href/xlink:href (any namespace prefix) kept only for a same-document fragment, or data:image/ off non-<a> elements. Any attribute value or <style> text containing url() kept only when every url() argument is a fragment; @import/expression(/javascript: dropped from style content. Serializing from a real DOM means output is always well-formed, so the self-closing-slash corruption class of bug is gone by construction. media-use's own copy (packages/cli/src/media-use/lib/svg-sanitize.mjs) can't import linkedom directly: its CI test lane runs bare `node --test` with no install step, so even an external dependency fails to resolve, not just a workspace import. It's now a generated file instead of a hand-copy: scripts/generate-media-use-svg-sanitize.mjs bundles the core source (esbuild, minified, zero remaining imports) and scripts/check-media-use-svg-sanitize-generated.test.mjs fails the build on drift. Verified the generated file runs with plain node in an empty directory with no node_modules on the path. Every payload from both independent-review reports is now a table-driven test in sanitizeSvg.test.ts (46 cases) and the same corpus in check-svg-sanitize-parity.test.mjs (39 cases) asserts core and the generated media-use copy agree and each independently rejects every non-local url(), javascript:, data: href on <a>, and on* handler. Mutation-proved against the real pre-fix blob, not a synthetic case. Sampled 56 real theSVG icons across all 7 collections to check the allowlist doesn't over-strip legitimate content: found and fixed three missing attributes (fx/fy, enable-background, a blanket data-* allowance) and one real behavioral bug - <style> elements were being dropped outright, silently unstyling any Illustrator-exported logo that colors paths via CSS classes. Fixed by keeping <style> on the allowlist and content-checking its text the same way as an inline style= attribute, with a regression test using that exact shape. Full suite after the fix: sanitizeSvg.test.ts 46/46, parity+drift tests 39/39, media-use suite 298/298, tsc/oxlint/oxfmt clean. * fix(lint): ignore generated svg-sanitize.mjs bundle in oxlint Minified esbuild output (packages/cli/src/media-use/lib/svg-sanitize.mjs, generated by scripts/generate-media-use-svg-sanitize.mjs) trips oxlint's correctness rules 504 times and fails `bun run lint`. Same shape as the existing registry/blocks/**/*.iife.js exclusions: a generated, minified, non-hand-edited bundle, not hand-written source. * fix(format): ignore generated svg-sanitize.mjs bundle in oxfmt Same generated esbuild bundle that needed an oxlint exclusion in the previous commit also fails (minified, no formatting to check against), cascading Preflight into Player perf, regression, Windows render verification, and preview-regression on CI. Same established pattern as the existing registry/blocks/**/*.iife.js and skills/**/*.min.js entries in .prettierignore. * refactor(media-use): reduce complexity flagged by fallow audit isSafeAttribute (10 cyclomatic) and the parity test's aggregate-marker check (11 cyclomatic, 26 cognitive, CRAP 132) both exceeded fallow's per-function thresholds after the rebase pulled in a stricter baseline. Extracted isXmlnsAttribute/isKnownAttribute as named predicates and survivingUnsafeMarkers as a named per-output helper; no behavior change, same guard order, same failure strings. Regenerated svg-sanitize.mjs. * refactor(media-use): make the parity test's marker check table-driven Fallow gives .mjs scripts no coverage credit, so its CRAP gate needs each function at cyclomatic 4 or less. The extracted survivingUnsafeMarkers still scored 9 (CRAP 90). The four regex markers are now a table, and the data:image-on-<a> and non-local url() checks are small named helpers. Same checks, same order, same failure strings. * fix(media-use): close CSS-escape and image-set bypasses in sanitizeSvg The url()/@import checks scanned raw text for the literal "url(" and "@import". A browser decodes CSS escapes first, so fill="\75 rl(...)", url\28...\29 and @\69mport all loaded an external resource, and image-set("https://...") loads one with no url() at all. Reproduced in headless Chrome: 8 of 8 payloads fired a request through the old code. CSS can only load a resource through a function or @import, so CSS text now passes only if it has no @import, every function is on a compute-only allowlist (color, transform, filter and math functions), and every url() is a same-document fragment, all checked on escape-decoded, lowercased text. Same fix reaches media-use through the regenerated bundle. Checked against all 13,384 files of the pinned theSVG corpus: 13,383 sanitize byte-identically; one drops an unreferenced id="XRP-(XRP)". The parity oracle now fails on any surviving attacker host, so it no longer shares the sanitizer's parsing blind spot. * refactor(media-use): share one capped body reader for freeze and manifest fetchJson copied freezeUrl's declared-length check and streaming size cap line for line. Both now call readCappedBody in media-fetch.mjs, which both already import; it has its own tests, since freeze's cap had none. The freeze cap errors now read "freeze failed for <url>: ..." (no caller or test matches the old wording). Also fixes freeze.mjs's comment, which still called the sanitizer a lexical pass. * style(media-use): format the parity test corpus * fix(media-use): make the sanitizer's CSS scans linear on hostile input The url() check used /url\(\s*(['"]?)([^)]*)\1\s*\)/ and the function check /([-\w]*)\(/: on attacker padding these backtracked cubically and quadratically (an unclosed url( plus 4,000 spaces took 20s; an 80,000 character word took 7.7s), and freeze runs the sanitizer synchronously on bodies up to 256MB. Both are now single-pass indexOf scans (a million characters in under 15ms), with the same verdict on ordinary input. A url() value holding "(" is rejected, so a nested url( can't hide inside one. A size test holds both payloads under 2s. * test(media-use): size payloads that fail the old scans fast instead of hanging * fix(media-use): stop the freeze timeout from aborting long downloads The 10 s AbortSignal.timeout on freezeUrl also aborted the response body, so any stock video, generated video or LUT that took longer than 10 s to download failed. The timer now covers only the wait for response headers and is cleared once they arrive; the 256 MB cap still bounds the body. * refactor(media-use): trim logo-provider comments to the repo limit The tier list and the thesvgMatch doc block ran past the four-line comment cap that the merge check enforces. Same facts, shorter blocks. * refactor(media-use): name the header wait and restore the variants note Renames the freeze timeout controller to headerWait so it no longer reads as HTTP headers, puts the theSVG variants fact dropped by the comment trim next to the line that picks the default mark, and keeps thesvgMatch on the file's JSDoc style. --------- Co-authored-by: Imad Phelps <imad.phelps@heygen.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
fb0d33c490 |
fix(ci): scope catalog dispatch and fail closed on detection errors (#4347)
* ci: scope catalog dispatch and fail closed on detection errors * fix(ci): distinguish full catalog validation dispatches |
||
|
|
a57e095468 |
feat(studio): panel tabs take the decided Current design (#4266)
Studio's panels become cards on a dark shell, and each panel's tabs take the "Current" design: the open tab is a fill in the card's colour that slides to the tab you pick, only the open tab shows its icon and close glyph, and a strip that is too narrow scrolls with a soft edge instead of hiding tabs behind a menu. This replaces the pill, underline and close treatment that was on this PR before. ## Before   ## After   Switching tabs (the fill slides in about 140 ms and the new icon fades in over 80 ms; the panel body switches at once): https://github.com/user-attachments/assets/8d359ada-c9e1-4425-9c95-61d5d4f5271b ### Every label whole, and the tabs use the whole strip (2x) An earlier head clipped the left strip's fourth tab next to empty strip space. That space was a 72 px slot (plus 4 px) held on every strip for the strip actions, even on strips whose panel is not focused and so draws no actions. Dockview sizes that slot by its content; this PR had pinned it. Now the slot takes room only where the actions are drawn, so an unfocused strip gives its tabs the whole width. Tab padding is 6 px with no gap between tabs, so the tabs also fit next to the actions. Labels, icons and close glyphs sit on the tab's centre in every strip. Left strip at 1440 x 900, tabs width against the room before the action slot: | | macOS | Linux (these captures) | |---|---|---| | Earlier head | 322 in 280, clipped | 312 in 280, clipped | | Now, panel unfocused | 278 in 358 | 268 in 358 | | Now, panel focused (actions shown) | 278 in 282 | 268 in 282 |    At 1280 x 800 the left column is narrower, and the pinned slot clipped Catalog next to the empty space. Room for the tabs there went from 241 px to 317 px:   Captured at the PR head ( |
||
|
|
ca4af78184 | ci: dispatch required checks for catalog publication (#4345) | ||
|
|
b1cf88aeba |
ci: generate catalog artifacts through a standing publish PR (#4337)
* ci: generate catalog artifacts through a standing publish PR * fix(catalog): reconcile obsolete publication snapshots * refactor(catalog): report failures after staging cleanup * refactor(catalog): separate generation and publication decisions * fix(catalog): unpublish deleted items with their sources * docs(catalog): keep scaffolding guidance concise |
||
|
|
b32b9a7dab |
feat(runtime): data-vfx-chain — a seek-deterministic per-pixel effect element (#4306)
* feat(core): data-vfx-chain defs and chain parsing (mirrors data-fx-chain)
Mirrors audioFx.ts's shape for the per-pixel VFX chain: a versioned
JSON chain attribute (data-vfx-chain), a def registry with typed
params (number/enum/bool/ref), and parse/serialize/normalize helpers.
HF_VFX holds one def, fractal-noise, with a placeholder frag; the
kernel and its consumers (packages/core/src/runtime/vfx.ts) land in
Task 1.2/1.3 of the vfx-chain-runtime plan, so vfx.ts is added to
.fallowrc.jsonc's entry list for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(runtime): paint data-vfx-chain canvases on every seek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): fractal-noise kernel (Basic/fBm approximation) with CPU reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): self capture through layoutsubtree with the page-composite readiness protocol
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): wave-warp kernel (Sine)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): displacement-map kernel (self-referential map)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): chain order is nesting order; preceding CSS effects are captured
...and following ones apply outside. The plan's one-line message for Task 2.4
is 105 characters; commitlint caps the header at 100, so the tail moved here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): seek determinism and CPU-reference PSNR for fractal-noise
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): size the output canvas from the host's layout box, not its transformed AABB
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(telemetry): vfx node count, capture class and types on render_complete
Threads vfxNodeCount, vfxCapture and vfxTypes from a static scan of the
compiled composition's data-vfx-chain hosts (mirrors the existing
compositionElementCount/colorGradingCount scan) through
RenderPerfSummary.drawElement into the render_complete event, next to
de_capture_mode. Exports chainCapture/parseVfxChain/HfVfxCapture from
@hyperframes/core's index for the producer to consume. The PostHog
dashboard tile is not created here — left for the coordinator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(vfx): data-vfx-chain guide — element shape, capture table, Studio flag, known constraints
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): register chain hosts that arrive with a sub-composition mount
`initVfx(document.body, fps)` ran once, synchronously, three lines after
`void loadExternalCompositions(...)` — so every `data-vfx-chain` host that
enters the DOM only when a `data-composition-src` mount resolves was never
registered. An unregistered host paints nothing and reports nothing: no
`[HyperFrames] composition script error`, the `.hf-vfx-out` canvas keeps its
300x150 default, and `paintVfx` never even arms the page-composite flag.
Measured on a real multi-file export served raw: `#main-l5-text`'s output
canvas stayed 300x150 and `window.__timelines` never gained the sub-comp.
Re-scan in the loader's `.finally`, before `maybePublishRenderReady()`, so
registration precedes readiness. `initVfx` now releases the outgoing
registry's programs, capture textures and ping-pong targets first — the
output canvas hands back the same WebGL2 context on a re-scan, so the
previous pass's GL objects would otherwise leak.
Two browser cases in `vfxDeterminism.test.ts`, both against the real runtime
bundle: a host whose `.hf-vfx-in` IS the mount (already worked — the mount
replaces the wrapper's children, not the wrapper, so the registered source
node survives), and a host that arrives INSIDE a mounted sub-composition
(failed with `armed: false` and no error; passes now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): runtime tsconfig typecheck passes
Under noUncheckedIndexedAccess, resolveCaptureSource's null return
widened VfxEntry.src past its VfxCaptureSource | undefined type; the
function is internal-only, so its return type moves to
VfxCaptureSource | undefined instead. Indexing PingPong.framebuffers
by a non-literal index produced | undefined against
WebGLFramebuffer | null, coalesced to null. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): structural DOM predicates instead of realm-bound instanceof
vfx.ts checked `instanceof HTMLCanvasElement`/`HTMLElement` directly, which
returns false for inner-realm nodes when the Studio previews across an
iframe realm. domRealm.ts had no canvas predicate yet, so this adds
isCanvasElement mirroring the existing isImageElement pattern, then swaps
all three vfx.ts checks to the structural predicates the lint guard requires.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): paint self-capture hosts under hyperframes snapshot, not only the engine path
`paintVfx(t, { engineMode: true })` armed the page-composite protocol and
returned, betting that every capturing chain runs under `frameCapture.ts`.
`hyperframes snapshot` (and check/compare/validate/layout, and Studio's
thumbnail capture) seek through the same `renderSeek`, never read
`__hf_page_composite_pending` and never call `__hf_page_composite_resolve` —
so a `self` chain painted nothing there, in silence, because its content
lives in a `layoutsubtree` canvas the browser never paints directly.
Engine mode now arms the preview-side capture as well and paints on whichever
completes first. A seek token stops a superseded async capture from repainting
a newer frame, and a registry-identity check stops it painting an entry
`initVfx` already released. `resolveVfxCapture` stays non-idempotent on
purpose: the engine resolves only after its own before-capture hooks have
mutated the DOM, so its capture is still the authoritative one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): an exporter-emitted .hf-vfx-out gets the box the spec fixes for it
findOrCreateOut adopted a canvas the exporter had already emitted without
applying the layout cssText, so it kept the default position:static;
display:inline and wrapped to the line AFTER the layer-wide .hf-vfx-src
canvas — painting a second, misplaced copy of the layer one full layer
height below the host. Measured on retro-wave #main-l6-text: .hf-vfx-src
rect top 315.1 h 93.0, .hf-vfx-out rect top 409.2, both style=null,
position static (ae-mcp findings Task 3.5b). Hosts with no emitted
out-canvas were never affected, which is why only this one host moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(telemetry): vfx_node_count counts hosts, not chain nodes — rename to vfx_host_count
The field increments once per `data-vfx-chain` attribute occurrence, never
per node inside a chain's JSON, so the name promised something the scan does
not measure. Renames the internal `vfxNodeCount` and the wire property
`vfx_node_count` to `vfxHostCount`/`vfx_host_count` across the orchestrator
scan, the perf summary, the CLI render path and the telemetry event. No
dashboard consumes the field yet, so the wire rename costs nothing now and
would cost a migration later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): join the preview capture to the seek-completion barrier, not race it
`paintVfx` fired `capturePreviewThenPaint` and forgot it, so nothing the CLI
awaits waited for a `self` chain's paint. Every snapshot-family command
(`snapshot`, `check`, `compare`, `validate`, `layout`) seeks through
`seekCompositionTimeline`, which drains `window.__hfWaitForSeekCompletion`
and then allows one settle race before screenshotting — and only the
`"hf-seek"` event's `waitUntil()` could put anything in that barrier. The
screenshot therefore landed on however many hosts happened to have finished,
which varied with host count and machine speed.
Extracts the "track, untrack on settle, retain the first rejection" block
`dispatch()` built inline into an exported `registerSeekCompletion`, and has
`paintVfx` register the preview capture through it. Also awaits the per-host
paints with `Promise.all` instead of sequentially: N hosts cost about two
animation frames now, not 2N.
Measured on the pre-fix bundle with two `self` hosts, both output canvases
read `[0,0,0,0]` at the moment the barrier returned; they painted only
because the settle race that follows happened to be long enough. Both tests
added here fail on that bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): report WebGL context loss loudly instead of silently no-op painting
One WebGL2 context per `data-vfx-chain` host, against a browser cap that is
commonly 16 and shared with `colorGrading.ts`'s per-element contexts. A lost
context makes every later call on it do nothing, per spec — so the chain kept
"painting" a blank canvas with no error, which is exactly what the module's
loud-failure contract exists to prevent.
Adds the `webglcontextlost` listener pair's first half, following
`colorGrading.ts`'s precedent: `preventDefault()`, mark the entry, report
once through `reportVfxError`. The entry is then skipped by `paintVfx`,
`capturePreviewThenPaint` and `resolveVfxCapture` — silently, since the one
loud report already happened and repeating it per frame is spam.
Restore is left unhandled on purpose, with the reasoning in the doc comment:
`initVfx`'s existing re-scan already rebuilds every GL object, and a second
narrower rebuild path for `webglcontextrestored` would duplicate it. The
shared-context redesign the review floated is likewise out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(vfx): cache uniform locations at pass-build time instead of querying every paint
`setPassUniforms` asked the driver for `u_size`, `u_t`, `u_fps` and every
param's location on every paint of every pass, and `bindPass` did the same
for `u_src` on every bind. `buildPasses` already constructs each `VfxPass`
once at init, right after the program links, and a linked program's
locations never change — so they are resolved there now and read from
`pass.locations` in the hot path.
Same values, same `null`-means-optimised-out semantics (`gl.uniform*(null,
…)` is a defined no-op, which is what the per-frame query produced too), so
the existing vfx.test.ts and the browser determinism suite cover it
unchanged — both still green, the latter against a rebuilt runtime bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): bound the paint wait per host so a stalled compositor can't hang the seek barrier
`paintVfx` registers `capturePreviewThenPaint` into the shared seek-completion
barrier (
|
||
|
|
b73df3549e |
refactor(studio): remove unreachable expanded-child timeline code (#4219)
* refactor(studio): keep legacy fixture state out of rendered rows * fix(studio): keep timeline keyboard actor mounted * refactor(studio): remove expansion telemetry and prove stable nested rows * test(studio): type nested timeline fixture broadly * test(studio): remove expansion lane assertions * test(studio): assert nested row identity by key * fix(studio): satisfy removal and format guards * fix(studio): clear fallow findings in timeline cleanup * test(studio): update timeline removal expectations * fix(studio): remove stale timeline test helpers * fix(studio): keep automation lanes mounted without disclosure * refactor(studio): keep audio lanes while removing clip expansion * fix(studio): restore audio disclosure state * test(studio): remove obsolete expanded fixture assertions * fix(studio): resolve provider rebase without clip auto expansion * fix(studio): remove inline keyframe expansion rows * fix(studio): restore automation disclosure state * fix(studio): remove stale timeline model inputs * fix(studio): pass animation state to timeline layout * fix(studio): remove stale expansion tests and helpers * test(studio): cover shared audio lane labels * fix(studio): remove unused keyframe callback plumbing * fix(studio): clear rebase lint leftovers * fix(studio): remove unused timeline header export * style(studio): format timeline cleanup * fix(studio): restore composable timeline exports * fix(studio): keep audio lanes open across selection * fix(studio): align timeline lane test contract * fix(studio): remove stale lane test import * fix(studio): align track header test callback * fix(studio): remove stale track header test prop * test(studio): preserve ruler half pixel alignment * test(studio): keep audio lane fixtures drawable * test(studio): assert shared lane owner label * fix(studio): align audio lane expansion with layout state * test(studio): use the audio lane owner label * fix(studio): unify audio lane expansion state * fix(studio): align automation lane labels * fix(studio): remove stale lane height import * test(studio): assert automation labels against the real curve, not a formula The previous test recomputed the expected top with the same formula the fix uses, so a regression in TimelineAutomationLaneSlot's own offset would still read green. Moved it to TimelineLanes.test.tsx, which mounts both the header and the canvas, and compares their rendered [data-automation-lane-label]/[data-automation-lane] tops directly. * revert(studio): restore the keyframe-row feature from main Miguel: keep the per-property keyframe rows (diamonds, prev/next arrows, disclosure, auto-open) exactly as main. This PR's real scope is removing dead expandedParentStart consumers, not the keyframe-row UX. * refactor(studio): remove dead expanded-child timeline code expandedParentStart is never set anywhere on main (the inline sub-composition child-row expansion that produced it was already removed in #4132/7797c5ead). Delete its now-dead consumers: the TimelinePane move/resize/delete/split rebase wrappers, the ZMirror call site that routed through them, the timelineDragLanding topology guards, isMainTrackElement's expanded-child exclusion, laneGapFloor's host-window-start math, and the paired expanded-clip-edit telemetry. Keeps every keyframe-row feature (diamonds, prev/next arrows, disclosure, auto-open, telemetry) untouched. |
||
|
|
bc1c60ae5b |
fix(catalog): regenerating the texture-mask page no longer duplicates its section (#4302)
* fix(catalog): regenerating the texture-mask page no longer duplicates its Texture masks section The page generator carries forward any section whose heading it does not own, and it emits "## Texture masks" without owning it. Every run kept the old copy and appended a new one; main's page had three. * chore(catalog): drop the two duplicate Texture masks sections from the texture-mask page * chore(catalog): run oxfmt on the texture-masks regression test |
||
|
|
dd97478175 |
fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice (#4179)
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice transcript-cut extracts each kept range to its own final-codec file, then concat-copies them. That produces two audible artifacts at every cut: 1. No boundary fades. Concat splices raw segment edges together, so the waveform steps discontinuously and each cut clicks. 2. Encoder priming silence. Each segment is encoded to AAC independently, so the encoder pads every one with priming samples. Stream-copy concat bakes that padding in as a silent gap at each splice. Measured 24.4 ms and 24.1 ms at the two splices of a three-range cut. It accumulates: the nth splice lands roughly 25n ms late, so splice positions drift away from the sum of the preceding segment durations. The second survives the first -- adding fades alone leaves the gap. Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead of the final codec, so audio is encoded exactly once, at concat. Boundary fades of 30 ms are applied per segment, scaled down on segments shorter than 4x the ramp and skipped on degenerate ones. Measured on the same three-range cut, widest run of digital silence at each splice: before 24.4 ms / 24.1 ms after 0.1 ms / 0.1 ms --copy is unchanged: stream copy cannot filter, so it keeps producing final-codec segments and a copy concat. Audio-only output is byte-identical in duration before and after. scripts/lib test suite: 34/35, the one failure (lut-preset-provider) pre-exists on main and is unrelated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(skills): regenerate manifest for media-use * fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed The caller already computed {fadeIn, fadeOut} per segment, but fadeFilterFor still took one parameter and always emitted both afade ramps -- so a single kept segment (no splice at all) and every segment's true start/end got faded regardless. Extracted the pure function into lib/transcriptCutFade.mjs, made it honor both flags, and added its first unit test: first/middle/last/single-segment filter strings, no ffmpeg needed, fails against the previous version. Also installs ffmpeg in the Test: skills CI job (same action the Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN proof runs in CI instead of skipping silently for lack of the binary. * fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed CLI's transcript-cut.mjs imported a file that no longer existed. Moved its test alongside, matching every other lib helper's tests. Also drops three redundant Boolean(args.copy) wrappers (a ternary condition already coerces) that oxlint's no-extra-boolean-cast flagged. * style(media-use): reflow the shortened ext ternary to oxfmt's shape Dropping the Boolean() wrapper shortened the line enough that oxfmt 0.41 now collapses the ternary onto one line; format:check was failing on the stale multi-line version. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |