Skip to content

fix: gate studio timeline actions by capability#415

Merged
miguel-heygen merged 2 commits intomainfrom
fix/studio-timeline-capabilities
Apr 22, 2026
Merged

fix: gate studio timeline actions by capability#415
miguel-heygen merged 2 commits intomainfrom
fix/studio-timeline-capabilities

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 22, 2026

Summary

  • gate timeline actions to clips Studio can control deterministically
  • disable direct move/trim for generic GSAP-timed DOM clips
  • add an in-clip Copy to Agent fallback for unsupported edits

Why

Studio should only advertise timeline actions it can round-trip to source HTML with deterministic meaning.

This PR now follows that stricter rule:

  • direct move/end-trim are only exposed for clips with a deterministic timeline window
  • start trim is only exposed for clips with a real content-offset model
  • unsupported motion clips now offer Copy to Agent so users still have a fast path to request source-level timing changes

In practice this means generic GSAP-authored DOM clips no longer pretend Studio can rewrite their visible timing just by patching data-start / data-duration.

What changed

  • added hasPatchableTimelineTarget() and getTimelineEditCapabilities() in timelineEditing.ts
  • tightened deterministic-window detection so only media, images, and composition hosts keep direct move/end-trim controls
  • kept wrapped media clips editable by recognizing real media metadata even when the host tag is a div
  • updated TimelineClip / Timeline to guard interactions with the shared capability model
  • added buildTimelineElementAgentPrompt() and a Copy to Agent fallback button for unsupported clips
  • added focused tests for capability derivation and the agent-prompt helper

Verification

Automated

  • bun test packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/Timeline.test.ts packages/studio/src/player/store/playerStore.test.ts packages/studio/src/utils/sourcePatcher.test.ts
  • bun run --filter @hyperframes/studio typecheck
  • bunx oxlint packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/TimelineClip.tsx
  • bunx oxfmt --check packages/studio/src/player/components/Timeline.tsx packages/studio/src/player/components/timelineEditing.ts packages/studio/src/player/components/timelineEditing.test.ts packages/studio/src/player/components/TimelineClip.tsx

Browser

Verified in Studio with live browser automation against http://127.0.0.1:4175/#project/timeline-edit-playground:

  • generic GSAP-timed clips (feature-card, title-card, prompt-card) show Copy to Agent and no direct move/trim affordances
  • wrapped media (media-card) still exposes direct controls and remains draggable
  • the local playground timings were realigned to match the authored GSAP positions, so preview visibility now matches the timeline windows during manual testing

Recording artifacts used during verification:

  • /tmp/timeline-capabilities-proof/capabilities-flow.webm
  • /tmp/timeline-capabilities-proof/capabilities-agent-flow.webm

@miguel-heygen miguel-heygen marked this pull request as ready for review April 22, 2026 16:14
@miguel-heygen miguel-heygen self-assigned this Apr 22, 2026
@miguel-heygen miguel-heygen requested review from jrusso1020 and vanceingalls and removed request for jrusso1020 and vanceingalls April 22, 2026 16:14
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The capability model is the right abstraction for this problem — previously canOffsetTrimClipStart was the only gate and every other timeline action assumed patchability implicitly; centralizing that into getTimelineEditCapabilities and then wiring cursor / handle opacity / handle pointer-events / drag + resize handlers through the same struct makes the rules discoverable and the UI honest. Tests cover the three meaningful variants. Clean PR.

A few staff-lens observations, in rough order of impact — none are blockers:

1. Double capability computation across Timeline.tsx and TimelineClip.tsx.
Both files call getTimelineEditCapabilities(el) per render — Timeline inside els.map() (L1085ish), TimelineClip inside its render (L63). Prefer computing once in Timeline and passing capabilities as a prop into TimelineClip. Two wins: avoids redundant work per-clip per-frame, and removes the risk of the two call sites drifting if the function ever grows a memoization/options parameter. Minor cleanup, easy to fold in.

2. Input shape duplicates a subset of TimelineElement.
The structural input to getTimelineEditCapabilities, hasPatchableTimelineTarget, and canOffsetTrimClipStart is a hand-written mirror of { tag, duration, domId?, selector?, playbackStart?, playbackStartAttr?, sourceDuration? }. Since every production caller passes a TimelineElement, typing the input as Pick<TimelineElement, "tag" | "duration" | "domId" | "selector" | "playbackStart" | "playbackStartAttr" | "sourceDuration"> (or just TimelineElement outright) auto-picks up new fields when the store grows — and, in particular, means a future field like a new trim-eligibility signal can't silently get forgotten here. Tests can still pass plain objects because Pick/TimelineElement fields are all optional except tag/duration.

3. API surface — canOffsetTrimClipStart vs getTimelineEditCapabilities side-by-side.
Now that the composite function is the documented entry point, canOffsetTrimClipStart is really an internal detail of "can we trim the start?". Leaving it as a peer export invites a future maintainer to gate on it directly and miss the canPatch && hasFiniteDuration preconditions. Consider @internal jsdoc, or un-exporting it and inlining into getTimelineEditCapabilities (the test for it would move to testing that capability slice of the composite function — arguably more meaningful anyway).

4. Silent semantic change on end-trim: now requires finite, positive duration.
Before this PR, end-trim pointer-events were gated only on onResizeStart, with handle opacity gated on showHandles. After this PR, both go through capabilities.canTrimEnd which adds Number.isFinite(duration) && duration > 0. For any clip with duration === Infinity or 0 that previously exposed an end-trim affordance, this silently removes it. Likely intentional (you can't write a new data-duration from nothing) — I'd just call it out explicitly in the PR body so the next person hunting a regression has the receipt. The current body says "end trim is valid when Studio can patch data-duration" which hides the finite-duration precondition under the noun phrase.

5. Test coverage gaps (low priority, fold in only if easy):

  • duration === 0 and duration === Infinity on an otherwise patchable clip → asserts canTrimEnd: false, canTrimStart: false. The > 0 and isFinite guards are stricter than the prior behavior — one test each locks that semantic in.
  • Clip with domId but no tag / invalid tag → confirms hasPatchableTimelineTarget alone isn't enough for start-trim.
  • Combo with canOffsetTrimClipStart's existing media-source-duration branch, to show it still participates in the composite.

Non-issues I checked:

  • TimelineElement on main has domId / selector / playbackStartAttr / sourceDuration / playbackRate as optional fields — good, the runtime el actually carries these. (I was briefly worried until I re-fetched main.)
  • The cursor change in TimelineClip (capabilities.canMove ? "grab" : "default") is consistent with the pointer-down early-return in Timeline.tsx — no UX dead zone where the cursor says "grab" but the clip won't drag.
  • Existing resolveTimelineResize math is untouched; this PR is purely gating.

Review by Rames Jusso

@miguel-heygen miguel-heygen merged commit 5a4dd8b into main Apr 22, 2026
19 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants