Skip to content

fix(studio): allow hfId-only clips to move and trim on the timeline - #3334

Open
miguel-heygen wants to merge 2 commits into
mainfrom
fix/timeline-move-hfid
Open

fix(studio): allow hfId-only clips to move and trim on the timeline#3334
miguel-heygen wants to merge 2 commits into
mainfrom
fix/timeline-move-hfid

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes "This clip can't be moved or resized from the timeline yet" on clips that are perfectly well-formed.

Root cause

findTimelineDomNodeForClip ends with a positional fallback:

return candidates[fallbackIndex] ?? null;

candidates is only nodes carrying [data-start]. A clip whose own element has none — an expanded child row, for instance — is never among them, so it falls through and is handed an unrelated node by index.

That corrupts two clips per collision:

  • the caller gets a host that is not its element, and
  • the clip that actually owned that node is starved to null, losing hfId, domId and selector — and with them canMove.

Instrumented on a six-scene composition, the resolver's own decisions:

clip s=0    d=14  trk=2 tag=img | branch=positional picked=hf-opwy   ← took the 6.0s scene div
clip s=0    d=14  trk=3 tag=img | branch=positional picked=hf-bxz4   ← took the 11.2s scene div
clip s=6    d=2.6 trk=1 tag=div | branch=none       picked=null      ← starved → refused
clip s=11.2 d=1.5 trk=1 tag=div | branch=none       picked=null      ← starved → refused

Those two starved scenes were exactly the two clips the timeline refused to move. The other four resolved normally and dragged fine.

Fix

Require the positional candidate to share the clip's tag. Same-tag attribute drift still resolves, which is what the fallback exists for; cross-tag theft cannot happen.

Also included (found while chasing this, kept because it is a real latent inconsistency): hasPatchableTimelineTarget accepts hfId, matching resolveDomEditCapabilities (Boolean(args.selector || args.hfId)) and timelineElementSplit.ts (Boolean(el.hfId || el.domId || el.selector)). TimelineElement already carries hfId, findElementForSelection resolves data-hf-id ahead of id/selector, and the move handler's primary persist is sdkTimingPersist(element.hfId, …).

Verification

End to end, in a Studio built from this branch:

  1. Opened a composition whose scene clips are plain <div class="s clip" data-start data-duration>.
  2. Dragged the clip that was previously refused.
  3. No toast. Timeline row moved 6.0 → 13.3.
  4. Source file updated: data-start="6"data-start="13.28".

Tests77/77 in the two touched files with the project's vitest config:

  • returns null instead of stealing a div for an img clipfails on main, passes here.
  • leaves the div clips resolvable after an unmatched img clip.
  • still allows a same-tag positional fallback when attributes drift — pins the behaviour the fallback is for, so this is not an over-fix.
  • 4 capability tests for hfId, two of which fail on main.

lint, format, fallow, typecheck and commitlint all pass via pre-commit hooks.

Note on scope

This supersedes my earlier framing of this PR. The hfId change alone did not fix dragging — I verified that and said so — and the positional fallback is the real cause. Both commits are here; the second is the fix.

Dragging a clip that has no author-written id showed "This clip can't be
moved or resized from the timeline yet", even though Studio could already
patch that exact element from the canvas.

hasPatchableTimelineTarget only accepted domId or selector. Studio stamps
data-hf-id into the source, TimelineElement already carries hfId, and
findElementForSelection resolves data-hf-id ahead of id and selector, so
the write path supported these clips the whole time — only the capability
gate did not. resolveDomEditCapabilities already treats hfId as a stable
target, so the two paths disagreed about the same element.

Accept hfId in the gate, and pass it through the two call sites that map
TimelineElement fields by hand (canMoveTimelineElement, canTrimEdge); the
call sites that forward the whole element already had it.

Clips with implicit timing or on a locked row stay blocked.
@miguel-heygen miguel-heygen changed the title fix(studio): let hfId-only clips move and trim on the timeline fix(studio): accept hfId as a patchable timeline target (does NOT fix clip dragging) Aug 18, 2026
… node

This is the actual cause of "This clip can't be moved or resized from the
timeline yet" on well-formed clips.

findTimelineDomNodeForClip falls back to candidates[fallbackIndex] when a
clip matches nothing by identity or attributes. Candidates are only nodes
carrying [data-start], so a clip whose element has none — an expanded child
row, for instance — is never among them, and the fallback hands it an
unrelated node.

That corrupts two clips per collision: the caller gets a host that is not
its element, and the clip that owned that node is starved to null, losing
hfId, domId and selector, and with them canMove.

Observed on a six-scene composition: two img clips took the scene divs at
6.0s and 11.2s positionally, and those two scenes were the only ones the
timeline refused to move.

Require the positional candidate to share the clip's tag. Same-tag
attribute drift still resolves, which is what the fallback is for.

Verified end to end: the previously refused clip now drags and its new
data-start is written back to the source file.
@miguel-heygen miguel-heygen changed the title fix(studio): accept hfId as a patchable timeline target (does NOT fix clip dragging) fix(studio): fix clip dragging — positional fallback stole another clip's DOM node Aug 18, 2026
@miguel-heygen miguel-heygen changed the title fix(studio): fix clip dragging — positional fallback stole another clip's DOM node fix(studio): allow hfId-only clips to move and trim on the timeline Aug 18, 2026
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.

1 participant