Skip to content

feat: lock timeline tracks so clip edits don't move them - #2378

Open
YoavLevavi wants to merge 2 commits into
CapSoftware:mainfrom
YoavLevavi:feat/track-lock
Open

YoavLevavi wants to merge 2 commits into
CapSoftware:mainfrom
YoavLevavi:feat/track-lock

Conversation

@YoavLevavi

@YoavLevavi YoavLevavi commented Sep 29, 2026 •

Copy link
Copy Markdown

What

Adds a lock toggle to each timeline track header. A locked track keeps its segments where they are when clips are edited, instead of rippling with them.

Clip edits that normally shift the other tracks leave locked tracks alone:

  • deleting clips
  • trimming clips
  • retiming clips
  • adding transitions
  • inserting clips
  • cutting from the transcript

This is useful when, for example, keyboard or text overlays are already placed against the final timing and you keep tightening the clip underneath. With nothing locked, behavior is unchanged.

  • The lock button appears on hover, and stays visible and accented while a track is locked.
  • Locks are saved in the project.
  • Locks survive applying a preset and aren't stored in presets.

Implementation

  • ProjectConfiguration.locked_tracks: Vec<LockableTimelineTrack>, with #[serde(default, skip_serializing_if = "Vec::is_empty")]. Existing projects are unaffected.
  • timeline-utils.ts: unlockedRippleTracks filters locked tracks out of the boundary-ripple set, and the delete ripple skips them. context.ts, ClipsSidebar and TranscriptPage route their clip edits through these helpers.
  • Keyboard segments have their own ripple, which also respects the lock.

Testing

  • timeline-utils tests cover locked tracks staying in place on clip delete, and being filtered out of boundary ripples.
  • Desktop Vitest suite passes; Biome and tsc --noEmit are clean; cargo clippy -p cap-project -D warnings is clean and cap-project tests pass.

🤖 Generated with Claude Code

RetriggerConfidence Score: 2/5

The PR is not ready to merge because clip deletion can misalign overlays, lane locks affect unintended lanes, and applying Default clears locks.

Findings

  1. P1 Locked holds misalign overlays ▶
  2. P1 One lock affects every lane ▶
  3. P1 Default preset clears locks ▶
Fix with agent prompt
### Issue 1
apps/desktop/src/routes/editor/timeline-utils.ts:484
When a locked text track has a fullscreen hold inside a deleted clip, this guard leaves the hold in place. But the shift applied to unlocked overlays still counts that hold as removed, moving overlays after the cut too far and misaligning them with the remaining timeline.

### Issue 2
apps/desktop/src/routes/editor/Timeline/index.tsx:1830-1838
The button appears on each lane, but it stores only the track kind. When a project has multiple audio or overlay lanes of the same kind, locking one also locks the others, so clip edits stop rippling lanes the user did not lock.

### Issue 3
apps/desktop/src/routes/editor/PresetsDropdown.tsx:108
This path preserves the project's locks when applying a saved preset, but applying Default uses the stock configuration's empty lock list instead. If tracks are locked, applying Default silently unlocks them.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds persisted, per-kind timeline locks and skips locked tracks during clip-edit ripples and retiming.

  • Adds track-header lock controls and Rust/TypeScript configuration types.
  • Preserves locks when applying saved presets and excludes them from stored presets.
  • Adds tests for locked-track deletion and boundary-ripple filtering.

Reviews (1) · Last reviewed commit: "feat: lock timeline tracks so clip edits..."

A lock toggle on each track header keeps that track's segments where they
are when clips are deleted, trimmed, retimed, given transitions, inserted,
or cut from the transcript. Locks live in the project, survive preset
application, and aren't stored in presets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
overlayShift,
);
if (timeline.textSegments)
if (timeline.textSegments && ripples("text"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Locked holds misalign overlays When a locked text track has a fullscreen hold inside a deleted clip, this guard leaves the hold in place. But the shift applied to unlocked overlays still counts that hold as removed, moving overlays after the cut too far and misaligning them with the remaining timeline.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/timeline-utils.ts
Line: 484

Comment:
**Locked holds misalign overlays** When a locked text track has a fullscreen hold inside a deleted clip, this guard leaves the hold in place. But the shift applied to unlocked overlays still counts that hold as removed, moving overlays after the cut too far and misaligning them with the remaining timeline.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 36c0ba2. The hold time inside the cut is now added to the overlay shift only when the text track ripples. With text locked, the hold stays, so unlocked overlays after the cut shift by the removed clip time alone. Added keeps a locked hold's time when cutting the clip around it, which fails without the fix.

Comment on lines +1830 to +1838
if (!kind) return;
const current = project.lockedTracks ?? [];
setProject(
"lockedTracks",
current.includes(kind)
? current.filter((track) => track !== kind)
: [...current, kind],
);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 One lock affects every lane The button appears on each lane, but it stores only the track kind. When a project has multiple audio or overlay lanes of the same kind, locking one also locks the others, so clip edits stop rippling lanes the user did not lock.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/Timeline/index.tsx
Line: 1830-1838

Comment:
**One lock affects every lane** The button appears on each lane, but it stores only the track kind. When a project has multiple audio or overlay lanes of the same kind, locking one also locks the others, so clip edits stop rippling lanes the user did not lock.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The lock is intentionally per track kind: lockedTracks is a list of kinds, and ripple logic runs per kind. Every lane of that kind shows the same locked state, so it's visible rather than silent. Per-lane locks would need stable lane identity, which audio and overlay lanes don't have today; lanes are positional and renumber on reorder or delete. In 36c0ba2 the tooltip and aria-label now say the lock applies to every lane of that kind, e.g. "Lock audio segments on every audio lane…". Happy to move to per-lane locks if you'd like lanes to get stable IDs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That makes sense. Given that locks are intentionally scoped to LockableTimelineTrack kinds and the UI now explicitly communicates that locking applies to every lane of that kind, the shared behavior is visible and deliberate rather than an unintended cross-lane side effect. Stable per-lane identity would be a separate design change, so I’m withdrawing this finding.

timeline: project.timeline ?? null,
overlayOrder: project.overlayOrder ?? [],
clips: project.clips,
lockedTracks: project.lockedTracks ?? [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Default preset clears locks This path preserves the project's locks when applying a saved preset, but applying Default uses the stock configuration's empty lock list instead. If tracks are locked, applying Default silently unlocks them.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/PresetsDropdown.tsx
Line: 108

Comment:
**Default preset clears locks** This path preserves the project's locks when applying a saved preset, but applying Default uses the stock configuration's empty lock list instead. If tracks are locked, applying Default silently unlocks them.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 36c0ba2. applyDefaultPreset now carries project.lockedTracks through, the same as saved presets.

- keep a locked fullscreen hold's time out of the overlay ripple shift
- keep track locks when applying the Default preset
- say in the lock tooltip that it applies to every lane of that kind

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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