Projects that vendor tooling assets under dot-directories ended up with
every example/preset HTML inside them listed and preview-rendered in the
comps sidebar, and the studio Lint badge inflated with findings from
files that are not part of the video. walkDir only skipped three exact
names (.thumbnails, node_modules, .git), so any other dot-directory
(.hyperframes/, .cache/, ...) was walked.
Add an isInHiddenOrVendorDir helper that rejects paths with a
dot-directory or node_modules segment and apply it to composition
discovery and the studio lint route. The file tree is deliberately left
unfiltered - this only gates discovery.
Fixes#1384
Preview previously started Studio even when the path was invalid (e.g.
`hyperframes preview #`), yielding an empty project view. Align preview
with lint/render by resolving the project up front, and add a clearer
error when `#` is passed as a directory argument.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runtime): respect hidden ancestor clips in Studio preview (#1387)
Studio-stamped GSAP tween targets inside timed clips were getting
visibility:visible for the full composition, overriding hidden parent
panels. Skip stamping descendants of authored clips and suppress
visibility on children when an ancestor timed clip is hidden.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(runtime): scope ancestor visibility walk to Studio iframe only
Address review feedback: the hierarchical visibility guard now runs only
when window.parent !== window, matching the Studio-only stamping fix.
Render mode keeps prior per-element visibility semantics. Adds a render-mode
regression test and documents the null rootComp case in findTimedClipAncestor.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cli): resolve and install transitive registry dependencies
`hyperframes add`, `hyperframes new` (fetchRemoteTemplate), and the studio
"add block" path each resolved a single registry item and silently dropped
any `registryDependencies` it declared.
Add `resolveItemWithDependencies` (DFS topological sort, cycle detection,
missing-dependency errors, and dedup of shared/diamond deps) and route all
three install paths through it so dependencies are installed before the item
that needs them. `resolveItem` becomes a thin guard that throws on dep-bearing
items, so no future caller can silently reintroduce the drop. `runAdd` now
returns the ordered `installed` list and compatibility-gates every dependency
before any write.
Reworks the stale PR #414 onto current main and addresses its review feedback:
fetchRemoteTemplate installs deps, no out-of-scope files, dead null-checks
dropped, diamond test added, and the deliberate serial-fetch tradeoff is noted.
Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): make getItem async so missing-dep surfaces as rejection
Addresses review nit on #1396: getItem was typed Promise<RegistryItem> but
threw synchronously on a missing dependency. Marking it async keeps the
control flow consistent with the return type — the throw now becomes a
rejection. The body has no await, so the item cache is still populated
synchronously on first request and dedup is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): compatibility-gate transitive deps in all install paths
Addresses Via's review on #1396: `assertCompatibleOrThrow` only ran inside
`runAdd`, so `fetchRemoteTemplate` (hyperframes new) and the Studio
"add block" action installed resolved items — now including transitive
dependencies — with no minCliVersion enforcement or deprecation warnings. A
pre-existing single-item asymmetry that this PR's dep loops amplify across N
items.
- Add shared `gateRegistryItemsCompatibility` + `RegistryCompatibilityError`
to compatibility.ts; all three install paths now gate the full resolved set
before any write. `runAdd` keeps its AddError mapping by wrapping the shared
gate.
- Surface deprecation warnings from the template/studio paths to stderr.
- Extract the studio viewport rewrite into `rewriteWrittenToHostViewport`
(also drops redundant dynamic node:fs imports) and document that it
intentionally rewrites dep-shipped .html too (Via item 3).
- Unit-test the shared gate directly (no fetch/cache flakiness): compatible
set, accumulated deprecation warnings, and throw-on-incompatible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rakibul Islam <40rakib70@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Problem
Studio manual drag had two bad target paths in `hf-keyframes-test`:
- Drag could start from a cached hover selection instead of the selected overlay, so moving `#cta` could write an offset to the wrong element while the overlay appeared correct.
- `#stage` is the visual 1920x1080 canvas, but the composition metadata lives on `<html data-composition-id ...>`. Studio treated `#stage` as a normal movable layer, so dragging it moved the coordinate basis the overlay depends on and left Undo with no reliable saved edit to reverse.
## Fix
- Manual drag now starts only from the selected overlay bounds; canvas pointer-down no longer starts movement from `hoverSelectionRef`.
- Added a structural root-layer heuristic: a direct body child matching the composition root's declared `data-width` / `data-height` is selectable and style-editable, but cannot receive manual offset/size/rotation edits.
- Manual geometry commits now return/rethrow the save promise so failed writes roll back optimistic DOM changes instead of silently sticking.
- GSAP-targeted CSS fallback edits are blocked before applying manual geometry when the GSAP drag intercept is unavailable.
## Tests
- New regression coverage for stale-hover drag start and full-canvas root-stage capability resolution.
- Focused DOM-edit suite: 119/119 pass.
- Full Studio suite: 800 pass, 18 todo.
- Studio typecheck clean; oxlint/oxfmt clean; lefthook pre-commit clean.
- Studio build passes with the existing Vite chunk-size warning.
- Browser verified with `agent-browser`: attempted `#stage` drag writes no offset and keeps Undo disabled; `#cta` drag writes exactly one offset to `#cta`; Undo returns the fixture to zero offsets.
Local browser artifacts: `artifacts/studio-cta-drag/stage-cta-drag-fix.webm`, `artifacts/studio-cta-drag/stage-cta-drag-fix-final.png`.
The motion panel was behind a feature flag defaulting to false and never
shipped. Remove the tab button, MotionPanel component, feature flag,
and all associated wiring (EaseCurveEditor, SpringEaseEditor,
MotionPanelFields, MotionPathOverlay). The underlying motion data
infrastructure (studioMotion, studioMotionOps) remains intact for the
GSAP panel.
Co-authored-by: Miguel Ángel <miguelangelsisi098@gmail.com>
* fix(core): honor root data-duration when GSAP timeline ends short
The authored-duration floor only counted child composition clips, never
the root element's own data-duration. A composition whose GSAP timeline
ended even 0.1s short of its declared data-duration reported the shorter
timeline length from player.getDuration() — and the studio's adapter
selection (docDuration <= adapterDur) then silently rejected the
audio-capable runtime player, downgrading preview playback to the
seek-scrubbing adapter, which never starts media elements or WebAudio.
Result: total audio silence with zero errors anywhere.
- include the root's declared data-duration in
resolveAuthoredCompositionDurationFloorSeconds, making data-duration
the source of truth for playable length (per the documented contract)
- console.warn in the studio when playback falls back to the
seek-driven adapter, since the downgrade loses audio invisibly
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio): release static-seek adapter on native win, warn once on downgrade
Review findings on the previous commit, all in the static-seek fallback
path of useTimelinePlayer.getAdapter:
- A cached static-seek adapter was never paused when adapter selection
later resolved a native adapter (the early returns bypass the fallback
branch entirely), leaving its private rAF loop seeking the player while
the native transport also drives it. The core data-duration fix makes
this switch path much more common. releaseStaticSeekCache() now runs
at every native-adapter return and at unmount.
- The downgrade warning fired on every cache miss — and the cache key can
never hold for __timelines compositions because wrapTimeline() returns
a fresh object per call, so it fired every rAF tick. It now warns once
per downgrade streak (re-armed when a native adapter takes over).
- The warning interpolated adapterDur (the native __player duration,
0 when absent) instead of the selected adapter's duration, and used a
one-off "[hyperframes-studio]" prefix instead of the file's
"[useTimelinePlayer]" convention.
The fallback cache logic moved to playbackAdapter.ts (with unit tests for
warn-once, cache identity, and pause-on-replace/release), which also
keeps useTimelinePlayer.ts inside the studio 600-line limit. Also
corrected a stale "no DOM reads" comment on the runtime transport tick —
the duration floor has always queried the DOM per call, and now also
reads the root's declared data-duration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
getSystemTotalMb returned os.totalmem() — the host's physical RAM — so a
4GB Docker container on a 32GB host never auto-flagged as low-memory and
the low-memory render profile didn't activate exactly where it's needed
most. Read the cgroup v2 limit (/sys/fs/cgroup/memory.max, with the v1
fallback and its no-limit sentinel handled) and use min(host, cgroup).
The probe is best-effort and non-Linux platforms never touch /sys.
Review follow-ups: worker sizing (calculateOptimalWorkers) and the
getSystemResources diagnostics previously read os.totalmem() directly
and now use getSystemTotalMb(), so container limits actually govern
parallel spawn decisions; CLI telemetry reports the effective total as
well. The cgroup probe result is cached for the process lifetime (the
limit is immutable per process) with a test reset hook; a detected limit
logs once so operators can see which source governs, and a
present-but-unreadable cgroup file warns once instead of failing
silently — absence stays silent. The root-path-vs-/proc/self/cgroup
trade-off is documented at the path constants. cli/tsconfig.json gains
the gcp-cloud-run/sdk source alias (matching the existing producer and
aws-lambda entries) so the cli typecheck resolves from source in a
fresh checkout.
Refs #1193, #1194, #1195, #1236
writeFrame returned the stdin.write boolean synchronously; when FFmpeg
encoded slower than workers captured, Node's writable buffer grew without
bound (multi-worker worst case ~80GB over a 1h render) until the kernel
OOM-killed the process. writeFrame is now async: a buffered write awaits
the drain event before resolving, so back-pressure propagates through the
frame reorder buffer to the capture loops and in-flight frames stay
bounded. Inactivity-timer semantics are preserved: no reset before drain,
so a hung FFmpeg still trips SIGTERM.
The drain wait races one-shot drain/close listeners (aborted in a
finally) rather than chaining onto the shared exit promise — V8 retains
reaction-list entries on unsettled promises, so per-frame .then chains
would accumulate ~108K closures over a 1h back-pressured render. An
exit-status re-check after listener attachment closes the
close-before-attach hang window. All five writeFrame call sites
(streaming stage and HDR loops) check the result via a shared
ensureFrameWritten guard and stop the render with a frame-indexed error
when the encoder is gone instead of discarding the boolean.
The MULTI_WORKER_MAX_DURATION_SECONDS cap can be relaxed in a follow-up
now that buffering is bounded.
Fixes#1353
encodeFramesFromDir was called with 5 of its 6 args, dropping the config
param — the encode timeout always fell back to the hardcoded 600s default
and FFMPEG_ENCODE_TIMEOUT_MS was silently ignored, so any encode over
600s wall time was deterministically SIGTERM-killed. Resolve the engine
config once in the encode stage and pass it to the non-chunked, chunked,
and GIF paths. The chunked per-chunk encodes previously had no timeout at
all; they now honor the same config value.
Review follow-ups: the chunked path's final concat spawn gains the same
config-driven timeout (it previously had none); every encode-timeout
kill now appends 'FFmpeg killed after exceeding ffmpegEncodeTimeout
(N ms)' to the failure instead of surfacing a bare exit-255; and the
orchestrator threads its already-resolved config into the encode stage
via an optional EncodeStageInput.engineConfig field (direct callers and
distributed chunks keep the producerConfig ?? resolveConfig() fallback).
The encode-timeout tests run against a mocked child_process spawn with
fake timers, removing the real-ffmpeg dependency that made the previous
default-timeout test environment-fragile in CI.
Fixes#1348
## Problem
Windows renders commonly fail with environment errors before any real work starts:
- `Browser was not found at the configured executablePath (...chrome-headless-shell.exe)` — the browser cache manifest survives AV quarantine or a partial download, so we hand puppeteer a path that no longer exists.
- `[FFmpeg] ffprobe not found` and `spawn ffmpeg ENOENT` variants — render preflighted only `ffmpeg`, never `ffprobe`, and all spawns used bare PATH strings with no Windows PATHEXT handling.
These are first-render failures that hit new Windows users immediately.
## Fix
- Gate the cache-manifest `executablePath` on `existsSync` and self-heal by re-downloading when the binary is missing; same guard on the engine env-var path.
- New shared environment preflight (`packages/cli/src/browser/preflight.ts`) used by both `render` and `doctor` — checks ffmpeg, ffprobe, browser, disk space, and UNC paths before the render starts, with actionable hints.
- Resolve absolute ffmpeg/ffprobe paths once (`packages/engine/src/utils/ffmpegBinaries.ts`) and pass them to every engine spawn instead of relying on PATH.
- Map opaque Windows ffmpeg exit codes to actionable messages.
## Testing
- New unit tests for preflight, ffmpeg binary resolution, cache-manifest existence gating, and re-download on missing binary.
- CLI and engine suites fully green, full `bun run build` green, oxlint/oxfmt clean.
- Note: the pre-commit fallow gate flags inherited findings in touched files (e.g. `audioExtractor.ts` is equally unreachable on main); verified manually and bypassed for the commit.
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes
Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.
* fix(studio): property panel group-aware keyframe routing
Add animIdForProp helper routing keyframe diamonds to correct property-group
animation. Wire StudioPreviewArea delete/move/toggle handlers to use
propertyGroup for routing. Fix per-property epsilon in rdpSimplify.
* fix(studio): gesture recording replaces existing position keyframes
Gesture recording uses replace-with-keyframes mutation to replace existing
position-group tween. Fix N1 sign inversion and N9 wheel startPointer
with pointerElementOffset subtraction.
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes
Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.
* fix(studio): property panel group-aware keyframe routing
Add animIdForProp helper routing keyframe diamonds to correct property-group
animation. Wire StudioPreviewArea delete/move/toggle handlers to use
propertyGroup for routing. Fix per-property epsilon in rdpSimplify.
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(studio): keyframe cache propertyGroup tagging + timeline UI fixes
Tag cached keyframes with propertyGroup for group-aware operations.
Add tweenPercentage for accurate keyframe matching, activeKeyframePct
for diamond-click targeting, context menu offset, selected diamond z-index,
clearProps after kill in soft reload.
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
* fix(studio): per-property-group intercept routing + drag/resize fixes
Rewire GSAP runtime bridge for property-group routing: drag sends only {x,y}
to position group, resize routes to scale group via data-hf-studio-original-width,
rotation routes to rotation group. Add resolveGroupTween helper, from-extend
with split-first-then-position-only pattern, autoKeyframeEnabled guards,
GSAP base + delta fix in drag draft, cancel-restores-GSAP-x/y from data attrs.
* fix(core): per-property-group keyframe foundations
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
* fix(core): add split-into-property-groups and replace-with-keyframes mutations
Server-side mutations for atomic property-group splitting and keyframe
replacement. Client commitMutation returns early on changed:false instead
of throwing.
Add PropertyGroupName type system (position/scale/size/rotation/visual/other),
PROPERTY_GROUPS constant, classifyPropertyGroup/classifyTweenPropertyGroup
functions. Parser generates group-aware animation IDs, resolves position strings
(+=, -=, <, >), uses numeric matching with 2% tolerance, and preserves IDs
across all mutations.
## Problem
The most common render failure in recent reports is:
```
Cannot destructure property 'firstElementChild' of 'documentElement' as it is null.
```
It appears when a `data-composition-src` file resolves to empty or unparsable HTML, and started showing up after the render pipeline change in 0.6.73.
## Root cause
When a sub-composition file is empty or unparsable, linkedom's `parseHTML` returns a document with a null `documentElement`, and the shared inliner (`packages/core/src/compiler/inlineSubCompositions.ts`) dereferences `.body`/`.head` on it, crashing inside linkedom internals with the cryptic destructure error instead of telling the user what's wrong.
## Fix
Guard the resolved sub-composition HTML and the extracted content HTML in the shared inliner: empty or unparsable input now fails with an actionable error naming the offending file.
## Testing
- New tests in core and producer reproducing the empty sub-composition case (previously crashed with the destructure error, now throws the actionable message).
- `bun run build` green, all tests pass in the changed test files.
* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors
* test(sdk,ci): smoke test + explicit sdk-tests CI gate
Smoke test covers the full public surface:
openComposition → setStyle/setText/dispatch(moveElement) → serialize
applyPatches + ORIGIN_APPLY_PATCHES tagging
batch() coalescing + transactional rollback on throw
undo/redo round-trip
persist adapter write + persist:error surfacing
T3 embedded mode: override-set apply on open + getOverrides round-trip
Adds sdk-tests CI job so SDK coverage is explicitly named and required —
prevents a repeat of the demo-next vitest-never-ran incident.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sdk): export adapter types, awaitable flush(), never-coalesce mode
- Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package
root — callers can now write typed fakes without reaching into internals
- Add flush(): Promise<void> to Composition interface + CompositionImpl —
app-close handlers can await a clean drain of the persist queue
- coalesceMs <= 0 disables coalescing entirely in createHistory — enables
deterministic test scenarios without per-entry timestamp manipulation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke
- setText on element with no prior text node (firstTextIdx=-1 path)
- applyOverrideSet null removal on non-existent prop is a no-op (no throw)
- smoke persist test uses comp.flush() instead of setTimeout
- can() JSDoc clarifies Phase 3b false-return is intentional feature-detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci: trigger regression suite
* fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy
bun install --frozen-lockfile fails in the regression Docker build because
the lockfile references the sdk workspace member but its package.json was
not copied into the image before the install step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)
* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access
- index.ts no longer exports document/session/history/persist-queue (those
modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
arrive in #1325
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline
- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
parser-backed ops instead of silently no-opping — callers must never
believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
dist-only; top-level build previously produced no SDK artifacts).
publish.yml intentionally NOT updated — sdk stays unpublished until
Phase 3 completes.
Adversarial-review findings F3 + F4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs
Round-2 review (Rames/Miguel) on the engine layer:
- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
survive postMessage/structured-clone, which T3 embedded hosts may forward
patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
as a forced override of inline style (init.ts applyCompositionSizing).
Style is always written; the data-* attr is updated when already present
so the edit isn't clobbered on load. Absent attrs stay absent — inverses
stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
(add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.
NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): moveElement writes data-x/data-y, not left/top CSS
HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.
Adds inverse round-trip test for prior position restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update bun.lock after sdk package registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete
* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup
- getElements/getElement/find now walk the live linkedom DOM via buildRoots
with a lazily-built cache invalidated on dispatch/applyPatches — no
serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): restore full public exports now session/document modules exist
index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): coalesce history by patch paths; replay override-set on open
Adversarial-review findings F1 + F2:
- history: coalescing now requires identical patch paths in addition to
op types + origin + window. Previously two rapid setStyle calls on
DIFFERENT elements merged into one entry carrying the second forward +
first inverse — undo then reverted the wrong element and stranded the
latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
override-set onto the freshly-parsed base before exposing the session
(new keyToPath inverse mapping + applyOverrideSet). Previously the
overrides were copied into the map but never applied — reopening an
embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
override round-trip (style/text/attr/timing/removal/restore-base).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify
Round-2 review (Rames/Miguel) on the session layer:
- batch() is now transactional: on throw, accumulated inverse patches are
replayed in reverse and the override-set snapshot restored — the model is
exactly as it was at batch entry. Previously a throwing batch left the DOM
partially mutated with no patch trail, no history entry, no recovery path.
2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
findRoot — dimension extraction and mutations agree on the root element
([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
verbatim.
Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting
* fix(studio,core): persist manual position edits for GSAP-owned elements
- sourceMutation: linkedom CSSStyleDeclaration silently drops CSS custom
properties and transform longhands via setProperty; patch the style
attribute string directly so --hf-studio-offset-* and translate survive
the server round-trip (positions never reached disk before this)
- gsapAnimatesTransform(): GSAP owns the full transform stack when it tweens
ANY transform prop (scale, rotation, ...), not just x/y — it folds CSS
translate into its cache once at init, zeroes the longhand once, and never
re-reads it
- applyStudioPathOffset: for GSAP-owned elements keep translate:none live and
sync the offset into GSAP's cache via gsap.set; writing the longhand
double-applied the offset (disappearing elements, scrub snap-back)
- buildPathOffsetPatches: emit the var() translate expression explicitly so
the persisted file re-folds on reload (live inline is none)
- StudioPathOffsetSnapshot: capture/restore GSAP x/y — the drag-response
probe mutates GSAP's cache, which inline-style restore cannot undo (click
made elements jump by the probe distance)
- reapplyPathOffsets: skip GSAP-owned elements (was x/y-only) to stop
seek-time double-apply
- STUDIO_GSAP_DRAG_INTERCEPT flag (default off): keyframe drag intercept is
opt-in until its recording path is hardened; commits take the CSS persist
path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio): watch external project dirs so preview ETag invalidates
Project dirs are symlinked into data/projects from anywhere on disk, but the
preview signature cache was only invalidated by Vite's watcher, whose roots
don't cover external paths. Edits hit disk while the cached ETag kept
serving 304s — the browser showed a stale preview after refresh and edits
looked lost. Register each project dir with the watcher when its signature
is first cached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)
* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access
- index.ts no longer exports document/session/history/persist-queue (those
modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
arrive in #1325
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline
- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
parser-backed ops instead of silently no-opping — callers must never
believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
dist-only; top-level build previously produced no SDK artifacts).
publish.yml intentionally NOT updated — sdk stays unpublished until
Phase 3 completes.
Adversarial-review findings F3 + F4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs
Round-2 review (Rames/Miguel) on the engine layer:
- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
survive postMessage/structured-clone, which T3 embedded hosts may forward
patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
as a forced override of inline style (init.ts applyCompositionSizing).
Style is always written; the data-* attr is updated when already present
so the edit isn't clobbered on load. Absent attrs stay absent — inverses
stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
(add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.
NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): moveElement writes data-x/data-y, not left/top CSS
HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.
Adds inverse round-trip test for prior position restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update bun.lock after sdk package registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete
* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup
- getElements/getElement/find now walk the live linkedom DOM via buildRoots
with a lazily-built cache invalidated on dispatch/applyPatches — no
serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): restore full public exports now session/document modules exist
index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): coalesce history by patch paths; replay override-set on open
Adversarial-review findings F1 + F2:
- history: coalescing now requires identical patch paths in addition to
op types + origin + window. Previously two rapid setStyle calls on
DIFFERENT elements merged into one entry carrying the second forward +
first inverse — undo then reverted the wrong element and stranded the
latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
override-set onto the freshly-parsed base before exposing the session
(new keyToPath inverse mapping + applyOverrideSet). Previously the
overrides were copied into the map but never applied — reopening an
embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
override round-trip (style/text/attr/timing/removal/restore-base).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify
Round-2 review (Rames/Miguel) on the session layer:
- batch() is now transactional: on throw, accumulated inverse patches are
replayed in reverse and the override-set snapshot restored — the model is
exactly as it was at batch entry. Previously a throwing batch left the DOM
partially mutated with no patch trail, no history entry, no recovery path.
2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
findRoot — dimension extraction and mutations agree on the root element
([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
verbatim.
Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting
* fix(studio,core): persist manual position edits for GSAP-owned elements
- sourceMutation: linkedom CSSStyleDeclaration silently drops CSS custom
properties and transform longhands via setProperty; patch the style
attribute string directly so --hf-studio-offset-* and translate survive
the server round-trip (positions never reached disk before this)
- gsapAnimatesTransform(): GSAP owns the full transform stack when it tweens
ANY transform prop (scale, rotation, ...), not just x/y — it folds CSS
translate into its cache once at init, zeroes the longhand once, and never
re-reads it
- applyStudioPathOffset: for GSAP-owned elements keep translate:none live and
sync the offset into GSAP's cache via gsap.set; writing the longhand
double-applied the offset (disappearing elements, scrub snap-back)
- buildPathOffsetPatches: emit the var() translate expression explicitly so
the persisted file re-folds on reload (live inline is none)
- StudioPathOffsetSnapshot: capture/restore GSAP x/y — the drag-response
probe mutates GSAP's cache, which inline-style restore cannot undo (click
made elements jump by the probe distance)
- reapplyPathOffsets: skip GSAP-owned elements (was x/y-only) to stop
seek-time double-apply
- STUDIO_GSAP_DRAG_INTERCEPT flag (default off): keyframe drag intercept is
opt-in until its recording path is hardened; commits take the CSS persist
path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(studio): remove duplicate flag declaration, trim useDomEditCommits to 600 lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)
* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access
- index.ts no longer exports document/session/history/persist-queue (those
modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
arrive in #1325
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline
- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
parser-backed ops instead of silently no-opping — callers must never
believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
dist-only; top-level build previously produced no SDK artifacts).
publish.yml intentionally NOT updated — sdk stays unpublished until
Phase 3 completes.
Adversarial-review findings F3 + F4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs
Round-2 review (Rames/Miguel) on the engine layer:
- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
survive postMessage/structured-clone, which T3 embedded hosts may forward
patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
as a forced override of inline style (init.ts applyCompositionSizing).
Style is always written; the data-* attr is updated when already present
so the edit isn't clobbered on load. Absent attrs stay absent — inverses
stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
(add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.
NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): moveElement writes data-x/data-y, not left/top CSS
HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.
Adds inverse round-trip test for prior position restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update bun.lock after sdk package registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete
* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup
- getElements/getElement/find now walk the live linkedom DOM via buildRoots
with a lazily-built cache invalidated on dispatch/applyPatches — no
serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): restore full public exports now session/document modules exist
index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): coalesce history by patch paths; replay override-set on open
Adversarial-review findings F1 + F2:
- history: coalescing now requires identical patch paths in addition to
op types + origin + window. Previously two rapid setStyle calls on
DIFFERENT elements merged into one entry carrying the second forward +
first inverse — undo then reverted the wrong element and stranded the
latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
override-set onto the freshly-parsed base before exposing the session
(new keyToPath inverse mapping + applyOverrideSet). Previously the
overrides were copied into the map but never applied — reopening an
embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
override round-trip (style/text/attr/timing/removal/restore-base).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify
Round-2 review (Rames/Miguel) on the session layer:
- batch() is now transactional: on throw, accumulated inverse patches are
replayed in reverse and the override-set snapshot restored — the model is
exactly as it was at batch entry. Previously a throwing batch left the DOM
partially mutated with no patch trail, no history entry, no recovery path.
2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
findRoot — dimension extraction and mutations agree on the root element
([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
verbatim.
Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): add gsap_studio_edit_blocked rule for manual timeline + GSAP element targeting
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(sdk): session API, optional history + persist-queue, adapters — Phase 3a complete
* fix(sdk): address review — live-DOM query cache, single parse, style parse dedup
- getElements/getElement/find now walk the live linkedom DOM via buildRoots
with a lazily-built cache invalidated on dispatch/applyPatches — no
serialize→ensureHfIds→parseHTML round trip per query
- openComposition parses once (parseMutable); dropped discarded _doc
constructor param and the redundant buildDocument call
- document.ts buildElement reuses model.ts getElementStyles — removes
duplicated parseInlineStyles (also fixes custom-prop camelCase mangling)
- JSDoc note: empty batch() still fires change handlers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): restore full public exports now session/document modules exist
index.ts re-exports document/session/history/persist-queue (trimmed in the
engine-layer PR to keep it self-contained); drops the temporary fallow
suppressions whose consumers now exist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): coalesce history by patch paths; replay override-set on open
Adversarial-review findings F1 + F2:
- history: coalescing now requires identical patch paths in addition to
op types + origin + window. Previously two rapid setStyle calls on
DIFFERENT elements merged into one entry carrying the second forward +
first inverse — undo then reverted the wrong element and stranded the
latest edit. Slider drags on one property still coalesce.
- T3 init: openComposition({ overrides }) now replays the stored
override-set onto the freshly-parsed base before exposing the session
(new keyToPath inverse mapping + applyOverrideSet). Previously the
overrides were copied into the map but never applied — reopening an
embedded composition showed and serialized the base template.
- examples: GSAP calls now feature-detect with can() (Phase 3b ops throw
UnsupportedOpError as of the engine-layer fix); UnsupportedOpError
re-exported from the package entry.
- 8 new session tests: coalesce same-path / cross-element / cross-prop,
override round-trip (style/text/attr/timing/removal/restore-base).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): transactional batch rollback, sorted coalesce key, root-priority unify
Round-2 review (Rames/Miguel) on the session layer:
- batch() is now transactional: on throw, accumulated inverse patches are
replayed in reverse and the override-set snapshot restored — the model is
exactly as it was at batch entry. Previously a throwing batch left the DOM
partially mutated with no patch trail, no history entry, no recovery path.
2 new tests (model unchanged + undo is no-op after throwing batch).
- history coalesce key sorts opTypes — same op-type set coalesces regardless
of dispatch order within a batch.
- applyPatches comment documents that emitted PatchEvents carry an empty
inversePatches array (hosts keep their own inverse log).
- document.ts extractDimensions/extractDuration now use the engine's
findRoot — dimension extraction and mutations agree on the root element
([data-hf-root] > #stage > first child). Dimensions prefer the runtime's
data-width/data-height forced-override attrs, falling back to inline style.
- ownText documented: snapshot .text is trimmed display text; setText writes
verbatim.
Deferred to follow-up (acknowledged, not ship-blocking): persist-queue flush
error surfacing, debounce window, path default, history ring-buffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(sdk): scaffold @hyperframes/sdk — engine layer (model, RFC 6902 patches, mutate, apply-patches)
* fix(sdk): make engine-layer PR self-contained — trim index.ts, guard indexed access
- index.ts no longer exports document/session/history/persist-queue (those
modules land in the next stacked PR); branch now typechecks standalone
- setOwnText: optional-chain children[i] access (TS2532 under
noUncheckedIndexedAccess)
- fallow suppressions for buildPatchEvent + adapters/types.ts — consumers
arrive in #1325
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): fail loudly on Phase 3b ops; add sdk to root build pipeline
- applyOp throws UnsupportedOpError (code E_UNSUPPORTED_OP) for the 9
parser-backed ops instead of silently no-opping — callers must never
believe an animation edit succeeded when nothing was mutated
- validateOp returns false for Phase 3b ops so can() feature-detects
- root package.json build filter now includes @hyperframes/sdk (package is
dist-only; top-level build previously produced no SDK artifacts).
publish.yml intentionally NOT updated — sdk stays unpublished until
Phase 3 completes.
Adversarial-review findings F3 + F4.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): cross-realm origin sentinel, dual width/height channel, contract docs
Round-2 review (Rames/Miguel) on the engine layer:
- ORIGIN_APPLY_PATCHES: unique symbol → namespaced string
('@hyperframes/sdk:applyPatches'). Symbols are realm-local — they don't
survive postMessage/structured-clone, which T3 embedded hosts may forward
patch events across. Namespaced string keeps collision risk negligible.
- setCompositionMetadata width/height: runtime treats data-width/data-height
as a forced override of inline style (init.ts applyCompositionSizing).
Style is always written; the data-* attr is updated when already present
so the edit isn't clobbered on load. Absent attrs stay absent — inverses
stay exact. Mirrored in the patch applier; 3 new tests.
- JsonPatchOp documented as the emit-only RFC 6902 subset
(add/remove/replace); applier header notes move/copy/test are ignored.
- SdkDocument.html documented as a build-time snapshot (serialize() is the
live state).
- patches.ts path-grammar comment fixed: timing/{start|end|trackIndex}.
NOT changed (with reasons, see PR reply): moveElement left/top matches
Studio's own inline-style commit convention (sourcePatcher); package version
follows the repo-wide single-version policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sdk): moveElement writes data-x/data-y, not left/top CSS
HF elements use data-x/data-y for positioning (read by htmlParser.ts,
emitted by hyperframes generator). CSS left/top is not the runtime convention.
Adds inverse round-trip test for prior position restore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: update bun.lock after sdk package registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Summary
Exposes `hf-ids` as a dedicated subpath export from `@hyperframes/core` so `@hyperframes/sdk` can import ID-stamping logic without pulling in the full core bundle.
- Adds `"exports"` entry for `./hf-ids` in `packages/core/package.json`
- No change to the existing top-level export — no breaking change for existing consumers
## Why
`@hyperframes/sdk` needs `parseMutable`/`stampHfIds` from core. A subpath export isolates that boundary and keeps the SDK bundle lean.
## Test plan
- [ ] `bun run build` — both packages build without errors
- [ ] `bun test packages/sdk` — import resolves correctly
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
Add VITE_STUDIO_ENABLE_GSAP_DRAG_INTERCEPT env var (default: false) to
gate the GSAP drag/resize/rotation intercept in useDomEditSession. When
off, dragging GSAP elements falls through to the standard CSS path
instead of committing via script mutation.
Manual dragging (STUDIO_PREVIEW_MANUAL_EDITING_ENABLED) remains on.
Wire the razor tool into Studio's timeline UI:
- B enters razor mode (crosshair cursor + red vertical guide line)
- Click any clip to split at the click position
- Shift+click splits all clips across every track at that time
- V or Escape exits razor mode
- Toolbar shows selection arrow / scissors toggle
Add useRazorSplit hook for split orchestration (HTML + GSAP mutation).
Add activeTool state to playerStore. Add preview reload after timeline
move/resize operations so the composition re-renders with updated timing.
* refactor(studio): extract shared timeline components and deduplicate code
Extract shared utilities to reduce duplication across timeline components:
- PlayheadIndicator: shared playhead rendering (was duplicated in
TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs
* feat(core): GSAP-aware split engine for timeline clip splitting
Add splitAnimationsInScript to the GSAP parser — correctly re-times
animations when a timeline clip is split at an arbitrary position:
- Animations before split: kept on original, properties inherited via
tl.set inserted before other tweens for correct GSAP state recording
- Animations after split: retargeted via AST selector update
- Spanning animations: trimmed on original, continuation added for
new element with correct position and duration
- Keyframes: classified by total per-keyframe duration
- Reverse iteration prevents stale animation ID collisions
Enhance splitElementInHtml:
- CSS rule duplication via PostCSS for ID-based styles
- Server-side ID deduplication for repeated splits
- Media playback-start adjustment for video/audio
Add split-animations route to gsap-mutations endpoint.
Extract shared utilities to reduce duplication across timeline components:
- PlayheadIndicator: shared playhead rendering (was duplicated in
TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs
Follow-up to #1328. That PR loosened the Cursor TERM_PROGRAM check from exact
`=== "cursor"` to `?.toLowerCase() === "cursor"` "for parity with Windsurf" —
but the parity is false. Windsurf is matched case-insensitively because its
sources genuinely disagree on casing ("windsurf" vs "Windsurf"); Cursor
consistently emits lowercase "cursor", so nothing justified loosening an
existing, working, exact-match rule. Per review feedback on #1328
(Magi/Hermes), revert Cursor to exact match and drop the TERM_PROGRAM=Cursor
test. Windsurf stays case-insensitive (sourced); its comment now documents the
asymmetry as intentional.
No functional change — Cursor always emitted lowercase, so detection is
unchanged; this just removes an unsourced false-positive surface.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Rebased onto main after #1294 merged. Adds four coding-agent vendors to
detectAgentRuntime() (existence-only checks, source/runtime-verified):
- windsurf — TERM_PROGRAM=windsurf (case-insensitive)
- cline — CLINE_ACTIVE (default vscode-terminal path)
- gemini_cli — GEMINI_CLI (runtime-confirmed; distinct from the managed-agent
/.agents/ detector, which runs ahead of VENDOR_RULES and wins when both match)
- crush — CRUSH (runtime-confirmed)
Also makes the cursor rule case-insensitive for parity with windsurf, and adds
a code-resident "deliberately NOT added" section (OpenHands/Aider/Goose/
opencode/Roo/Amp/Devin/Jules/Factory) carrying the empirical rejection
rationale.
Test isolation: the Gemini managed-agent suite now clears its node:os/node:fs
doMock registrations in afterEach so they don't leak into the env-var-only
suites that follow it in the same file.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>