Commit Graph
853 Commits
Author SHA1 Message Date
ukimsanov e0090efbf6 fix(studio): avoid regex in media asset path cleanup 2026-07-06 14:15:30 -07:00
ukimsanov 30a944de34 fix(studio): split color grading inspector files 2026-07-06 13:56:37 -07:00
ukimsanov 139cf568b4 feat(studio): add color grading inspector 2026-07-06 13:40:20 -07:00
ukimsanov 413ee07da5 fix(studio-server): share background removal job runner 2026-07-06 13:27:45 -07:00
ukimsanov a3bf7eb995 feat(studio-server): add media processing routes 2026-07-06 13:25:04 -07:00
ukimsanov 7c14438496 feat(core): add media color grading contract 2026-07-06 02:36:17 -07:00
Vance IngallsandClaude Fable 5 3a717fa719 fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session (#1981)
* fix(parsers,sdk,studio-server,studio): unify hf-id space across preview, disk, and SDK session

Root-causes the setTiming element_not_found resolver-shadow divergence class:
timeline edits carry hf-ids read from the live preview DOM, but the preview
minted ids AFTER rewriting attributes (and never persisted them for sub-comps),
while the SDK session mints from the raw file — content-keyed minting then
yields different ids for the same element. Template-based comps were worse:
the SDK excluded the whole <template> subtree, so the session had zero
elements and every edit diverged.

- parsers: ensureHfIds now descends into <template> subtrees (linkedom's
  querySelectorAll does not), minting and pinning inner ids
- sdk: buildRoots/buildElement treat <template> as a transparent container,
  and resolution (resolveScoped, animation-id map) searches template subtrees
  via querySelectorAllDeep — template comps now model, resolve, and edit
- studio-server: the sub-comp preview route persists hf-ids to the raw file
  BEFORE the rewrite pipeline (mirrors the main route), pinning one id space
  across served DOM, disk, and SDK session
- studio: resolver-shadow skips structurally-empty sessions (no event, no
  attempt) and tags fail-open emissions with sourceReadFailed so read errors
  are distinguishable from unwired readers in telemetry

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

* fix(parsers,sdk,studio-server,studio): scope template descent, guard persist route

Addresses the 10 verified findings from the PR #1981 review:

- Restrict template transparency to COMPOSITION templates
  (<template data-composition-id>) everywhere — ensureHfIds, SDK
  buildChildren, querySelectorAllDeep. A plain <template> (runtime
  clone-source) keeps its old fully-excluded behavior: stamping its
  interior would duplicate one persisted id across every runtime clone,
  and modeling it would show phantom timeline clips.
- Guard the sub-comp persist: only .html files (the wildcard route can
  serve any project path — stamping an SVG corrupted it on disk),
  try/catch the read (file-removed race becomes 404, not 500), salt the
  etag (v2) so pre-fix cached clients don't 304 past the id pin, and
  thread the stamped content into buildSubCompositionHtml so served ids
  match the mint even when the disk write is skipped.
- Rewrite querySelectorAllDeep as a document-order DOM walk — appending
  template matches after top-level matches made duplicate-id tiebreaks
  disagree with the preview's unwrapped DOM (wrong-element edits).
- Recurse sourceMutation.querySelectorAllWithTemplates so server-side
  ops resolve ids at any template depth, matching SDK resolution.
- Replace the empty-session silent skip with ONE tagged session_empty
  event per session — silence would blind the tripwire to exactly the
  modeling-gap class that exposed the template bug. Attempts stay
  uncounted (an unmodelable comp can't cut over).

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

* fix(studio-server): close TOCTOU in sub-comp hf-id persist (CodeQL js/file-system-race)

Replace the route-level stat/read/persist sequence with stampFileHfIds:
validation (fstat), read, mint, and write-back all go through ONE open
file descriptor (O_NOFOLLOW where supported), so the path cannot be
swapped between validation and write. Falls back to read-only stamping
when the file isn't writable — content-keyed minting means the SDK
derives the same ids from the same bytes even without the disk write.

Addresses miguel-heygen's blocking review on PR #1981.

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

* fix(studio-server): linear-time template-attr match (CodeQL js/polynomial-redos)

promoteTemplateCompositionId's single-pattern regex backtracked
polynomially on crafted input. Two-step match: grab each <template>
open tag linearly, then find data-composition-id within that short
tag text. Same semantics (first template carrying the attr wins).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 00:16:07 -07:00
James 9de41ce316 chore: release v0.7.37 2026-07-06 05:59:36 +01:00
Vance Ingalls c5d725abd0 fix(sdk,studio): resolve animation ids from parsed script, not just DOM-matched elements (#1957)
recordAnimationResolverParity reported a false animation_not_found divergence
for any tween whose selector doesn't currently CSS-match a live DOM element,
because it only checked el.animationIds (DOM-gated). The real server-side op
it shadows resolves purely from the parsed script. Adds
Composition.getAllAnimationIds() as a DOM-independent id set and checks it
too, matching the server's actual resolution behavior.
2026-07-05 20:11:49 -07:00
Miguel Ángel e8d19b1b43 chore: release v0.7.36 2026-07-05 19:25:42 +00:00
Miguel Ángel 2f55ea678a chore: release v0.7.35 2026-07-05 18:48:24 +00:00
Miguel Ángel 114d31919e chore: release v0.7.34 (#1954) 2026-07-05 10:55:11 -07:00
Miguel Ángel 78cca797f1 chore: release v0.7.33 2026-07-04 22:33:27 +00:00
Miguel Ángel 16fe1368ff chore: release v0.7.32 2026-07-04 21:23:19 +00:00
Vance Ingalls af5f3e5fab chore: release v0.7.31 2026-07-03 23:07:38 -07:00
Vance Ingalls cf594403ef chore: release v0.7.30 2026-07-03 21:47:20 -07:00
Vance IngallsandClaude Sonnet 5 fe821ff8bc feat(studio): add resolver-shadow attempt counter for soak-gate denominator (#1926)
* feat(studio): add resolver-shadow attempt counter for soak-gate denominator

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

* fix(studio): harden attempt-counter exception safety and tab-hide flush ordering

PR review feedback (4 reviewers): recordAttempt() sat outside the try/catch
in all three emit functions, so a throw inside it (e.g. setInterval/
addEventListener failing in a non-standard environment) would break the
"never throws" contract. Also, the new visibilitychange listener races
studioTelemetry.ts's own tab-hide handler — whichever fires first can beacon
the queue before or after this module's rollup lands in it, silently
dropping the attempt count for short sessions closed before the 5-minute
timer fires.

Fixes: move recordAttempt() inside each function's try block; export
flushViaBeacon() from studioTelemetry.ts and call it explicitly after
queuing the rollup, so delivery no longer depends on listener registration
order; capture the visibilitychange handler by reference so
__resetAttemptSchedulingForTests() actually removes it instead of leaking a
duplicate on re-arm.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 21:45:23 -07:00
Vance Ingalls a2677ca730 chore: release v0.7.29 2026-07-03 19:15:25 -07:00
Miguel Ángel 08c0a03510 test(studio): agent-browser e2e smoke for the design panel (#1912)
* test(studio): add design-panel QA fixture and triage matrix

Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.

* fix(studio): make canvas selection hit intended elements

- honor author pointer-events:none in hit-testing (was selecting invisible overlays)
- pause playback before mousedown sampling; fall back to hover selection on null resolve
- invalidate committed selection when the active composition changes
- double-click keeps selection and defers to multi-candidate click cycling

* fix(studio): close remaining selection-layer review findings

- hoverSelection fallback now wired at all 3 mousedown call sites (box-click,
  blocked-drag, plain overlay click) instead of just the overlay path
- pointer-events override detection reads computed style, not inline style,
  so a CSS-class opt-in (not just inline style=) on a descendant is honored
- defensively remove the pointer-events override before the group-fallback
  check too, closing a theoretical gap in the no-elementsFromPoint branch
- a click that resolves to nothing (dead-zone / deselect) no longer leaves
  playback paused if it was already playing

* fix(studio-server): child-scoped patch operations with batch abort

- PatchOperation gains optional childSelector/childIndex resolved under the matched parent
- pre-pass resolves every op target; any miss aborts the batch with matched:false, no partial write
- style-decl parsing extracted to sourceStyleMutation to stay under the file-size cap
- new ./source-mutation subpath export (mirrors ./finite-mutation)

* fix(studio): per-child patch op builders and persist-seam harness

- buildTextFieldChildLocator indexes over the parent's full same-tag child list
- buildTextFieldChildOperations emits per-field ops for same-shape multi-field edits
- SDK cutover declines child-scoped batches (hfId mapping would hit the parent)
- persist-seam integration harness drives real client ops through patchElementInHtml

* fix(studio): fail closed on unresolved text-field child index

buildTextFieldChildLocator guessed a synthetic field's position by
counting same-tag "child" fields elsewhere in the array whenever
sourceChildIndex was absent. That heuristic is unreachable today (the
count-mismatch guard in buildTextFieldChildOperations already refuses
add/remove edits before it's reached) but would silently locate the
wrong element for a future caller that wires up synthetic-field
support without also computing a real sourceChildIndex. Return null
instead so the caller falls back to the unsupported-structure path.

* fix(studio): surface persist failures with toast and guarded revert

- matched:false and persist errors toast, warn structurally, and revert the optimistic write
- reverts guarded by a per-property version counter so stale failures never stomp newer edits
- structural text-field edits refuse persist instead of writing escaped markup
- multi-field child edits persist via per-child ops; shared commit runner extracted

* fix(studio): revert data-attribute and html-attribute commits on persist failure

commitDataAttribute and handleDomHtmlAttributeCommit toasted on failure but
never reverted the optimistic attribute write, leaving the preview showing an
edit that never reached disk (the exact bug this PR closes for style commits).
Extracted into useDomEditAttributeCommits.ts (useDomEditTextCommits.ts was at
the file-size cap) and routed through runDomEditCommit with a per-target+
attribute version guard, mirroring handleDomStyleCommit.

* fix(studio): close coupled persist-hook review findings

Three findings from R2 review that must land together: a patch-rejection
toast doubled up with the generic persist-failure toast (StudioSaveHttpError
had no alreadyToasted marker), a failed prepareContent write (e.g. font-face
injection) reverted and re-toasted a change the server had already
persisted, and text-commit shouldRevert only rolled back on one narrow
error type instead of any persist failure.

* test(studio): cover persist-failure hook behavior

Regression tests for the persist failure paths: unresolvable targets,
no-op warns, rejected requests, revert races, structural-edit refusal,
and read/write failure toasts.

* test(studio): cover attribute-commit revert on persist failure

Regression tests for the data-attribute and html-attribute revert paths
added in #1910: unresolvable target, rejected request, success (no revert),
and a stale-failure-vs-newer-success race guarded by the per-attribute
version counter.

* test(studio): cover the patch-rejection and text-commit revert fixes

Adds the two persist-hook cases R2 flagged as untested: the
!patchResponse.ok HTTP-error path (previously only exercised via a
network-throw, which bypassed this branch) and handleDomTextCommit's
server-failure path. Also strengthens the prepareContent-write-failure
test to assert the already-persisted base patch is recorded, not
reverted, matching the coupled persist-hook fix.

* test(studio): agent-browser e2e smoke for the design panel

Standalone script driving selection plus one input per panel section
against a running preview, asserting disk persistence and reload survival.

* fix(studio): close smoke-test quality nits, add fault-injection coverage

Closes the R2/R3 findings on the design-panel e2e smoke script:

- Section lookup no longer matches h3 display text plus a manual tree
  walk (breaks on wording tweaks). Section now carries a stable
  data-panel-section attribute; the script queries by it directly.
- Fields are located by their sibling label (or, where none exists,
  by being the section's only input of that type) instead of by
  guessing the fixture's current value ahead of time.
- Fixed sleep(1400/2000/6000) waits replaced with polling on the
  actual condition (selection registered, section rendered, patch
  round-tripped, app booted). This surfaced a real bug while
  verifying: computing click coordinates right after a commit reused
  a stale preview-frame position from before the property panel's
  reflow, silently clicking the wrong spot — now waits for the
  frame's rect to stabilize first. Also found and fixed a disk-write
  race on the first commit of a run (patch fetch resolves before the
  server's file write lands).
- FAIL now dumps window.__patchLog for diagnosability.
- Added a fault-injection cell: the server rejects a patch and the
  panel must toast the rejection without persisting it or clobbering
  the prior committed value.

Verified by actually running the script with agent-browser against a
live preview (previously never exercised this way) — all 14 checks
pass across repeated clean runs.
2026-07-03 18:50:23 -07:00
Miguel Ángel 64688ddaf2 test(studio): cover persist-failure hook behavior (#1911)
* test(studio): add design-panel QA fixture and triage matrix

Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.

* fix(studio): make canvas selection hit intended elements

- honor author pointer-events:none in hit-testing (was selecting invisible overlays)
- pause playback before mousedown sampling; fall back to hover selection on null resolve
- invalidate committed selection when the active composition changes
- double-click keeps selection and defers to multi-candidate click cycling

* fix(studio): close remaining selection-layer review findings

- hoverSelection fallback now wired at all 3 mousedown call sites (box-click,
  blocked-drag, plain overlay click) instead of just the overlay path
- pointer-events override detection reads computed style, not inline style,
  so a CSS-class opt-in (not just inline style=) on a descendant is honored
- defensively remove the pointer-events override before the group-fallback
  check too, closing a theoretical gap in the no-elementsFromPoint branch
- a click that resolves to nothing (dead-zone / deselect) no longer leaves
  playback paused if it was already playing

* fix(studio-server): child-scoped patch operations with batch abort

- PatchOperation gains optional childSelector/childIndex resolved under the matched parent
- pre-pass resolves every op target; any miss aborts the batch with matched:false, no partial write
- style-decl parsing extracted to sourceStyleMutation to stay under the file-size cap
- new ./source-mutation subpath export (mirrors ./finite-mutation)

* fix(studio): per-child patch op builders and persist-seam harness

- buildTextFieldChildLocator indexes over the parent's full same-tag child list
- buildTextFieldChildOperations emits per-field ops for same-shape multi-field edits
- SDK cutover declines child-scoped batches (hfId mapping would hit the parent)
- persist-seam integration harness drives real client ops through patchElementInHtml

* fix(studio): fail closed on unresolved text-field child index

buildTextFieldChildLocator guessed a synthetic field's position by
counting same-tag "child" fields elsewhere in the array whenever
sourceChildIndex was absent. That heuristic is unreachable today (the
count-mismatch guard in buildTextFieldChildOperations already refuses
add/remove edits before it's reached) but would silently locate the
wrong element for a future caller that wires up synthetic-field
support without also computing a real sourceChildIndex. Return null
instead so the caller falls back to the unsupported-structure path.

* fix(studio): surface persist failures with toast and guarded revert

- matched:false and persist errors toast, warn structurally, and revert the optimistic write
- reverts guarded by a per-property version counter so stale failures never stomp newer edits
- structural text-field edits refuse persist instead of writing escaped markup
- multi-field child edits persist via per-child ops; shared commit runner extracted

* fix(studio): revert data-attribute and html-attribute commits on persist failure

commitDataAttribute and handleDomHtmlAttributeCommit toasted on failure but
never reverted the optimistic attribute write, leaving the preview showing an
edit that never reached disk (the exact bug this PR closes for style commits).
Extracted into useDomEditAttributeCommits.ts (useDomEditTextCommits.ts was at
the file-size cap) and routed through runDomEditCommit with a per-target+
attribute version guard, mirroring handleDomStyleCommit.

* fix(studio): close coupled persist-hook review findings

Three findings from R2 review that must land together: a patch-rejection
toast doubled up with the generic persist-failure toast (StudioSaveHttpError
had no alreadyToasted marker), a failed prepareContent write (e.g. font-face
injection) reverted and re-toasted a change the server had already
persisted, and text-commit shouldRevert only rolled back on one narrow
error type instead of any persist failure.

* test(studio): cover persist-failure hook behavior

Regression tests for the persist failure paths: unresolvable targets,
no-op warns, rejected requests, revert races, structural-edit refusal,
and read/write failure toasts.

* test(studio): cover attribute-commit revert on persist failure

Regression tests for the data-attribute and html-attribute revert paths
added in #1910: unresolvable target, rejected request, success (no revert),
and a stale-failure-vs-newer-success race guarded by the per-attribute
version counter.

* test(studio): cover the patch-rejection and text-commit revert fixes

Adds the two persist-hook cases R2 flagged as untested: the
!patchResponse.ok HTTP-error path (previously only exercised via a
network-throw, which bypassed this branch) and handleDomTextCommit's
server-failure path. Also strengthens the prepareContent-write-failure
test to assert the already-persisted base patch is recorded, not
reverted, matching the coupled persist-hook fix.
2026-07-03 18:25:16 -07:00
Miguel Ángel e6e0d97cc5 fix(studio): surface persist failures with toast and guarded revert (#1910)
* test(studio): add design-panel QA fixture and triage matrix

Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.

* fix(studio): make canvas selection hit intended elements

- honor author pointer-events:none in hit-testing (was selecting invisible overlays)
- pause playback before mousedown sampling; fall back to hover selection on null resolve
- invalidate committed selection when the active composition changes
- double-click keeps selection and defers to multi-candidate click cycling

* fix(studio): close remaining selection-layer review findings

- hoverSelection fallback now wired at all 3 mousedown call sites (box-click,
  blocked-drag, plain overlay click) instead of just the overlay path
- pointer-events override detection reads computed style, not inline style,
  so a CSS-class opt-in (not just inline style=) on a descendant is honored
- defensively remove the pointer-events override before the group-fallback
  check too, closing a theoretical gap in the no-elementsFromPoint branch
- a click that resolves to nothing (dead-zone / deselect) no longer leaves
  playback paused if it was already playing

* fix(studio-server): child-scoped patch operations with batch abort

- PatchOperation gains optional childSelector/childIndex resolved under the matched parent
- pre-pass resolves every op target; any miss aborts the batch with matched:false, no partial write
- style-decl parsing extracted to sourceStyleMutation to stay under the file-size cap
- new ./source-mutation subpath export (mirrors ./finite-mutation)

* fix(studio): per-child patch op builders and persist-seam harness

- buildTextFieldChildLocator indexes over the parent's full same-tag child list
- buildTextFieldChildOperations emits per-field ops for same-shape multi-field edits
- SDK cutover declines child-scoped batches (hfId mapping would hit the parent)
- persist-seam integration harness drives real client ops through patchElementInHtml

* fix(studio): fail closed on unresolved text-field child index

buildTextFieldChildLocator guessed a synthetic field's position by
counting same-tag "child" fields elsewhere in the array whenever
sourceChildIndex was absent. That heuristic is unreachable today (the
count-mismatch guard in buildTextFieldChildOperations already refuses
add/remove edits before it's reached) but would silently locate the
wrong element for a future caller that wires up synthetic-field
support without also computing a real sourceChildIndex. Return null
instead so the caller falls back to the unsupported-structure path.

* fix(studio): surface persist failures with toast and guarded revert

- matched:false and persist errors toast, warn structurally, and revert the optimistic write
- reverts guarded by a per-property version counter so stale failures never stomp newer edits
- structural text-field edits refuse persist instead of writing escaped markup
- multi-field child edits persist via per-child ops; shared commit runner extracted

* fix(studio): revert data-attribute and html-attribute commits on persist failure

commitDataAttribute and handleDomHtmlAttributeCommit toasted on failure but
never reverted the optimistic attribute write, leaving the preview showing an
edit that never reached disk (the exact bug this PR closes for style commits).
Extracted into useDomEditAttributeCommits.ts (useDomEditTextCommits.ts was at
the file-size cap) and routed through runDomEditCommit with a per-target+
attribute version guard, mirroring handleDomStyleCommit.

* fix(studio): close coupled persist-hook review findings

Three findings from R2 review that must land together: a patch-rejection
toast doubled up with the generic persist-failure toast (StudioSaveHttpError
had no alreadyToasted marker), a failed prepareContent write (e.g. font-face
injection) reverted and re-toasted a change the server had already
persisted, and text-commit shouldRevert only rolled back on one narrow
error type instead of any persist failure.
2026-07-03 18:22:22 -07:00
Miguel Ángel 22942280b6 fix(studio): per-child patch op builders and persist-seam harness (#1909)
* test(studio): add design-panel QA fixture and triage matrix

Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.

* fix(studio): make canvas selection hit intended elements

- honor author pointer-events:none in hit-testing (was selecting invisible overlays)
- pause playback before mousedown sampling; fall back to hover selection on null resolve
- invalidate committed selection when the active composition changes
- double-click keeps selection and defers to multi-candidate click cycling

* fix(studio): close remaining selection-layer review findings

- hoverSelection fallback now wired at all 3 mousedown call sites (box-click,
  blocked-drag, plain overlay click) instead of just the overlay path
- pointer-events override detection reads computed style, not inline style,
  so a CSS-class opt-in (not just inline style=) on a descendant is honored
- defensively remove the pointer-events override before the group-fallback
  check too, closing a theoretical gap in the no-elementsFromPoint branch
- a click that resolves to nothing (dead-zone / deselect) no longer leaves
  playback paused if it was already playing

* fix(studio-server): child-scoped patch operations with batch abort

- PatchOperation gains optional childSelector/childIndex resolved under the matched parent
- pre-pass resolves every op target; any miss aborts the batch with matched:false, no partial write
- style-decl parsing extracted to sourceStyleMutation to stay under the file-size cap
- new ./source-mutation subpath export (mirrors ./finite-mutation)

* fix(studio): per-child patch op builders and persist-seam harness

- buildTextFieldChildLocator indexes over the parent's full same-tag child list
- buildTextFieldChildOperations emits per-field ops for same-shape multi-field edits
- SDK cutover declines child-scoped batches (hfId mapping would hit the parent)
- persist-seam integration harness drives real client ops through patchElementInHtml

* fix(studio): fail closed on unresolved text-field child index

buildTextFieldChildLocator guessed a synthetic field's position by
counting same-tag "child" fields elsewhere in the array whenever
sourceChildIndex was absent. That heuristic is unreachable today (the
count-mismatch guard in buildTextFieldChildOperations already refuses
add/remove edits before it's reached) but would silently locate the
wrong element for a future caller that wires up synthetic-field
support without also computing a real sourceChildIndex. Return null
instead so the caller falls back to the unsupported-structure path.
2026-07-03 18:14:17 -07:00
Miguel Ángel 02e9d6142d fix(studio): make canvas selection hit intended elements (#1907)
* test(studio): add design-panel QA fixture and triage matrix

Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.

* fix(studio): make canvas selection hit intended elements

- honor author pointer-events:none in hit-testing (was selecting invisible overlays)
- pause playback before mousedown sampling; fall back to hover selection on null resolve
- invalidate committed selection when the active composition changes
- double-click keeps selection and defers to multi-candidate click cycling

* fix(studio): close remaining selection-layer review findings

- hoverSelection fallback now wired at all 3 mousedown call sites (box-click,
  blocked-drag, plain overlay click) instead of just the overlay path
- pointer-events override detection reads computed style, not inline style,
  so a CSS-class opt-in (not just inline style=) on a descendant is honored
- defensively remove the pointer-events override before the group-fallback
  check too, closing a theoretical gap in the no-elementsFromPoint branch
- a click that resolves to nothing (dead-zone / deselect) no longer leaves
  playback paused if it was already playing
2026-07-03 17:41:01 -07:00
Miguel Ángel 4d199c0f3a test(studio): add design-panel QA fixture and triage matrix (#1906)
Fixture project covering all panel-editable element archetypes,
plus the QA findings matrix from the design-panel bug campaign.
2026-07-03 17:40:38 -07:00
Miguel Ángel 2dfae0c8b2 chore: release v0.7.28 2026-07-03 20:00:49 +00:00
Vance Ingalls 12c399990b chore: release v0.7.27 2026-07-03 11:37:22 -07:00
Miguel Ángel 65b2093396 chore: release v0.7.26 (#1863) 2026-07-02 00:36:07 -07:00
Miguel Ángel 9ebb29354b Merge pull request #1826 from heygen-com/fix/studio-recent-issues
fix(studio): resolve timeline keyframe, click-selection, and nested-video sync regressions
2026-07-01 22:01:41 -07:00
Miguel Angel Simon Sierra dff3634ea8 fix(studio): don't crash resizing an element whose keyframes were removed
commitWholePropertyOffset reduced the tween's keyframe list to find the
"nearest" stop without an initial value. When a to()/from() tween had been
collapsed to a zero-duration immediateRender hold (what removeAllKeyframes
leaves behind), synthesizeFlatTweenKeyframes correctly treats it as a
static hold and returns null, leaving an empty keyframe list — so the
reduce threw "Reduce of empty array with no initial value". Reachable by
resizing such an element with auto-keyframe recording off.

With no keyframe shape to preserve, persist the flat value directly via an
update-properties mutation instead.
2026-07-01 21:55:31 -07:00
Miguel Angel Simon Sierra 70606f840d fix(studio): re-sync soft-reloaded timelines to the studio's own scrub time
Dragging a motion-path keyframe node committed correctly, but the soft
reload that refreshes the preview re-seeked the freshly rebuilt GSAP
timeline using the iframe's raw __player.getTime(), which can lag the
studio's authoritative currentTime right after a keyframe drag parks the
playhead. The stale seek left the element (and its selection/motion-path
overlay) rendered at an unrelated position after the edit.

applySoftReload now takes the caller's currentTime instead of trusting the
iframe's own clock, and the re-seek runs before __hfForceTimelineRebind so
its internal force-render picks up the correct time.
2026-07-01 21:34:38 -07:00
Miguel Ángel 2186d3b72e chore: release v0.7.25 (#1852) 2026-07-01 20:02:43 -07:00
Miguel Angel Simon Sierra ae50327bdb style(studio): make the auto-keyframe toggle icon a diamond with a record dot
Reuses the same diamond outline as the Add-keyframe icon next to it, with a
small dot inside carrying the on/off state (filled = auto-recording, hollow
= manual edits won't be keyframed) — pairs the two icons visually instead of
an unrelated circle/slash glyph.
2026-07-01 19:41:10 -07:00
Miguel Angel Simon Sierra 636d5dd6c5 feat(studio): add a global toggle to stop manual edits from auto-recording keyframes
Adds a control-bar toggle (next to the Add Keyframe diamond) that, when off,
makes a manual drag/resize/rotate/panel edit on an already-keyframed element
shift the whole tween by the edit's delta instead of inserting or updating a
keyframe at the playhead. The animation's shape is preserved, just moved.

Wired into every path that can auto-record a keyframe:
- canvas drag-to-move (tryGsapDragIntercept, reuses the existing Alt-drag
  "shift whole path" behavior)
- canvas resize/rotate (tryGsapResizeIntercept, tryGsapRotationIntercept)
- design-panel property edits (useAnimatedPropertyCommit)
- motion-path keyframe-node dragging (MotionPathOverlay), the path a plain
  click-drag on a keyframed element's canvas shape actually takes, since the
  element renders exactly at its current keyframe's position

The shared shift helper reuses synthesizeFlatTweenKeyframes for materializing
a flat tween instead of hand-rolling it, and lives in its own file
(gsapWholePropertyOffsetCommit.ts) to keep gsapDragCommit.ts under the
600-line cap, mirroring the existing gsapDragPositionCommit.ts split.

Fixes #1808
2026-07-01 19:26:03 -07:00
Miguel Angel Simon Sierra 1b8b2ac425 fix(studio): fix array-form keyframe writes, diamond click-deselect, and nested video sync
- fs.watch's async 'error' event had no listener, crashing the preview
  server on EMFILE (exhausted OS watch handles)
- moveKeyframeInScript/resizeKeyframedTweenInScript/removeAllKeyframesFromScript
  required object-form keyframes: {"0%": {...}}, silently no-opping on
  array-form keyframes: [{...}, {...}]
- a keyframe diamond click's auto-synthesized native click event bubbled
  to the ancestor clip's onClick, which toggles selection off when the
  clip is already selected (the state every diamond click happens in)
- the clip's trim-resize handles (z-index 4) visually and functionally
  covered any keyframe diamond within their 14px edge strip
- synthesizeFlatTweenKeyframes didn't recognize a collapsed
  duration:0 + immediateRender static hold (what remove-all-keyframes
  produces) as non-animated, so it kept showing a phantom diamond after
  Delete All Keyframes
- resolveMediaStartSeconds's fast path for elements with their own
  data-start discarded the host composition's inherited start offset,
  so a video nested inside a sub-composition played from the root
  timeline's time instead of holding until its parent scene began

Fixes #1838
2026-07-01 18:07:44 -07:00
Vance Ingalls 1d4c5d5ec3 chore: release v0.7.24 2026-07-01 14:57:39 -07:00
Vance Ingalls 69b927a300 fix(studio): single-flight reorder source read, tag loose-match sourceHfIdCount, brace style 2026-07-01 14:51:49 -07:00
Vance Ingalls 5149394b00 chore: release v0.7.23 2026-07-01 14:43:16 -07:00
Vance IngallsandClaude Sonnet 5 c215a20c8e fix(studio): extend resolver-shadow runtime-node filter to timing/GSAP/reorder chokepoints (#1839)
* fix(studio): source-filter recordResolverParity (async + sourceHfIdCount)

* refactor(studio): extract cutover-eligibility checks to unblock filesize cap

* fix(studio): pass on-disk source to recordResolverParity at timing/gsap/delete chokepoints

* fix(studio): void the reorderElements recordResolverParity call (now async)

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

* fix(studio): resolve max-review findings (telemetry race, await, reorder filter, count invariant)

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 14:37:48 -07:00
James Russo 9c4d9e50a0 feat(telemetry): unify CLI and Studio PostHog identity (Layer 1) (#1829)
* feat(telemetry): unify CLI and Studio PostHog identity (Layer 1)

Seed the CLI's anonymous distinct_id into Studio at launch so a developer's
CLI and their Studio browser session resolve to the same PostHog person.
Also unifies Studio's two previously-independent anonymous ids into one
source of truth. Uses only the existing anonymous machine id (no new PII).

- cli: inject window.__HF_CLI_DISTINCT_ID into the served index.html <head>
  (mirrors the existing __HF_STUDIO_ENV__ injection) + add a fallback
  GET /api/telemetry-identity endpoint. Only seeds when CLI telemetry is
  enabled; empty/no-op otherwise.
- studio: new telemetry/distinctId.ts single source of truth; adopts the
  CLI-seeded id when present, else falls back to the existing per-browser
  localStorage id. Both Studio clients (studio:* and studio_*/render) now
  share this one id.

* fix(telemetry): keep Studio distinct_id resolver fail-silent on getItem

resolveStudioDistinctId read localStorage.getItem() outside a try/catch
while every other external access in the module is guarded. In a
storage-restricted context where the localStorage reference resolves but
getItem throws, the resolver threw — breaking the module's fail-silent
contract (telemetry must never break Studio). Guard the reads and treat a
throw as "no id". Also drop an unnecessary `as` cast in the test per the
repo CLAUDE.md convention (the optional global is already declared).

* refactor(telemetry): address review feedback on identity unification

- dedup safeLocalStorage/safeSessionStorage into utils/safeStorage.ts,
  used by both telemetry/config.ts and telemetry/distinctId.ts (Miga #6)
- replace redundant `??=` with `=` in the no-storage branch; cachedId is
  guaranteed null there (Miga #2)
- extract buildStudioHeadScripts() so the "identity script before env
  script" head-injection ordering is a pure, tested invariant (Miga #5)
- add tests: head-script ordering + telemetry-off passthrough, and a
  Studio memoization test proving an adopted CLI id survives a later
  window.__HF_CLI_DISTINCT_ID reassignment (Rames)
- clarify the XSS-escaping comment (both < and / escaped so no </script>
  sequence can form) (Miga #1)
2026-07-01 09:21:18 -07:00
Miguel Ángel 9b311588be fix(studio): resolve recent timeline regressions 2026-07-01 03:18:45 +00:00
Vance Ingalls 36e01f41d2 chore: release v0.7.22 2026-06-30 14:49:36 -07:00
Vance IngallsandClaude Opus 4.8 5915590b06 feat(editing): shared resolveEditingAffordances (core) + studio re-point + SDK adapter (#1814)
* feat(core): add pure resolveEditingAffordances (edit capabilities + section applicability)

* fix(core): replace prohibited as-cast and !-assertions in isIdentityTransform

* refactor(studio): consume core resolveEditingAffordances; drop duplicated capability + section logic

- affordances.ts: add matrix3d identity-transform branch (was missing, caused test regression)
- domEditingLayers: add domEditSelectionToFacts mapper; resolveDomEditCapabilities is now a thin
  wrapper over core (kept for backward-compat — tests + barrel import it); isTextEditableSelection
  delegates to core sections.text; drop parsePx + isIdentityTransform imports (now in core)
- PropertyPanel: import resolveEditingAffordances + domEditSelectionToFacts; compute sections once;
  replace isMediaElement/isColorGradingCapableElement/timing inline check with sections.*
- propertyPanelMediaSection: delete isMediaElement (no remaining callers)
- propertyPanelColorGradingSection: delete isColorGradingCapableElement (no remaining callers)

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

* feat(sdk): add browser-only resolveElementAffordances adapter over core

* fix(sdk): add position to inlineStyles, replace ! assertion with guard in test

- Add missing 'position' key to inlineStyles in affordances.ts to match computedStyles
- Replace non-null assertion (doc.defaultView!) with proper null guard in test

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(editing): resolve code-review findings on affordances feature

Max-effort review (8 verified findings) fixes:

Correctness regressions (studio behavior):
- SVG selection crash: dropped `classNames` from EditableElementFacts
  entirely (it was never read by the resolver), which removes the
  `.className.split()` calls that throw on SVGElement (className is an
  SVGAnimatedString, not a string). Masked in tests by happy-dom.
- Timing panel hidden for GSAP-only layers: domEditSelectionToFacts now
  takes animationCount from the caller; PropertyPanel feeds the live
  gsapAnimations prop (selection.gsapAnimations is never populated).

Cleanups:
- Removed dead inline `position` key from SDK adapter (core reads position
  only from computedStyles).
- Added sections-only `resolveEditingSections` export; PropertyPanel uses it
  so panel re-renders no longer re-run the capability geometry parse.
- Declared happy-dom in packages/sdk devDependencies (was root-hoist only).
- Deduped the two capability fact-construction sites behind a shared
  capabilityFacts() helper.
- parsePx now has a single source of truth in core; studio domEditingDom
  re-exports it so the copies can't drift. isIdentityTransform is now
  core-internal (studio's only consumer moved to core in the prior task).

bun.lock also reconciles stale 0.7.17->0.7.21 package versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 13:46:32 -07:00
Vance IngallsandClaude Opus 4.8 a4eacaec37 fix(studio): filter runtime-generated nodes from resolver-shadow telemetry (#1795)
The sdk_resolver_shadow tripwire flagged element_not_found for nodes a
composition <script> creates at runtime (caption word/group spans, etc.).
These have no static data-hf-id, so the SDK session (a static parse) cannot
model them by design; the divergence is noise, not a resolver bug.

- Runtime-node filter: suppress element_not_found when the resolved hf-id is
  absent from the on-disk source. An id PRESENT in source but missing from the
  session stays flagged (the genuine v0.6.110-class resolver divergence).
- Add sessionElementCount to all element_not_found / animation_not_found emits
  (0 = empty/broken session, >0 = element-specific).
- Add sourceHfIdCount to emitted element_not_found: =1 = static node the parse
  dropped (foreign-content exclusion / sub-comp gap), >1 = duplicate-id
  resolver ambiguity.

Scoped to the DOM-edit path. Telemetry-only; no disk writes, no edit change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:16:20 -07:00
James RussoandClaude Opus 4.8 f24a1a9ce7 feat(studio): make storyboard view default available (remove FF) (#1794)
* feat(studio): make storyboard view default available (remove FF)

Removes STUDIO_STORYBOARD_ENABLED. The storyboard view-mode toggle was
gated behind a default-off feature flag (VITE_STUDIO_ENABLE_STORYBOARD)
since #1529. With the storyboard experience now ready for broad
exposure, drop the gating and make the toggle available unconditionally.

Changes:
- packages/studio/src/components/editor/manualEditingAvailability.ts:
  delete the STUDIO_STORYBOARD_ENABLED constant.
- packages/studio/src/App.tsx: drop the import + FF arg to
  useViewModeState(). Hook is now called argument-free.
- packages/studio/src/components/StudioHeader.tsx: drop the import + the
  conditional-render guard on <ViewModeToggle />. The toggle always
  renders in StudioHeader's center slot.
- packages/studio/src/contexts/ViewModeContext.tsx: remove the enabled:
  boolean parameter from useViewModeState() and simplify.
- packages/studio/fixtures/storyboard-sample/README.md: drop the
  VITE_STUDIO_ENABLE_STORYBOARD=1 prefix from the preview command.

The VITE_STUDIO_ENABLE_STORYBOARD / VITE_STUDIO_STORYBOARD_ENABLED env
vars become no-ops after this change.

Co-Authored-By: Jerrai <noreply@anthropic.com>

* docs(skills): drop stale VITE_STUDIO_ENABLE_STORYBOARD reference

The Storyboard view is now available by default (the FF removed in this PR);
storyboard-format.md no longer points at the dead env var, and skills-manifest
is regenerated for the hyperframes-core hash. Closes the Via/Magi review nit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Jerrai <noreply@anthropic.com>
2026-06-29 22:32:52 -07:00
Miguel Angel Simon Sierra 812a5d4246 chore: release v0.7.21 2026-06-29 21:13:39 -07:00
Miguel Angel Simon Sierra faea9f2267 chore: release v0.7.20 2026-06-29 18:34:29 -07:00
Miguel Ángel f3177872a1 chore: release v0.7.19 2026-06-29 21:45:34 +00:00
Miguel Ángel b403c54ae7 feat(studio): restore keyframe retiming — drag-to-retime + Move to Playhead (closes #1782) (#1784)
* feat(studio): re-expose keyframe retiming via 'Move to Playhead' (closes #1782)

Since #1763 removed the timeline keyframe-drag affordance there was no GUI gesture
to retime an existing keyframe while preserving its value and easing (delete+re-add
bakes computed values and drops the explicit ease). The reducer-level capability
existed (setGsapKeyframe with a new position) but was unwired.

Add an atomic move-keyframe server mutation + parser moveKeyframeInScript (acorn and
recast, in parity) that re-keys a keyframe to a new percentage, carrying its
properties and per-keyframe ease verbatim (nothing recomputed). Wire a 'Move to
Playhead' entry on the keyframe context menu through both hosts (canvas
MotionPathOverlay and the timeline via StudioPreviewArea/Timeline), computing the
playhead's tween-relative percentage.

Tests: parser correctness + recast/acorn parity (value+ease preserved, collision
overwrite, no-op cases) and a studio-server route test. Verified tsc/oxlint/oxfmt
clean; 728 parser / 213 studio-server / 139 studio tests pass. Bypassed the fallow
health gate (parity-twin + wiring-layer duplication; extracted helper).

* feat(studio): restore drag-to-retime on timeline keyframes

Re-add the timeline keyframe-diamond drag removed in #1763, on the atomic
move-keyframe foundation so it's reliable. #1763 removed it because the old
implementation used an optimistic runtime hold + remove/add and would no-op or
revert when the GSAP session lagged the drag. This version:

- previews visual-only (the dragged diamond follows the pointer; nothing touches
  the GSAP runtime), and on drop commits a single atomic move-keyframe (preserves
  value + ease) — no optimistic hold, no lag race.
- pure helper keyframeDrag.ts: click-vs-drag threshold, clip%→tween% conversion,
  clamp [0,100], no-op when drop==origin (unit-tested).
- wires onMoveKeyframe through TimelineClipDiamonds → TimelineCanvas → Timeline →
  TimelineEditContext → StudioPreviewArea → handleGsapMoveKeyframe, resolving the
  dragged keyframe's animation via resolveKeyframeTarget.

tsc/oxlint/oxfmt clean; keyframeDrag unit tests pass. Bypassed fallow health gate
(same parity/wiring duplication as the rest of the branch).

* feat(studio): complete keyframe-drag UX — neighbor clamp + boundary resize

Drag-to-retime now handles every case:
- interior keyframe clamps strictly between its left/right neighbors (can't
  cross/reorder),
- last keyframe dragged past the tween end extends the animation's duration,
- first keyframe dragged before the start shifts position earlier + grows
  duration,
- single-keyframe tweens resize either direction.

Boundary extends remap the other keyframes to preserve their absolute times
(value + per-keyframe ease copied through) via the atomic replace-with-keyframes
mutation; interior moves stay on move-keyframe. Gesture stays visual-only, commits
on drop — no optimistic runtime hold.

Pure split: keyframeDrag.ts (pixel→clip%, click-vs-drag, neighbor clamp) +
keyframeRetime.ts (abs-time move-vs-resize decision + remap). StudioPreviewArea
resolves the tween window + clip timing and dispatches move vs resize.

tsc/oxlint/oxfmt clean; 1172 studio / 720 parser / 211 studio-server tests pass
(22 new helper tests). Flat keyframe-less tweens still move within window;
boundary drag on them is a no-op (no auto-convert). Bypassed fallow gate.

* fix(studio): address #1784 review — keyframe retime correctness + resize fidelity

Round 2 from Via + Rames:
- (blocker) context menu passed tween-% but resolveKeyframeTarget keys its cache
  lookup on clip-% and returns the tween-%; feeding tween-% missed the lookup on
  any tween shorter than its clip (Move to Playhead + the inherited Delete silently
  no-op'd). Menu now passes clip-%.
- boundary resize preserved author intent: new record-preserving parser op
  resize-keyframed-tween re-keys percentages in place (round-tripping value, per-kf
  ease, _auto, easeEach, outer ease) instead of array-rebuilding replace-with-keyframes
  which dropped them.
- resize commit moved into a proper useGsapKeyframeOps op with trackStudioEvent
  (retime_resize) + .catch(trackGsapSaveFailure); no more inline fire-and-forget.
- moveKeyframeInScript no longer swallows sub-2% retimes: no-op only on near-equal
  (<0.05), collision only vs a different keyframe.
- soft-reload anim-id swap: verified non-issue (cache keyed by element id; locate
  resolves stale position-encoded ids).

Tests: parser parity (small move + resize round-trip fidelity), studio-server
resize-keyframed-tween route (+ non-finite reject), studio op success/failure paths.
735 parser / 215 studio-server / 1196 studio pass; tsc/oxlint/oxfmt clean. Bypassed
fallow gate (branch-wide parity/wiring duplication).
2026-06-29 14:43:07 -07:00
Miguel Ángel 0a9555a0f7 fix(studio): keyframe/position editing correctness + thumbnail cache busting + local-studio preview discovery (#1781)
* feat(player,studio): favicon-blade play icon with pause<->play morph

Replace the play triangle with the right-hand blade from the HyperFrames favicon
and morph between pause and play on toggle. Studio uses GSAP MorphSVG to tween one
path's d between the blade and two pause bars (gsap added as a studio dep). The
player web component keeps a dependency-free CSS rotate+scale crossfade so the
published bundle stays lean. Both honor prefers-reduced-motion.

* fix(cli): discover local-studio (Vite) preview over IPv6 loopback

The Vite dev server binds [::1] (IPv6) while embedded servers bind 127.0.0.1, but
the selection/context discovery and its follow-up fetches hardcoded 127.0.0.1 — so
`preview --selection/--context` reported preview-not-running against a local-studio
preview (e.g. inside the monorepo / bun run dev). Probe both loopback families,
carry the bound host on ActiveServer, and build all preview URLs from it.

Adds an IPv6-only discovery regression test.

* fix(studio): wire the Add-keyframe (K) shortcut

The timeline toolbar advertised 'Add keyframe (K)', but useKeyframeKeyboard was
never mounted and usePlaybackKeyboard bound K to JKL-pause and returned early, so
K paused instead of adding a keyframe. Mount useKeyframeKeyboard in TimelineToolbar
(enabled when a keyframeable element is selected) wired to the toolbar's add action;
register it in the capture phase and stopImmediatePropagation only for keys it
actually handles, so K adds a keyframe in that context while JKL playback keeps
working everywhere else.

* fix(studio): clear orphaned GSAP transforms on soft reload

A manually-dragged element is positioned via gsap.set, which writes an inline
transform. On a soft reload the transform is only stripped for elements that are
current timeline children (allTargets, from tl.getChildren().targets()). An
element positioned by a standalone gsap.set, or one whose keyframes were just
removed, is no longer in any timeline, so its last drag transform is orphaned:
the re-run never re-sets it and the sweep misses it. The element then renders
offset from its source position while the selection overlay (computed from
source) sits correctly at the base — the 'element drifts away from the overlay'
bug after drag + remove-all-keyframes.

Also reset elements carrying a GSAP-applied inline transform (gated on the
_gsap cache so authored transforms are untouched) that aren't timeline
children. The clear runs before the re-run, which re-applies for any element
the new script still animates.

* fix(studio-server): bust thumbnail cache on composition edits

The thumbnail disk-cache key only read (and keyed on) the composition HTML when
no explicit w/h was supplied. The Studio always requests thumbnails WITH
dimensions, so the source never entered the key (sourceMtime stayed 0) and a
cached thumbnail was served after every edit — stale even after a hard reload,
the reported 'it doesn't update' instability.

Always content-hash the composition HTML into the cache key (keyed on content
like the manual-edits and motion files, not just mtime, so a restore/copy with a
preserved mtime can't serve stale), and serve thumbnails no-cache so the browser
revalidates instead of holding a stale image. Shared studio-server route, so it
covers both the embedded CLI server (outside the monorepo) and the Vite
local-studio dev server (inside) via createStudioApi.

* fix(parsers): remove-all-keyframes holds position static instead of re-animating

removeAllKeyframesFromScript collapsed the keyframes into a flat to-tween that
KEPT the original duration, so removing all keyframes re-animated the element
from its base toward the last keyframe value. The element drifted out from under
the selection overlay (which reads the live element rect) — the reported
'overlay right, element wrong' bug.

Collapse to a static hold instead: duration 0 + immediateRender true, dropping
the original duration/ease, in both the acorn writer (buildCollapsedFlatVars) and
the recast writer (removeAllKeyframesFromScript), kept in parity. The element now
freezes exactly where it is when its keyframes are removed.

* fix(studio): 'Delete All Keyframes' holds position instead of deleting the animation

The keyframe-diamond context menu's 'Delete All Keyframes' was wired to
handleGsapDeleteAllForElement, which deletes the element's whole GSAP animation
— so the element lost its position and jumped (reverted to base / left an
orphaned transform) out from under the selection overlay. Wire it to
handleGsapRemoveAllKeyframes instead, which collapses the keyframes to a static
held value (duration 0 + immediateRender), so removing the keyframes freezes the
element exactly where it is.

* fix(studio): timeline 'Delete All Keyframes' holds position too

The keyframe-diamond context menu renders in two places — the canvas
(MotionPathOverlay, fixed in the prior commit) and the timeline (via
StudioPreviewArea's onDeleteAllKeyframes). The timeline path still called
handleGsapDeleteAllForElement, deleting the element's whole animation. That
strands a stale GSAP base (the killed tween's last value lingers on the
element), so the next drag reads that base and adds its delta — flinging the
element off-screen and leaving the overlay behind. Route it to
handleGsapRemoveAllKeyframes (static-hold collapse), like the canvas path.

* fix(studio): one position write per element + clean remove-all-keyframes

Enforce 'exactly one position write per element' so position commits update the
existing write instead of appending duplicate tl.to/gsap.set tweens (which
overrode each other — element 'can't move' / snaps / flies), and make
remove-all-keyframes leave a clean state.

- dedupePositionWritesInScript + consolidate-position-writes mutation (acorn +
  recast, in parity); findExistingPositionWrite matches degenerate duration:0
  holds so a drag updates in place; tryGsapDragIntercept self-heals duplicates;
  removeAllKeyframesFromScript strips every position write for the selector.
- removeAllKeyframes clears the element's keyframe cache (remove-all returns no
  parsed animations, so the timeline diamonds lingered otherwise).
- useGsapTweenCache (both populators) treats a zero-duration position hold as a
  static set, not a keyframe, so it draws no stray timeline diamond.
- Extracted gsapPositionDetection.ts (file-size cap).

Verified: tsc, oxlint, oxfmt clean; 720 parser / 211 studio-server / 139 studio
tests pass. Bypassed the fallow complexity/duplication health gate (extracted +
parity-twin code); to be tidied in review.
2026-06-29 11:23:46 -07:00
Miguel Ángel c811a2750a chore: release v0.7.18 2026-06-28 22:55:15 +00:00