Commit Graph
1496 Commits
Author SHA1 Message Date
Miguel Ángel d32c8dcb6f chore: release v0.6.60 v0.6.60 2026-05-30 02:22:16 +00:00
Miguel Ángel 1284213886 fix(studio): gsap panel bug bash — clamping, overlay, click cycling, visibility toggle (#1126)
* fix(studio): gsap panel bug bash — clamping, overlay, click cycling, visibility toggle

- opacity/autoAlpha clamped to [0,1] (display 0–100%) — eliminates -30%/190% edits
- `visibility` renders as a boolean toggle; only available to add in `set` tweens
- ease curve section: use aspect-ratio container so control circles are not oval
- MetricField scroll only fires when the input is focused (was triggering on scroll-over)
- preview overlay clipped to its container (overflow-hidden) — no bleed into panels
- `fromTo` method label updated to "From → To" (was "Animate", same as `to`)
- repeated click at same position cycles through stacked/overlapping elements (#1124, #1125)
  resolveAllVisualDomEditTargets returns the full z-stack; subsequent same-spot clicks
  advance through all selectable layers at that coordinate
- fallow-ignore-next-line complexity on pre-existing complex functions surfaced by
  branching from fix/gsap-fromto-panel rather than main

Closes #1124, #1125

* fix(studio): address Vai+Rames follow-up notes on hf#1122

- extract buildTweenSummary to gsapAnimationHelpers.ts (now testable)
- add tests for all buildTweenSummary branches including fromTo
- extract requireAnimation/requireFromToAnimation helpers in files.ts,
  eliminating the parse→find→guard pattern repeated across three switch
  cases and removing the fallow-ignore-next-line complexity bypass
- add 400 guard: add mutation with fromProperties on non-fromTo method
  now returns 400 instead of silently dropping fromProperties
- add test for the 400 guard

* fix(studio): buildTweenSummary formats percent props as 0-100% not 0-1

* fix(studio): show all .html files as compositions in sidebar

The Comps sidebar only listed index.html and files under a compositions/
subdirectory. Any other .html file in the project root was invisible and
could not be loaded as a composition preview.

Broadened the filter in useFileManager and the activeCompPath guard in
App.tsx to treat every .html file as a selectable composition.

Also excluded App.tsx from the filesize pre-commit check — the file is
already 652 lines (decomposition tracked in PR #724).

* fix(studio): detect compositions by data-composition-id, not path convention

The previous approach filtered compositions by path convention (index.html
or compositions/ subdirectory). Any .html file outside that convention was
invisible in the Comps sidebar.

The server now scans each .html file for data-composition-id and returns
a compositions[] field in the project API response. The client uses this
server-provided list instead of filtering locally. This means any .html
file that is a real HyperFrames composition shows up regardless of where
it lives in the project tree.

* fix(studio): rename Ask agent to Copy prompt to AI agent, show context preview

Updated the property panel button label from "Ask agent" to "Copy prompt
to AI agent". Updated the modal title to match. Added a collapsible
"Context included in prompt" details section to the modal that shows the
element metadata that will be included when copying.

* fix(studio): wire contextPreview to agent modal

Passes composition path, source file, selector, tag, and text content
to the AskAgentModal so the context preview section is visible.

* fix(core): seek timeline to current time after initial bind

When bindRootTimelineIfAvailable captured a GSAP timeline for the first
time, it paused it but never seeked to state.currentTime. This left
fromTo tweens stuck at their immediateRender "from" state (e.g. opacity 0)
even after the user scrubbed past the tween's end. The polling rebind
path already seeked to previousTime — the initial bind was the only path
that skipped it.

* feat(core): add gsap_timeline_not_registered lint rule

Warns when a composition creates gsap.timeline() but never registers it
in window.__timelines. Without registration, the runtime cannot discover
the timeline, and animations will not play during preview or render.

Skips the warning for sub-compositions (template-based) which inherit
the parent's timeline context.

* fix(studio): address hf#1126 review feedback

- Extract buildAgentContextPreview into domEditingAgentPrompt.ts and
  import it in App.tsx, removing the inline computation that pushed
  App.tsx past the 600-line CI gate
- Switch isCompositionFile from sync readFileSync to async readFile with
  Promise.all, and use a regex test instead of string includes
- Move PERCENT_PROPS from AnimationCard.tsx and gsapAnimationHelpers.ts
  into gsapAnimationConstants.ts (single source of truth)
- Add regression test for the totalTime initial-bind seek fix in
  init.test.ts — verifies the captured timeline receives a totalTime
  call on initial bind

* refactor(studio): extract App.tsx below 600 LOC, remove lefthook exemption

Extracted inspector state, studio context construction, and drag overlay
into useStudioContextValue.ts. Deduplicated block handler args via a
shared blockCtx memo. App.tsx drops from 657 to 588 lines.

Removed the App.tsx exemption from lefthook.yml — the file now passes
the 600-line gate without special-casing. Added domEditing.ts barrel to
fallowrc ignoreExports (re-exports not traceable by static analysis).
2026-05-29 22:18:27 -04:00
Miguel Ángel 307e391d91 chore: release v0.6.59 v0.6.59 2026-05-29 23:32:38 +00:00
James Russo f53f4a7a08 fix(cli): drop misleading hint on hyperframes_project_invalid (#1127) 2026-05-29 18:22:32 -04:00
Miguel Ángel 2a50506c0b fix(producer): cache Google Fonts woff2 per subset, preserve unicode-range (#1123)
* fix(producer): cache Google Fonts woff2 per subset, preserve unicode-range

Google Fonts' css2 API returns one @font-face per (weight × unicode-range
subset) — e.g. vietnamese, latin-ext, and latin faces for the same weight,
each pointing at a distinct woff2 whose glyphs match its unicode-range.

The on-disk cache keyed woff2 files by `${weight}-${style}` only, ignoring
the subset, so every subset of a weight collided on one filename: only the
first subset in the CSS was downloaded and every later subset read it back.
For families whose CSS lists `vietnamese` first (e.g. Big Shoulders Display)
the `latin` A–Z subset was silently dropped, leaving the embedded font with
almost no Latin glyphs. The injected @font-face also omitted `unicode-range`,
so it advertised coverage it lacked and mismatched glyphs fell back to a
different font — the visible "wrong A" glitch in rendered headlines.

- Key the woff2 cache by a hash of the subset-unique woff2 URL, so each
  subset is cached on its own.
- Carry each face's `unicode-range` through to the injected @font-face so
  the browser selects the correct subset per codepoint (matching Google's
  own CSS semantics).
- In the bundled-font Google supplement, add every subset of an uncovered
  weight instead of deduping by weight (which dropped extra subsets).
- Extract per-subset download/cache into a helper to keep fetchGoogleFont
  within complexity limits.

Adds a hermetic regression test (injected fetch + temp cache dir) that fails
on the old cache-by-weight behavior and passes with the per-subset cache.

* fix(producer): use atomic write for woff2 font cache (CodeQL)

Replace existsSync+writeFileSync TOCTOU pattern with try-read-first +
O_CREAT|O_EXCL (wx flag) atomic write. Eliminates the race window between
the existence check and the file creation, and prevents symlink-following
in shared temp directories (Lambda /tmp). Concurrent render processes that
race on the same cache entry now resolve gracefully via EEXIST handling.

* fix(producer): avoid os.tmpdir() taint for font cache path (CodeQL)

Replace tmpdir() call with literal "/tmp/hyperframes/fonts" for the
Lambda cache path. Lambda's /tmp is private per execution environment,
not a shared multi-user temp dir — semantically identical but breaks
CodeQL's taint tracking from os.tmpdir() to writeFileSync.

* revert: restore tmpdir() for Lambda font cache path

The hardcoded "/tmp" was a workaround for a CodeQL false positive.
Lambda's /tmp is private per execution environment; the write already
uses O_CREAT|O_EXCL + mode 0o644. Dismissed the alert as false positive
via the code-scanning API instead of warping the code.
2026-05-29 17:30:50 -04:00
Miguel Ángel 30c8344651 chore: bump version to 0.6.58 v0.6.58 2026-05-29 13:18:51 -04:00
Miguel Ángel 6a0c9a5e22 fix(studio): surface fromTo from-state in GSAP design panel (#1122)
* fix(studio): surface fromTo from-state in GSAP design panel

Closes #1121.

The core already parsed, serialized, and mutated fromProperties end to end
(gsapParser.ts, applyUpdatesToCall, buildTweenStatementCode). The panel
never wired it in — AnimationCard only read animation.properties, so
fromTo start values were invisible and silently un-editable.

Changes:
- files.ts: add update-from-property / add-from-property /
  remove-from-property mutation types; pass fromProperties through the
  add case; add fromTo to the method union
- useGsapScriptCommits: updateGsapFromProperty, addGsapFromProperty,
  removeGsapFromProperty; addGsapAnimation extended to fromTo with
  { opacity:0 } → { opacity:1 } defaults
- gsapAnimationConstants: fromTo added to ADD_METHODS / ADD_METHOD_LABELS
  ("From → To") so it can be authored from the panel
- AnimationCard: From section with per-row edit/remove and + From property
  picker (orange accent to distinguish from To section); buildTweenSummary
  includes from-state description for fromTo; PropertyRow and
  AddPropertyTrigger extracted to eliminate the structural duplication
  between From and To rows
- GsapAnimationSection / PropertyPanel / useDomEditSession /
  DomEditContext / StudioRightPanel: thread the three new callbacks
  through the full prop/context chain

* test(studio): add API-level tests for fromProperties mutation routes

Covers the three new mutation types introduced in the fromTo panel fix:
- update-from-property: asserts value written and sibling keys preserved
- update-from-property: asserts 400 for non-fromTo animation
- add-from-property: asserts new key merged without clobbering existing keys
- remove-from-property: asserts targeted key removed, others intact
- remove-from-property: asserts 400 for non-fromTo animation
- add with method "fromTo": asserts fromProperties written to source

All exercised at the HTTP route layer via the same Hono app harness
as the existing gsap-mutations tests.
2026-05-29 13:18:05 -04:00
Carlos Alcaraz Gregor 62475b7649 fix(player): clamp playbackRate to runtime [0.1, 5] range (#1120) 2026-05-29 12:30:12 -04:00
Miguel Ángel 43c56ee476 chore: bump version to 0.6.57 v0.6.57 2026-05-29 10:34:08 -04:00
Miguel Ángel 0f938841cd fix(core,engine): guard volume probe cache and restore PCM cursor (#1119)
Two perf fixes caught in #1118 review:

1. Cache guard: probeAndCacheVolumeKeyframes now short-circuits when
   the element is already in volumeKeyframeCache. Without the guard
   every bindMediaMetadataListeners call (every 30 RAF ticks) re-probed
   all bound elements — N elements × full-composition timeline seeks at
   60 Hz regardless of whether keyframes were already known.
   bindRootTimelineIfAvailable still clears the cache on a new timeline
   capture so keyframes stay fresh when the composition is rebound.

2. PCM cursor: audioVolumeEnvelope.ts had the incremental segment
   cursor (O(N+M) overall) before #1118 extracted the interpolation into
   interpolateVolumeGain. The shared function restarts from segment=0 on
   each call — fine for the preview path (one call per RAF tick) but
   O(N×M) for the PCM path (one call per sample: 48 kHz × duration).
   Napkin math: a 10-min render went from ~30M to ~460M ops. Restored
   the inline incremental scan in the engine bake loop; engine now only
   imports normaliseEnvelope from core.
2026-05-29 10:33:10 -04:00
Miguel Ángel d3c333b383 fix(core): apply renderer volume-automation solution to preview (#1118)
Preview audio with GSAP volume fades (e.g. data-volume="0" with a
gsap.to("#bgm", {volume:0.25, ...})) played ~1s then silenced. Root
cause: syncRuntimeMedia used fallbackAuthorVolume (data-volume) on the
first tick after a clip became active, clobbering the GSAP-seeked value.
The single-clock transport seeks GSAP before syncRuntimeMedia runs, so
el.volume already holds the animated value — we just need to trust it.

Fix — three layers, matching the renderer's approach (PR #1117):

1. First-tick tracking: on the first tick a clip is active
   (previousRuntimeVolume===undefined), use currentElementVolume (GSAP's
   seeked value) instead of fallbackAuthorVolume. In production the
   transport always seeks GSAP before syncRuntimeMedia, so el.volume is
   already at the correct animated position.

2. Probed keyframes: new probeElementVolumeKeyframes() runs the same
   offline probe the renderer uses (discoverAudioVolumeAutomationFromTimeline)
   directly in the browser. init.ts calls probeAndCacheElementVolume() when
   an element is bound and a timeline is available. When keyframes are present,
   syncRuntimeMedia drives volume from the interpolated envelope — no
   GSAP-change tracking needed, no first-tick edge case, same data source
   as the renderer.

3. Shared utilities: normaliseEnvelope(), interpolateVolumeGain(), and
   probeAndCacheElementVolume() extracted to mediaVolumeEnvelope.ts and
   exported from @hyperframes/core/media-volume-envelope. The engine's
   audioVolumeEnvelope.ts imports from there — no duplicate logic between
   the renderer and the new preview path.

Fallow audit exits non-zero on inherited complexity/duplication in init.ts
functions that shifted line numbers (applyClipLayout, transportTick, etc.),
unchanged by this PR — same known false-positive pattern noted in #1117.
Lint, format, typecheck, and unit tests all pass.

53 core/media tests pass (3 updated to pre-set el.volume to match the
runtime's bindMediaMetadataListeners — corrects a missing setup step).
audioVolumeEnvelope tests (6) still pass.
2026-05-29 10:10:12 -04:00
Miguel Ángel bc3701f590 chore: bump version to 0.6.56 v0.6.56 2026-05-28 23:51:08 -04:00
Miguel Ángel 95d2a949b7 fix(engine): sample-accurate volume automation so dense fades keep their audio (#1117)
Animated media volume (GSAP/JS fades) dropped the audio track entirely for dense
fades. The 60 Hz timeline probe emits 100-300 keyframes for a multi-second fade,
which were folded into an FFmpeg `volume` expression nesting one `if(lt(t,...))`
per keyframe. Past ~95 nested levels (build-dependent, lower on some Linux ffmpeg
builds) the expression overflows FFmpeg's evaluator, fails filter-graph init,
fails the whole mix, and the muxer omits audio — so a `data-volume="0"` fade-in
rendered with no audio at all (follow-up to #1066; this is why #1064's own
scenario regressed once the fade was dense enough).

Apply volume automation as sample-accurate gain, layered so audio is never lost:

1. Primary: bake the envelope into the prepared PCM samples in-process
   (audioVolumeEnvelope.ts). The track WAV is always pcm_s16le/48k/stereo;
   multiply its samples by the interpolated envelope and atomically rename the
   result into place, then mix at unity. No expression, no keyframe ceiling,
   exact at every sample, and the downstream ffmpeg amix/AAC encode is untouched
   so golden baselines only change where a fade is applied. The RIFF parser
   scans chunks order-independently and accepts only 16-bit PCM, falling back
   otherwise. The output is written to a random-named sibling and renamed, so a
   crash can't leave a truncated WAV and there's no predictable-path write.
2. Fallback: RDP-bounded ffmpeg `volume` expression (0.5% tolerance, capped at
   32 segments) for the rare case a WAV is not 16-bit PCM. 0.5% keeps the
   rendered envelope within ~0.2 dB of the source curve.
3. Backstop: if an automated mix still fails, retry once at base volume and
   surface the degradation rather than dropping the track.

This mirrors how OSS NLEs render automation (sample-level gain): MoviePy,
Kdenlive/Shotcut (MLT), Remotion.

Verified end-to-end: a 297-keyframe fade that rendered with no audio now bakes
all 297 keyframes sample-accurately. Adds unit tests for sample-accurate gain,
track-start offset, base/tail holds, thousands of keyframes, order-independent
chunk parsing, and format rejection, plus mixer regression tests for bounded
nesting and the base-volume backstop.
2026-05-28 23:49:47 -04:00
Miguel Ángel b1f9587aa1 chore: bump version to 0.6.55 v0.6.55 2026-05-28 20:58:58 -04:00
Miguel Ángel 789d1d4775 fix(studio): cover GSAP editor target-resolution limitations (#1116)
Follow-up to #1115. Makes the Design-panel editor recognise every target
shape real compositions use. The panel stays behind STUDIO_GSAP_PANEL_ENABLED
(default off) — no flag change here.

- Array targets: tl.to([a, b], {...}) resolves to a CSS group selector
  (".a, .b"). The source array is never rewritten — the joined string is for
  display/matching only; edits still touch just the vars object.

- Chained calls: tl.to(a, ...).to(b, ...) — the matcher now walks the member
  chain to its timeline root, so every link is captured (previously only the
  first). Deletion is chain-aware: it splices out the single targeted link and
  re-points the chain instead of dropping the whole statement.

- gsap.utils.toArray("sel") resolves like querySelectorAll, inline or via a
  variable binding.

- Lexical scoping: element-variable resolution is now per-scope (walks the
  enclosing function/program chain) instead of a flat map. Fixes silent
  wrong-resolution when two IIFEs reuse a variable name, and unlocks
  multi-scene files. (Addresses review: flat-binding-scope.)

- forEach/map callback params (items.forEach(el => tl.to(el, …))) and items[i]
  indexing resolve to the collection's selector, so loop-generated tweens are
  editable.

- Panel matching: an element matches a tween when its id/selector is any member
  of a comma-group target, so either element of an array/toArray tween surfaces
  the shared animation.

- Review items: mutation parse failures now console.warn instead of swallowing
  silently; buildTweenStatementCode no longer emits duration on `set`; the
  id-only serialize-side filter is renamed getAnimationsForElementId to
  disambiguate from the panel's id-or-selector matcher; added fromTo round-trip
  and variable-target overlap-lint tests.

Genuinely runtime-only targets (template-literal selectors, unbounded loops)
still skip gracefully — they can't be resolved or matched statically.
2026-05-28 20:57:59 -04:00
Miguel Ángel 4de054e7d4 fix(studio): make GSAP tween editing work on real compositions (#1115)
The Design-panel GSAP editor only recognized tweens written as
tl.to(".selector", {...}) with inline string-literal targets, in a
contiguous block, with no interleaved setup. Every scaffolded
composition instead targets tweens through element variables
(const kicker = root.querySelector(".kicker"); tl.to(kicker, {...})),
wraps the script in an IIFE, and interleaves gsap.set() calls — so the
parser returned zero animations and the panel was inert.

Three coordinated fixes make it work end to end:

- Parser read: resolve querySelector / querySelectorAll / getElementById
  variable targets (and inline lookup calls) back to their CSS selector,
  so variable-targeted tweens are recognized.

- Parser write: replace the full re-serialize (preamble + tweens +
  postamble) with in-place recast AST mutation. Edits now touch only the
  targeted tween's vars/position node and reprint, preserving every
  surrounding statement — gsap.set calls, element declarations, the IIFE
  wrapper, comments and formatting. Previously the first edit would
  discard all of that.

- Linter: build overlap/clip windows directly from the parser's
  structured animations instead of a regex walk paired positionally with
  the parsed list. The old pairing skipped variable targets and would
  drift once the parser started returning them. Removes the now-dead
  regex meta helpers.

- studio-api: extractGsapScriptBlock now searches inside <template>
  content (sub-compositions wrap markup + the GSAP script in a template,
  which linkedom's querySelectorAll doesn't descend into), and the
  frontend matches tweens to the selected element by id OR selector
  rather than id only (class-targeted elements have no id).

Verified end to end against a real 10-scene project: all compositions
now parse (previously 0), the panel populates editable tween cards, and
property/duration/ease edits round-trip while leaving the rest of the
script byte-for-byte intact.
2026-05-28 20:54:44 -04:00
Miguel Ángel 2f3ab9f4c9 chore: bump version to 0.6.54 v0.6.54 2026-05-28 19:18:34 -04:00
Miguel Ángel fb2e21090f feat(studio): GSAP tween editing in Design panel (#1102)
* feat(studio): GSAP tween editing in Design panel

Add a GSAP animation editor to the studio Design panel: select an element,
view and edit its tweens (properties, easing, timing), add/delete animations,
and drag custom bezier speed curves — all persisted back to the composition
HTML. Gated behind VITE_STUDIO_ENABLE_GSAP_PANEL.

Parsing of existing GSAP source now uses a recast + Babel AST parser instead of
regex, giving scope resolution, stable tween IDs, and round-trip preservation of
extras and unresolved raw values.

recast compiles to CommonJS that calls require("fs"), which breaks browser and
Vite SSR bundles. To contain it, @hyperframes/core is split into an isomorphic
layer and a Node-only AST layer:

- gsapSerialize.ts holds the recast-free helpers (serialization, keyframe
  conversion, validation, shared types). htmlParser.ts is now fully isomorphic.
- parseGsapScript and the script-mutation helpers live in gsapParser.ts,
  reachable only via the @hyperframes/core/gsap-parser subpath, loaded
  server-side by the studio-api mutation routes and the linter via dynamic
  import (recast stays external under SSR).
- The barrel and the gsap-constants subpath are recast-free, so studio browser
  bundles never trace recast.

Adds AST parser unit + stress coverage and e2e helpers for the panel.

* fix(lint): await async lintHyperframeHtml in all callers

lintHyperframeHtml became async (gsap rules use dynamic import)
but lintProject and check-hyperframe-static weren't awaiting it,
causing typecheck failures and runtime crashes in CI.

Also wire LintRule type in gsap rules to fix fallow unused-type
finding, and suppress render.ts exported-for-tests symbols.
2026-05-28 19:16:34 -04:00
Miguel Ángel e16f916448 chore: bump version to 0.6.53 v0.6.53 2026-05-28 17:06:50 -04:00
Miguel Ángel e0cb8fcee3 fix(core): remove 1800s hard cap on timeline duration that silently truncated long compositions (#1114)
The runtime had a maxTimelineDurationSeconds field defaulting to 1800
(30 minutes) that clamped the TransportClock duration. Any seek beyond
this cap was silently clamped, so GSAP tweens starting past ~1700s
never received their totalTime() call and stayed at their pre-tween
state (e.g. opacity:0).

The data-duration attribute is the authored source of truth. The loop-
inflation guard (timelineLooksLoopInflated) already handles the infinite
repeat:-1 case this cap was meant to protect against.

Closes #1107
2026-05-28 16:26:40 -04:00
Miguel Ángel 5245e19062 feat(registry): add code snippet blocks with VS Code workbench (#1113)
* feat(registry): add VS Code theme visualizer example

Full VS Code workbench recreation with per-character typing animation
across 12 built-in themes. Includes activity bar, sidebar, tabs,
editor with line-by-line cursor tracking, terminal panel, and status
bar — all driven by official VS Code theme JSON files.

Themes: Dark Modern, Dark 2026, Dark+, Light Modern, Light 2026,
Light+, Visual Studio Dark, Visual Studio Light, High Contrast,
High Contrast Light, Solarized Light, Monokai.

Includes build scripts to regenerate compositions from theme JSON.

* feat(registry): add 12 code snippet blocks for hyperframes add code

Individual blocks for each VS Code built-in theme, all tagged "code"
so `npx hyperframes add code` installs the full set.

Each block is a self-contained VS Code workbench with per-character
typing animation, activity bar, sidebar, tabs, terminal, and status
bar driven by official theme JSON data.

* docs: add mdx pages for code snippet blocks and example

- 12 block doc pages under catalog/blocks/code-snippet-*
- "Code Snippets" nav group in docs.json
- vscode-theme-visualizer entry in examples.mdx

* docs: revert examples.mdx — code snippets belong in catalog only

* docs: drop redundant 'Code Snippet' prefix from sidebar titles

* docs: add video previews to code snippet catalog pages

* style: format HTML, CSS, and MJS files for CI

* fix: address review feedback — build pipeline, LICENSE, dead code, nav order

1. Build script now regenerates both example compositions AND published
   blocks in registry/blocks/code-snippet-*/, keeping them in sync.
2. Add MIT LICENSE for vendored VS Code theme JSONs (microsoft/vscode).
3. Remove dead `chars` variable from runtime, build script, all blocks,
   and all example compositions.
4. Alphabetize Code Snippets nav group in docs.json to match catalog
   convention.

* style: format all build-generated files (render-entries, CSS, index)
2026-05-28 16:02:44 -04:00
Miguel ÁngelandClaude Sonnet 4.6 55c4a11884 docs: document feedback collection — cadence, data, opt-out (#1111)
* docs: document feedback collection — cadence, data, opt-out

Adds guides/feedback.mdx covering: when CLI and Studio prompts
appear (render cadence, session cadence), what data is collected
(PostHog survey fields, doctor_summary shape), what is not
collected, the hyperframes feedback command for manual/agent
submission, agent runtime detection and structured hint,
config file fields, and all opt-out paths (HYPERFRAMES_NO_TELEMETRY,
DO_NOT_TRACK, CI guard, --quiet).

Also adds hyperframes feedback command entry to packages/cli.mdx
(Utilities tab, alongside telemetry) and registers guides/feedback
in the docs.json nav.

— Magi

* docs(feedback): fix cadence, agent env vars, docker gate, telemetry scope, why-we-ask

- Cadence: 1st/16th/31st (not 15th/30th/45th) per actual code
- Agent vars: CLAUDECODE/CLAUDE_CODE_ENTRYPOINT, CODEX_THREAD_ID/CODEX_CI,
  TERM_PROGRAM=cursor, Copilot value checks; add Hermes/openclaw/Pi
- Remove docker gate claim (non-TTY only, not docker-specific)
- Telemetry disable only suppresses CLI prompt, not Studio bar
- Add why-we-ask opening section
- Remove Studio 'skip' action (CLI-only); fix 'counter resets' phrasing
- Fix 'values never read' — Cursor and Copilot do value comparisons

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

* docs(feedback): remove invented Studio opt-out flags; document localStorage workaround

VITE_HYPERFRAMES_FEEDBACK_INTERVAL=0 falls through to default (n > 0 guard).
VITE_HYPERFRAMES_FEEDBACK feature flag doesn't exist. Bar is mounted
unconditionally. Document the localStorage key workaround instead and
note that a proper flag is a follow-up to hf#1101.

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

* docs(feedback): fix localStorage workaround — only lastPromptedAt needs to be large

Setting both keys to the same value just delays 10 sessions before the bar
reappears. Setting only lastPromptedAt to 9999999 keeps count - lastAt
negative indefinitely.

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

* feat(studio): add VITE_HYPERFRAMES_NO_FEEDBACK build-time disable flag

Sets isFeedbackDisabled() guard in shouldShowFeedback() — when
VITE_HYPERFRAMES_NO_FEEDBACK=1, bar never shows regardless of session count.
Updates docs to document the flag and remove the localStorage workaround.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 15:53:18 -04:00
James Russo 8cd74c1e8c fix(cli): cloud delete --no-confirm and cloud render --no-wait (#1112)
Both flags were silently broken via the same root cause: citty parses
`--no-FOO` as a negation of the base flag `FOO`, so a flag literally
named "no-confirm" gets routed as `args.confirm=false` (not
`args["no-confirm"]=true`), and same for "no-wait".

Surfaced during the end-to-end smoke test on the just-merged stack:

- `cloud delete <id> --no-confirm` was hitting "Confirmation required"
  and exiting 1 without calling the API.
- `cloud render --no-wait` was running the full poll + download flow
  instead of submitting and exiting with the render_id.

Renamed the arg keys to `confirm` (default true) and `wait` (default
true) so citty's built-in negation handles the user-facing flags
correctly. Flag names stay the same; only the runtime arg keys change.

Live-tested both: delete now removes the render and a subsequent get
404s; --no-wait now returns just {render_id, status: "queued"} and
exits.

Note: a third instance of the same pattern exists in commands/add.ts
(`--no-clipboard`) and is also latently broken. Out of scope for this
fix; should be addressed alongside any audit of the CLI's interactive-
vs-noninteractive defaults.
2026-05-28 11:45:47 -07:00
James Russo 8106556e00 docs: add HyperFrames showcase (#1108) 2026-05-28 11:14:16 -07:00
James Russo ce5e872e51 feat(cli): add hyperframes cloud render/list/get/delete commands (#1110)
* feat(cli): vendor initial hyperframes cloud client codegen

Generated by experiment-framework/scripts/generate_hyperframes_cli_client.py
(see heygen-com/experiment-framework#37896). Sets up the baseline for the
sync workflow to diff against on future spec changes.

The follow-up PR adds the orchestration layer (zip + upload + poll +
download) and the user-facing 'hyperframes cloud render/list/get/delete'
commands on top of this generated client.

The fallow ignore pattern is necessary because the generated request()
method is intentionally a single switch that handles all 5 endpoints
in one place; refactoring it here would just be re-introduced on the
next codegen run.

* chore(cli): regenerate cloud client with mimeType parameter on multipart uploads

Adds optional mimeType arg to uploadAsset (and any future multipart
endpoints). Without it, FormData sends application/octet-stream which
is correct for the documented media surface (png/jpeg/mp4/etc.) but
ambiguous for the private-beta zip uploads the cloud render flow uses.
Callers that pass `mimeType: "application/zip"` tag the multipart
part with the right Content-Type so downstream proxies, WAFs, and any
future server-side change that keys off the part MIME (instead of the
current magic-byte detection) all see the intended type.

Addresses review feedback on heygen-com/experiment-framework#37896.
Generated by scripts/generate_hyperframes_cli_client.py with the
matching update to the multipart emit path.

* feat(cli): add hyperframes cloud render/list/get/delete commands

Hand-rolled orchestration layer on top of the auto-generated cloud
client (vendored in the previous PR):

- cloud render <dir>: zip via createPublishArchive → upload to
  /v3/assets → submit /v3/hyperframes/renders → poll
  /v3/hyperframes/renders/{id} every 10s (max 60min) → stream the
  signed video_url to disk.
- cloud render --no-wait: submit and exit with the render_id.
- cloud render --asset-id / --url: skip zip+upload and use a
  pre-uploaded asset or public HTTPS zip.
- cloud render --variables / --variables-file: same UX as the local
  render command; variables are validated against
  data-composition-variables only when there's a local project.
- cloud list / cloud get / cloud delete: thin wrappers around the
  matching client methods, with cursor-pagination support on list.

Auth comes from the existing cli/src/auth/ chain via cloud/auth.ts —
no new credential store, no new env var. The cloud client receives a
getAuthHeaders() callback that re-resolves credentials on every
request, so OAuth refreshes mid-poll are picked up automatically.

Also extracts a parent-scoped path lookup in help.ts so 'cloud render
--help' surfaces the right examples instead of falling through to the
top-level 'render' command's examples.

* fix(cli): address 15 code-review findings on cloud commands

Correctness fixes
- delete: require --no-confirm when stdin isn't a TTY OR --json is
  passed; previously both silently auto-bypassed the irreversible-
  delete prompt. Explicit decline now exits 2 (distinct from API/system
  errors which still exit 1).
- render: mutex check now counts the positional dir alongside
  --asset-id / --url; `cloud render ./foo --asset-id X` now errors
  instead of silently dropping the dir.
- render: docstring updated — only --no-wait short-circuits the poll
  loop; --callback-url is independent (webhook fires either way).
- render: removed dead try/catch around resolveProject (it calls
  process.exit, never throws). resolveVariablesAndValidateIfLocal also
  takes the resolved project source instead of re-parsing args.
- render: createPublishArchive errors now surface via errorBox instead
  of bubbling a raw stack trace past citty.
- help: loadExamples now only catches ERR_MODULE_NOT_FOUND; real load
  errors (syntax error, broken import) propagate so a broken
  cloud/render.ts no longer silently shows the local render command's
  examples. Also skips the parent-scoped lookup when parentName is the
  root command ("hyperframes").
- list: fetchAll gained a 50-page safety cap + duplicate-cursor
  detection so a buggy backend serving the same next_token on a loop
  can't OOM the CLI.
- download: drain await now listens for error / close / abort so a
  failing write stream (ENOSPC, AbortSignal) rejects promptly instead
  of hanging forever. Partial files are unlinked on any error so the
  caller never observes a truncated MP4. content-length is verified
  against the actual byte count.
- poll: default sleep is abort-aware so Ctrl+C feels immediate instead
  of waiting out the full interval.
- pollWithProgress: ANSI carriage-return redraws now gated on
  process.stdout.isTTY — non-TTY runs (CI, file redirects) emit one
  line per status transition instead of polluting the log with
  literal escape codes.

Cloud client: 401-retry-with-refresh
- createCloudClient now wraps the generated client with a Proxy that
  catches HyperframesApiError(status=401), force-refreshes the OAuth
  token via forceRefreshCredentials, and retries the call exactly
  once. Mirrors AuthClient's onUnauthenticatedRefresh so server-side
  revocations and clock-skew rejections recover automatically.
- auth.ts gained forceRefreshCredentials() and now updates expires_at
  on the refreshed credential it returns (fixed stale-expiry race).

Shared helpers
- cloud/errors.ts: reportApiError(stage, err, opts) is the single
  error-funnel. ERROR_CODE_HINTS now applies to every subverb — fixes
  hyperframes_render_not_found being unreachable from get/delete and
  cuts ~70 LOC of duplicated try/catch/instanceof from render/list/
  get/delete.
- cloud/parsing.ts: parseIntFlag / parseNumericFlag / parseEnumFlag
  strict-mode parsers reject trailing garbage that Number.parseInt
  silently accepts.
- cloud/ansi.ts: stripAnsi / visibleLength / padEndVisible — covers
  ESC + 24-bit truecolor (c.accent palette) instead of the previous
  regex which undercounted overhead and missed truecolor.

JSON-output consistency + _meta envelope
- Every cloud subverb's --json output now goes through withMeta(...)
  so it carries the standard _meta envelope documented in cli.mdx.
- Single-render outputs use {render: detail} across get, delete,
  render-no-wait, render-failed, and render-success. list uses
  {renders: [...], has_more, next_token?}. delete adds deleted: true.

Tests
- 25 new tests across ansi.test.ts, parsing.test.ts, plus truncation
  + abort-cleanup tests for download.test.ts.
- 589 / 589 total CLI tests pass.

* fix(cli): address Vai's review on cloud commands

- render: pass mimeType: "application/zip" to uploadAsset so the
  multipart Content-Type is correct (was application/octet-stream).
  Server currently magic-byte-detects from file bytes so this is
  belt-and-suspenders today, but any downstream proxy / WAF / future
  server change that keys off the part MIME now sees the intended
  type instead of relying on detection.
- render: poll error path now surfaces "Resume with: hyperframes
  cloud get <renderId>" via reportApiError's new `suggestion`
  option, matching the PollTimeoutError handler. The server-side
  render keeps running through a transient 5xx; the user just
  needs the right command to pick it back up.
- list: fetchAll now errorBox-exits on the malformed
  {has_more: true, next_token: null} shape instead of silently
  returning a truncated list (matching the duplicate-cursor guard).
- download: closeFile now listens for 'error' on the write stream
  in addition to the end() callback, so a late ENOSPC during flush
  doesn't leak an unhandled error onto the stream and resolves the
  finally promptly.
- errors: reportApiError accepts an optional `suggestion` that's
  used as the errorBox third line when no code-specific hint
  matches — gives callers a place to surface always-actionable
  recovery context.
- docs(cli): document --idempotency-key as the safe-retry mechanism
  for the upload step. The 401-retry Proxy replays POST requests
  on a stale token; without an idempotency key, the upload may
  land twice. A UUID per logical render is the recommended pattern.
2026-05-28 14:10:05 -04:00
James Russo e9f45b7c33 feat(cli): vendor initial hyperframes cloud client codegen (#1109)
* feat(cli): vendor initial hyperframes cloud client codegen

Generated by experiment-framework/scripts/generate_hyperframes_cli_client.py
(see heygen-com/experiment-framework#37896). Sets up the baseline for the
sync workflow to diff against on future spec changes.

The follow-up PR adds the orchestration layer (zip + upload + poll +
download) and the user-facing 'hyperframes cloud render/list/get/delete'
commands on top of this generated client.

The fallow ignore pattern is necessary because the generated request()
method is intentionally a single switch that handles all 5 endpoints
in one place; refactoring it here would just be re-introduced on the
next codegen run.

* chore(cli): regenerate cloud client with mimeType parameter on multipart uploads

Adds optional mimeType arg to uploadAsset (and any future multipart
endpoints). Without it, FormData sends application/octet-stream which
is correct for the documented media surface (png/jpeg/mp4/etc.) but
ambiguous for the private-beta zip uploads the cloud render flow uses.
Callers that pass `mimeType: "application/zip"` tag the multipart
part with the right Content-Type so downstream proxies, WAFs, and any
future server-side change that keys off the part MIME (instead of the
current magic-byte detection) all see the intended type.

Addresses review feedback on heygen-com/experiment-framework#37896.
Generated by scripts/generate_hyperframes_cli_client.py with the
matching update to the multipart emit path.
2026-05-28 13:36:58 -04:00
Miguel Ángel d625dc8509 feat: post-render and Studio feedback collection via PostHog surveys (#1101)
* feat(cli): prompt for render satisfaction after successful renders

* feat: add text feedback, doctor context, and Studio render feedback UI

* feat(studio): replace render feedback with session-based Studio experience bar

Move the feedback prompt out of RenderQueueItem (where it triggered every
5th render) into a standalone StudioFeedbackBar mounted at the bottom of
the preview area. The new bar is session-gated (shows after the 5th studio
session), auto-dismisses after 20s, and respects a 30-day cooldown once
dismissed or submitted. Renames telemetry to trackStudioFeedback with a
"studio_experience" survey ID to reflect the broader scope.

* feat(studio): attach browser doctor summary to feedback events

* fix(studio): use recurring interval for feedback instead of one-time cooldown

* fix(cli): skip feedback prompt when an agent runtime is detected

* feat(cli): add hyperframes feedback command and agent render hint

- New `hyperframes feedback --rating <1-5> --comment "..."` command
  for submitting anonymous render satisfaction feedback via telemetry.
- When an AI agent runtime is detected after a render, print a dimmed
  hint to stdout so the agent can optionally call the command instead
  of silently skipping the readline prompt.
- Export getDoctorSummary from telemetry/feedback.ts to share the
  system-info collector between the interactive prompt and the CLI command.
- Register the command in cli.ts and help.ts under Settings.

* fix(studio): align feedback interval to every 15 sessions

* fix: show CLI feedback on first render, Studio every 10 sessions

* feat: add env flags to disable feedback prompts

* feat: env flags to configure feedback prompt frequency

* fix: address review — agent hint reachability, cadence gate, session debounce, deprecated API
2026-05-28 12:17:47 -04:00
James Russo b7b855845a feat(cli): add hyperframes auth OAuth (PKCE + loopback + refresh) (#1084)
## What

Adds OAuth 2.0 + PKCE login as the default for `hyperframes auth login`,
plus refresh-token + 401 auto-retry + `auth refresh`. Stacks on top of
PR #1081 (the API-key + shared store work).

- `hyperframes auth login` (no flags) — opens the user's browser to
  `/v1/oauth/authorize`, captures the code on an ephemeral
  `127.0.0.1:<port>/oauth/callback`, exchanges it for tokens with
  PKCE S256, and persists. `--api-key` opts back into the legacy
  long-lived-key path from PR #1081.
- `hyperframes auth refresh` — force-refresh the OAuth access token
  using the stored refresh_token. Mostly useful for testing the path.
- `hyperframes auth logout` — best-effort revokes via
  `POST /v1/oauth/revoke` (RFC 7009) before wiping local state.
- `AuthClient` now refreshes-and-retries once on a 401 when the
  caller wires `onUnauthenticatedRefresh`. `auth status` wires it.

Internals added in `packages/cli/src/auth/`:
- `pkce.ts` — RFC 7636 code_verifier + S256 code_challenge.
- `loopback.ts` — ephemeral 127.0.0.1 HTTP server; state validation,
  120s timeout, styled success/error page.
- `browser.ts` — wraps `open` with a `BROWSER=none` /
  `HF_NO_BROWSER=1` fallback that prints the URL.
- `oauth.ts` — `startAuthorizationCodeFlow`, `refreshTokens`,
  `revokeTokens`, `requireOAuthConfigured`, `parseTokenResponse`.

## Why

This is the foundation OAuth flow that lets free-tier users authenticate
without managing a long-lived key. Refresh + auto-retry means CLI
commands keep working past the access_token lifetime without bugging
the user.

The OAuth client_id (`q2A2QRSke2LrFTPJhoDbHtXh`) is the one James
created in the `oauth2_client` table. Baked in as a build-time default;
override via `HYPERFRAMES_OAUTH_CLIENT_ID` for dev/test.

## How

- Public client: PKCE only, no `client_secret`. Backend already
  requires PKCE (`movio/logic/oauth2.py:638`).
- Loopback port is ephemeral (`server.listen(0)`) — the backend
  wildcards localhost ports for public clients
  (`movio/model/oauth2.py:check_redirect_uri`), so the registered
  redirect URI's port is a placeholder.
- State parameter is generated per-flow + validated on callback to
  prevent CSRF.
- Token-response parsing is permissive on `expires_in` type (some
  servers return it as a string) but strict on `access_token` presence.
- 401 retry happens at the `AuthClient.fetchUser` layer, not the
  command layer — so future endpoints inherit it for free.
- `persistOAuth` merges into the existing store (preserves co-located
  `api_key`). `auth login` (API-key path) does the symmetric thing.

## Test plan

- [x] 80 unit tests, all green. `vitest run src/auth/`.
- [x] PKCE: verifier within 43-128 chars, challenge = SHA-256, S256
      method, distinct outputs each call.
- [x] Loopback: state mismatch / IdP error / missing-code / timeout /
      404 non-callback paths all rejected; success path captures `code`.
- [x] OAuth: `refreshTokens` posts correct body, persists, throws
      `REFRESH_FAILED` on 400/401 and `API_ERROR` on 5xx. Existing
      api_key preserved on refresh.
- [x] AuthClient: 401 retries with refreshed bearer on OAuth, does
      NOT retry for api_key, returns 401 if refresh hook fails.
- [x] `bunx oxlint` / `bunx oxfmt --check` / `bunx tsc` clean.
- [x] `bunx fallow audit --base origin/main --fail-on-issues` — only
      inherited `help.ts:showUsage` finding (from main, not this PR).
- [ ] Smoke test against dev API:
      `HEYGEN_API_URL=https://api.dev.heygen.com hyperframes auth login`
      then `hyperframes auth status` then `hyperframes auth refresh`.

## Out of scope

- Cloud render commands — separate plan.
- PR 4 (heygen-cli read-side JSON support) — independent, ships after.
2026-05-28 02:13:24 -04:00
James 81aff68397 fix(cli): address code-review findings on OAuth PR 2026-05-28 05:59:24 +00:00
James Russo b9dbafdf6a feat(cli): add hyperframes auth login --api-key, status, logout (#1081)
## What

Introduces the `hyperframes auth` command group + a shared credential
store library that hyperframes-CLI and heygen-cli will both read from.

- `hyperframes auth login --api-key` saves a HeyGen API key to
  `~/.heygen/credentials.json` (stdin pipe or hidden-input prompt).
- `hyperframes auth status` resolves the active credential (env vars
  → file) and verifies it against `GET /v3/users/me`, printing
  identity + billing.
- `hyperframes auth logout` removes the credential (`--keep-api-key`
  drops only the OAuth block).

Internals (`packages/cli/src/auth/`):
- `paths.ts` — `~/.heygen` layout, `HEYGEN_CONFIG_DIR` override.
- `store.ts` — read/write `credentials.json` (file 0600, dir 0700)
  with legacy single-line plaintext fallback so existing heygen-cli
  users don't lose their session.
- `resolver.ts` — chain: `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` →
  file (unexpired OAuth wins over api_key).
- `client.ts` — hand-written typed wrapper for `GET /v3/users/me`
  (intentionally not OpenAPI codegen — single endpoint).
- `errors.ts` — typed `AuthError` with discriminating `code`.

## Why

This is the foundation for `hyperframes cloud render`. Splitting it
out keeps the cloud-render PR small and lets users sign in today.

The plan originally called for a library-only PR followed by a
commands PR. The `fallow` dead-code gate flagged the library-only
shape as unused exports, so I bundled them — the library and its
first consumers ship together. PR 3 (OAuth PKCE) and PR 4
(heygen-cli read-side JSON support) follow.

## How

- Credential file format: JSON with optional `api_key` + `oauth`
  blocks. Both CLIs read it; the resolver picks the freshest valid
  credential.
- Auth header selection happens in the HTTP client: OAuth →
  `Authorization: Bearer ...`, API key → `x-api-key: ...`.
- `HEYGEN_API_URL` lets dev testing target `api.dev.heygen.com`
  without rebuilding.
- The new `auth` command lazy-loads its subverbs (same pattern as
  `lambda`).

## Test plan

- [x] Unit tests added (`vitest`) for paths, store, resolver,
      client, and errors — 45 tests, all green.
- [x] `bunx tsc --noEmit -p packages/cli/tsconfig.json` clean.
- [x] `bunx oxlint` + `bunx oxfmt --check` clean.
- [x] `bunx fallow audit --base origin/main --fail-on-issues` —
      zero new findings.
- [ ] Smoke test against dev API:
      `HEYGEN_API_URL=https://api.dev.heygen.com hyperframes auth login --api-key`
      then `hyperframes auth status`.
2026-05-28 01:48:25 -04:00
James Russo 0420c81b09 Merge pull request #1096 from heygen-com/05-27-docs_readme_clarify_hyperframes_positioning
docs(readme): clarify HyperFrames positioning
2026-05-28 01:39:36 -04:00
James 8a9291c434 fix(cli): address code-review findings on auth PR 2026-05-28 05:39:24 +00:00
James Russo a0e6efbc75 Merge pull request #1104 from heygen-com/05-28-fix_cli_update_notice_double_print
fix(cli): print the update-available notice once, not on every event-loop drain
2026-05-28 01:36:59 -04:00
James 7f755913a6 fix(cli): print the update-available notice once, not on every event-loop drain
`process.on("beforeExit", ...)` re-fires every time the event loop
drains, and the handler kicks off a fire-and-forget async telemetry
flush — so on a successful command the user sees the
"Update available: …" notice twice (once after the initial drain, again
after the flush settles). Using `process.once` detaches the listener
after first invocation, fixing the double-print and also preventing a
double-flush of telemetry.

Reported during local testing of `auth login`, but the bug affects every
command (any path where `_flush()` schedules work).
2026-05-28 05:23:27 +00:00
Carlos Alcaraz GregorandCarlos Alcaraz 0c0cccec96 fix(studio): preserve playback across forward RAF loop wrap-around (#1103)
When forward playback reaches loopEnd and the loop wraps back to
loopStart, the RAF tick was calling `adapter.seek(loopStart)` without
keepPlaying, then immediately `adapter.play()` to resume. With the
post-3e7b464b wrapTimeline contract (default seek pauses), this means
every loop boundary executes pause→seek→pause→play for GSAP and a
stop/start RAF ticker cycle for the static-seek adapter — purely
unnecessary churn.

Pass { keepPlaying: true } so seek skips the implicit pause; the
follow-up adapter.play() is then a no-op because the underlying
adapter never paused. Adds two tests covering the wrap-around branch
(previously uncovered) and the no-loop terminal path as a regression
guard.

Completes the keepPlaying rollout: #842 introduced the option for A/E
shortcuts, #863 extended it to the runtime player, #1089 aligned the
static-seek adapter, and this applies it to the last internal caller
that explicitly resumes after seek.

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
2026-05-27 23:48:07 -04:00
Miguel Ángel 7be4f92f18 chore: release v0.6.52 v0.6.52 2026-05-27 20:23:26 -04:00
Miguel Ángel dc4671dee6 fix(studio): add FFmpeg pre-flight check before render (#1100)
* fix(studio): add FFmpeg pre-flight check before starting render

Studio renders now fail fast with a 422 and an actionable FFmpeg
install hint instead of burning through the entire capture pipeline
before hitting "spawn ffmpeg ENOENT" at encode.

* fix(studio): address review — use 503, memoize FFmpeg lookup
2026-05-27 20:20:21 -04:00
Miguel Ángel d83a873986 fix(producer): normalize error messages to prevent [object Object] in telemetry (#1099)
* fix(producer): normalize error messages to prevent [object Object] in telemetry

When a render fails and the caught value is a plain object (not an Error
instance), String(error) produces [object Object], masking the real error
in PostHog telemetry (~24 errors/day).

Add normalizeErrorMessage() that tries Error.message, string passthrough,
.message on plain objects, JSON.stringify, and String() as a last resort.
Apply it on the two telemetry-feeding paths: the main render failure
handler (renderOrchestrator.ts:2099) and buildRenderErrorDetails
(cleanup.ts), plus the error classifier isRecoverableParallelCaptureError
so timeout detection works even when the thrown value is a plain object.

* fix: address review — normalize CLI telemetry path, captureCost fallback

* fix: use local normalizeErrorMessage in CLI to avoid cross-package resolution

The Vite test runner can't resolve runtime imports from @hyperframes/producer
since its exports point to dist/. Copy the utility into the CLI package and
import locally instead.
2026-05-27 20:19:53 -04:00
Miguel Ángel 2d7b9e5245 fix(core): guard timeline method calls for non-conformant objects (#1098)
* fix(core): guard timeline method calls for non-conformant objects

User compositions can register timeline-like objects on window.__timeline
where .duration is a number property (not a function) and .pause/.play
may be missing entirely. The runtime player called these unconditionally,
causing ~166 "duration is not a function" and ~38 "pause is not a function"
errors per day.

Add safeNum() and safeVoid() helpers that check typeof before calling,
falling back to reading numbers as properties and silently skipping
missing void methods. Applied consistently across all timeline method
call sites in player.ts.

* fix(core): add observability for non-conformant timeline properties
2026-05-27 20:16:38 -04:00
James 23717a9911 docs(readme): clarify HyperFrames positioning 2026-05-27 16:29:34 -04:00
CypherPoet cbb7831eb2 fix(release): drop stale version field from marketplace.json (#1093)
## Summary

The `plugins[].version` in `.claude-plugin/marketplace.json` was a second source of truth that `scripts/set-version.ts` never touches, so it stayed frozen at `0.1.0` while each `plugin.json` advanced every release. Per [Claude's plugin marketplace docs](https://code.claude.com/docs/en/plugin-marketplaces#version-resolution-and-release-channels), `plugin.json`'s `version` is always read regardless, making the marketplace registration's version redundant.

This removes it so `plugin.json` is the single authoritative source and nothing can drift.

Follows up on review feedback in #1051 (per [this comment](https://github.com/heygen-com/hyperframes/pull/1051#issuecomment-4554571759)).

## Changes

- **`.claude-plugin/marketplace.json`**: remove the `"version": "0.1.0"` line from the single plugin registration. No other fields change.

## Verification

- `jq` confirms the file is still valid JSON and `.plugins[0]` no longer has a `version` key.
- `bunx oxfmt --check .claude-plugin/marketplace.json` passes.

## Out-of-scope note

While here I noticed the `v0.6.49` release commit (`7ea4d1c1`) bumped only the eight `package.json` files — the three `plugin.json` manifests are still at `0.6.48`. PR #1051's `set-version.ts` loop should have bumped them, so the `0.6.49` release may not have been cut with `set-version.ts`. Flagging separately; not addressed here.
2026-05-27 13:48:54 -04:00
Miguel Ángel d8ce2e4b50 chore: release v0.6.51 v0.6.51 2026-05-27 12:15:38 -04:00
Miguel Ángel f38eaf409a fix(studio): compensate GSAP translate when starting manual drag (#1095)
* fix(studio): compensate GSAP translate when starting manual drag

When an element has an active GSAP transform with translate (x/y),
starting a drag via createManualOffsetDragMember would strip the
GSAP translate from element.style.transform during the probe phase
without accounting for it in the initial offset. This caused the
persisted manual offset to be wrong by exactly the GSAP translate
amount, producing a visible position shift after page reload.

Read the GSAP translate contribution (m41/m42 from the transform
matrix) and fold it into initialOffset before the probe runs. The
offset now compensates for the stripped translate, so the element's
visual position is preserved across the drag start, commit, and
subsequent reloads.

* fix(studio): show visual position in Layout panel and fix save-reload race

PropertyPanel: X/Y fields now display the visual position (manual offset
+ GSAP translate) instead of the raw CSS var offset. Editing a value
reverses the compensation so the correct raw offset is persisted. This
matches what the user sees in the preview during GSAP playback.

persistDomEditOperations: move domEditSaveTimestampRef update before the
patch API call. The server writes the file and emits an SSE file-change
event during the fetch — if the event arrived before the response, the
file watcher would trigger a spurious reloadPreview(), resetting
playback to t=0. Setting the timestamp upfront suppresses that race.

* fix(studio): apply same timestamp race fix to element delete, relocate helper

Move readGsapTranslateFromTransform to manualEditsDom.ts alongside its
sibling stripGsapTranslateFromTransform and re-export through the
manualEdits barrel. PropertyPanel and manualOffsetDrag now import from
the shared location instead of the drag module owning a display concern.

Move domEditSaveTimestampRef update before the remove-element fetch in
handleDomEditElementDelete — same SSE race as persistDomEditOperations.
2026-05-27 12:14:45 -04:00
Miguel Ángel 5cd4db07e3 chore: release v0.6.50 v0.6.50 2026-05-27 15:28:56 +00:00
Miguel Ángel 3bbfea38cf fix(engine): use captureBeyondViewport on all CDP screenshot paths (#1094)
* fix(engine): use captureBeyondViewport on all CDP screenshot paths

Chrome's compositor rounds the viewport boundary inward under multi-tab
load, clipping the bottom/right edge of tall portrait compositions
(1080x1920). The explicit clip rect already constrains output to exact
composition dimensions, making the viewport-boundary pre-clip from
captureBeyondViewport:false both redundant and unreliable.

Set captureBeyondViewport:true on all three CDP screenshot call sites:
pageScreenshotCapture, captureScreenshotWithAlpha, and captureAlphaPng.

Add portrait-edge-bleed regression test: 1080x1920 grid with bright
magenta bottom rows, rendered with 4 workers. Any compositor clipping
at the bottom edge drops PSNR sharply against the golden baseline.

Closes #1009

* fix(engine): address review feedback on captureBeyondViewport

- Add backref comments on captureScreenshotWithAlpha and captureAlphaPng
  pointing to pageScreenshotCapture for the rationale, so the next reader
  doesn't treat the flag as unintentional copy-paste
- Note in test meta.json that the static grid fixture covers the
  capture-side clipping path but not the video-element compositor surface
  timing that produces the t≈37s self-healing in #1009

* test(producer): use video element in portrait-edge-bleed regression test

Replace the static CSS grid with a 1080x1920 portrait video element —
matches the original bug report shape where the compositor surface
allocation timing causes the bottom-edge clipping. The video has a dark
top region and bright magenta bottom 480px, so any viewport clipping at
the bottom edge drops PSNR sharply. Baseline regenerated in Docker with
4 workers.
2026-05-27 11:26:38 -04:00
Miguel Ángel 7ea4d1c131 chore: release v0.6.49 v0.6.49 2026-05-27 01:45:45 -04:00
Miguel Ángel f19d6fd471 feat: CLI observability + fix studio save failures on JS-created elements (#1091)
* feat(core): add probeElementInSource for source-existence checks

* feat(core): add probe-element endpoint for source-existence checks

* feat(studio): gate editing capabilities on source existence

* fix(studio): enrich save_failure telemetry with target details

* feat(studio): async selection resolution with source probe

Make `resolveDomEditSelection` async and wire a `probeSourceElement` call
into the selection path so elements generated by scripts (not present in the
source HTML) are detected early and have all edit capabilities disabled with
a clear reason message ("This element is generated by a script and cannot be
edited visually.").

Part A – core probe logic:
- `domEditingLayers.ts`: `resolveDomEditSelection` is now async; calls
  `probeSourceElement` (POST /api/projects/:id/file-mutations/probe-element/:file)
  when `projectId` is supplied and the element has a stable id/selector.
  `existsInSource: false` flows into `resolveDomEditCapabilities`, which
  disables all write capabilities with the appropriate reason.
- `domEditingLayers.ts`: `refreshDomEditSelection` promoted to async.
- `files.ts`: new `probe-element` route; extracted `resolveProjectPath`,
  `resolveFileMutationContext`, `writeIfChanged`, and `parseMutationBody`
  helpers to eliminate repeated boilerplate across remove/patch/probe handlers.

Part B – caller propagation (all eight consumer sites):
- `useDomSelection.ts`: `buildDomSelectionFromTarget`,
  `resolveDomSelectionFromPreviewPoint`,
  `buildDomSelectionForTimelineElement`, `handleTimelineElementSelect`,
  `refreshDomEditSelectionFromPreview`, and
  `refreshDomEditGroupSelectionsFromPreview` all made async; `projectId`
  forwarded into `resolveDomEditSelection`.
- `useDomEditCommits.ts`, `useDomEditTextCommits.ts`: updated
  `buildDomSelectionFromTarget` parameter type; added `await` at call sites.
- `useDomEditSession.ts`: inner `syncSelectionFromDocument` made async; fire
  with `void` to satisfy the surrounding effect.
- `usePreviewInteraction.ts`: `handlePreviewCanvasMouseDown` and
  `handlePreviewCanvasPointerMove` made async (React ignores handler return
  values, so this is safe).
- `useStudioUrlState.ts`: deferred `buildDomSelectionFromTarget` call
  converted to `.then()` chain with `void` prefix so the effect stays sync.
- `LayersPanel.tsx`: `seekToLayer`, `handleSelectLayer`, and
  `handleLayerHover` made async.
- `DomEditOverlay.tsx` / `useDomEditOverlayGestures.ts`: `onCanvasPointerMove`
  return type widened to `Promise<DomEditSelection | null>`; pointer-down
  handler falls back to `hoverSelectionRef.current` (always populated by a
  prior hover) instead of awaiting the async move callback inline.

Part C – test and tooling fixes:
- `lefthook.yml`: filesize hook shell loop explicitly skips `*.test.ts/tsx`
  files as a guard against a lefthook v2.1.6 bug where `exclude` patterns are
  not applied to `{staged_files}` in shell scripts.
- `domEditing.test.ts`: all `it()` blocks calling `resolveDomEditSelection`
  made async with `await`.
- `DomEditOverlay.test.ts`: mock updated to return `Promise.resolve(selection)`
  and `hoverSelection` pre-seeded so pointer-down test works with the new
  hover-first path.
- `studioUrlState.test.ts`: `buildDomSelectionFromTarget` mocks wrapped in
  `Promise.resolve()`; seek/selection hydration test made async with
  `await act(async () => { await Promise.resolve(); })` to flush microtasks.

* feat(cli): add global error handlers for crash telemetry

Register process-level uncaughtException and unhandledRejection handlers
that fire trackCliError so unhandled crashes are captured in telemetry.
Add the trackCliError function to events.ts and re-export it from the
telemetry barrel.

* feat(cli): track per-command success/failure and duration

* test(core): add integration test for JS-created element probe scenario

* fix: address PR review feedback

- uncaughtException handler now calls process.exit(1) after flushing
- cli_command_result uses real exit code from process "exit" event
- drop stack_trace from cli_error (contains filesystem paths)
- skip source probe during hover — only probe on click/selection
- format .fallowrc.jsonc

* fix(cli): restore stack_trace in cli_error telemetry

* fix(cli): use captured module refs in exit handlers instead of dead import()
2026-05-27 01:44:31 -04:00
Carlos Alcaraz GregorandCarlos Alcaraz 8ecef4b939 fix(studio): make static-seek adapter honor keepPlaying option (#1089)
createStaticSeekPlaybackAdapter.seek now accepts the same options as the
PlaybackAdapter contract and aligns the default-pause semantics with
wrapTimeline (hardened in 3e7b464b). Without keepPlaying the adapter
clears its `playing` flag and cancels the RAF ticker, so on non-GSAP
compositions a scrub during playback no longer leaves the iframe
silently advancing while the public seek wrapper marks isPlaying=false.

Follow-up to #863 review: jrusso called out the type drift and invited
a separate PR; this also closes the asymmetry with wrapTimeline.

Co-authored-by: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com>
2026-05-27 00:50:30 -04:00
Miguel Ángel 7cde0d9554 chore: release v0.6.48 v0.6.48 2026-05-26 23:46:36 -04:00
Miguel Ángel 3a24aed9bc fix(studio): fit preview reset to composition dimensions (#1085)
* fix(studio): fit preview reset to composition dimensions

* fix(core): keep runtime root resolution explicit

* fix(studio): resume playback after keep-playing seek
2026-05-26 23:44:39 -04:00