Skip to content

fix(timeline): close the same ambiguity in playback and auto-zoom - #221

Merged
EtienneLescot merged 2 commits into
release/v1.8.0from
claude/shared-recording-clips-bugs-a449bf
Aug 1, 2026
Merged

fix(timeline): close the same ambiguity in playback and auto-zoom#221
EtienneLescot merged 2 commits into
release/v1.8.0from
claude/shared-recording-clips-bugs-a449bf

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

v7 gave trims a clipId, but three readers still answered "which clip is this?" from (assetId, sourceTime) — or from array position. Source time is per asset, so the moment two clips draw on the same media it names two places at once. One cause, both reported symptoms, and two adjacent defects found while verifying.

Playback stops on reaching the second clip

isWithinClipBounds gave a clip an exclusive closing edge (sourceEndSec - ε) unless it was the last element of the clips array, and the preferredClipId guard shared that edge. In a clip's final ~50 ms — exactly when the rAF loop decides what to play next — the clip disowned its own last frames, and the ambiguous scan handed them to its twin, reporting the playhead near the end of that twin. reachedClipEnd then fired on a clip nothing follows: pause, playhead parked at the end of the timeline.

Because the exception keyed off array position, the bug depended on clip order, which is what the reporter's experiments isolated:

Layout Before
A1 → A2 → C3 no bug — the last clip belongs to another asset, the filter excludes it, the scan returns null, and the timeline-order fallback did the right thing by accident
A1 → C3 → A2 bug
C3 → A1 → A2 bug
A1 → A2 / A2 → A1 bug

Fix: an inclusive closing edge when the clip is named — identity beats proximity, the exclusive bound exists only to break ties in the scan, and a named clip has no tie to break — plus a two-pass scan (strict containment first, closing edge only if nobody claims the instant). Same answer on a plain single-asset timeline, reached without consulting the order.

"Automatic zooms" only decorates the first clip

Cursor telemetry is recorded against the original file: timeMs is the asset's source time (the axis cursor-track.ts maps through locateSourcePosition). Zoom regions are authored in raw timeline ms. The two axes coincide for exactly one layout: a single clip, at 0, covering the whole recording. Anywhere else, suggestions landed wherever [0, assetDuration] falls on the ruler — the first clip. Telemetry was also only read for videoSources[0], so a second recording was never consulted at all.

buildAutoZoomSuggestionsForClips does the projection, per clip and by a plain shift (a raw clip is identity between its source and raw-virtual time). A dwell replayed by two clips therefore yields one zoom on each.

Two adjacent defects, found while verifying

  • A cut was never skipped during playback if a twin kept that stretch. locateKeptSegment narrows "am I inside a cut?" to the segments of the clip being played.
  • Playback could loop forever. findNextKeptSegment's source-clock fallback compared source positions across a whole asset. "Later in source time" only means something within one clip: with a slice from late in a recording laid down before a trimmed slice from early in it, playing into that cut answered the first clip — raw start 0 — so playback jumped to the top of the timeline, fell into the same cut, and looped. Pre-existing, but the fix above makes that path far more reachable: leaving it would have traded one bug for a worse one.

Deliberately not touched

totalVirtualDuration (clips.at(-1)) and locateVirtualPosition's index === clips.length - 1 still depend on array order. resequenceClips maintains that order, so neither is reachable today, and locateVirtualPosition has five callers: that is a separate change, not something to smuggle into a bugfix.

Tests

Every fix was verified failing without it. The integration test (VirtualPreview.playback.test.tsx, rAF loop driven by hand) reproduces the symptom end to end on the pre-fix code — pause() at 9.96 s. No existing test covered the closing edge of a clip with a twin over the same media.

npm run test: 1419 tests / 117 files ✅ · tsc --noEmit ✅ · biome check ✅ · check-docs ✅ · i18n:check

technical-documentation/architecture/timeline-model.md gains the playback and telemetry sections, plus the invariant: clip order is never an input.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved preview playback when clips reuse the same source, ensuring boundaries and trims resolve against the correct clip.
    • Prevented playback from stopping prematurely when clips overlap or are arranged in different orders.
    • Improved playback when navigating across edited or removed segments.
  • New Features

    • Auto-zoom suggestions now apply independently to each applicable timeline clip, including repeated source clips, while preserving existing zoom regions.
  • Documentation

    • Added documentation describing clip-aware playback and auto-zoom behavior.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4d384e90-ef6d-4b41-9f34-5b01087a3697

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes make playback resolution clip-aware and add regression coverage for overlapping clips, boundaries, trims, and source-time fallback. Automatic zoom generation now processes telemetry per applicable timeline clip and projects suggestions into raw timeline coordinates.

Changes

Clip-aware playback disambiguation

Layer / File(s) Summary
Clip-scoped playback resolution
src/lib/ai-edition/timeline/virtual-preview.ts
Playback lookup prioritizes active clip identity, handles closing boundaries independently of array order, and restricts source-time fallback to the active clip.
VirtualPreview transition handling
src/components/ai-edition/VirtualPreview.tsx, src/components/ai-edition/VirtualPreview.playback.test.tsx
VirtualPreview uses active clip context for trim and next-segment resolution. Tests cover same-source transitions, clip ordering, and clip-specific trims.
Playback behavior validation and model rules
src/lib/ai-edition/timeline/virtual-preview.test.ts, technical-documentation/architecture/timeline-model.md
Tests and architecture documentation cover duplicate assets, boundaries, stale identities, forward resumption, and source-time fallback.

Clip-aware automatic zoom

Layer / File(s) Summary
Per-clip zoom suggestion projection
src/lib/ai-edition/timeline/zoom-suggestions.ts
buildAutoZoomSuggestionsForClips filters telemetry by asset and source window, rebases coordinates, and returns raw-timeline suggestions.
Timeline auto-zoom integration
src/components/ai-edition/v4/V4Timeline.tsx
Automatic zoom generation processes all sources represented by timeline clips and submits accumulated suggestions while preserving existing regions.
Auto-zoom validation and model rules
src/lib/ai-edition/timeline/zoom-suggestions.test.ts, technical-documentation/architecture/timeline-model.md
Tests and documentation cover repeated clips, coordinate translation, filtering, overlap suppression, and missing durations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VideoPlayback
  participant VirtualPreview
  participant PlaybackResolver
  participant RawClip
  VideoPlayback->>VirtualPreview: report current source time
  VirtualPreview->>PlaybackResolver: locateKeptSegment with activeClipId
  PlaybackResolver->>RawClip: resolve active clip source window
  PlaybackResolver-->>VirtualPreview: return kept segment
  VirtualPreview->>VideoPlayback: seek next segment and continue playback
Loading
sequenceDiagram
  participant V4Timeline
  participant CursorTelemetry
  participant buildAutoZoomSuggestionsForClips
  participant ZoomRegions
  V4Timeline->>CursorTelemetry: fetch telemetry per source
  V4Timeline->>buildAutoZoomSuggestionsForClips: pass clips and telemetry
  buildAutoZoomSuggestionsForClips->>ZoomRegions: rebase existing regions per clip
  buildAutoZoomSuggestionsForClips-->>V4Timeline: return raw-timeline suggestions
  V4Timeline->>ZoomRegions: submit accumulated suggestions
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the timeline ambiguity fixes for playback and auto-zoom.
Description check ✅ Passed The description thoroughly explains the changes, affected behavior, tests, and documentation, although it does not follow the template headings exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/shared-recording-clips-bugs-a449bf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/components/ai-edition/v4/V4Timeline.tsx (1)

899-925: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fetch telemetry for each source in parallel.

The for loop awaits nativeBridgeClient.cursor.getTelemetry one source at a time. Each iteration blocks on an IPC round trip before starting the next. With multiple recordings on the timeline, this adds up the latency of every fetch instead of overlapping them.

existingRegions is read once before the loop and does not depend on iteration order, so the fetches can run concurrently without changing behavior.

⚡ Proposed fix to parallelize telemetry fetches
 			const existingRegions = tl.zoomRegions.map((z) => ({ startMs: z.startMs, endMs: z.endMs }));
-			const suggestions: AutoZoomSuggestion[] = [];
-			for (const source of sources) {
-				const telemetry =
-					(await nativeBridgeClient.cursor.getTelemetry(fromFileUrl(source.src))) ?? [];
-				suggestions.push(
-					...buildAutoZoomSuggestionsForClips({
-						cursorTelemetry: telemetry,
-						assetId: source.id,
-						clips,
-						existingRegions,
-						defaultDurationMs: 2000,
-					}),
-				);
-			}
+			const perSourceSuggestions = await Promise.all(
+				sources.map(async (source) => {
+					const telemetry =
+						(await nativeBridgeClient.cursor.getTelemetry(fromFileUrl(source.src))) ?? [];
+					return buildAutoZoomSuggestionsForClips({
+						cursorTelemetry: telemetry,
+						assetId: source.id,
+						clips,
+						existingRegions,
+						defaultDurationMs: 2000,
+					});
+				}),
+			);
+			const suggestions: AutoZoomSuggestion[] = perSourceSuggestions.flat();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ai-edition/v4/V4Timeline.tsx` around lines 899 - 925, Update
runAutoZooms to fetch telemetry for all sources concurrently instead of awaiting
each nativeBridgeClient.cursor.getTelemetry call sequentially. Use Promise.all
while preserving the existing source-to-asset association and existingRegions
handling, then build AutoZoomSuggestion results from the completed telemetry
responses without changing current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ai-edition/VirtualPreview.playback.test.tsx`:
- Around line 185-187: Update the AxcutTrimRange fixture in VirtualPreview
playback tests to include the required reason and origin fields, using values
consistent with the AxcutTrimRange type and existing test conventions so tsc
--noEmit passes.

In `@src/lib/ai-edition/timeline/virtual-preview.test.ts`:
- Around line 291-293: Update the AxcutTrimRange fixture passed to
resolvePlaybackSegments in virtual-preview.test.ts by adding the required reason
and origin fields, matching the established fixture shape used in
VirtualPreview.playback.test.tsx while preserving the existing trim values.
- Around line 451-453: Update the AxcutTrimRange fixture passed to
resolvePlaybackSegments in the playbackClips test setup to include the required
reason and origin fields, matching the complete fixture shape used elsewhere.
Keep the existing id, assetId, clipId, and trim range values unchanged.

In `@src/lib/ai-edition/timeline/virtual-preview.ts`:
- Around line 143-160: Update isWithinClipBounds to default an absent
clip.sourceEndSec to clip.sourceStartSec, matching resolvePlaybackSegments.
Preserve the existing inclusive/exclusive epsilon handling while ensuring
unprobed clips with nonzero sourceStartSec can match their source content.

---

Nitpick comments:
In `@src/components/ai-edition/v4/V4Timeline.tsx`:
- Around line 899-925: Update runAutoZooms to fetch telemetry for all sources
concurrently instead of awaiting each nativeBridgeClient.cursor.getTelemetry
call sequentially. Use Promise.all while preserving the existing source-to-asset
association and existingRegions handling, then build AutoZoomSuggestion results
from the completed telemetry responses without changing current behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca52d276-811c-4aff-afa3-d6f88b0e405f

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0ec8a and 3db6541.

📒 Files selected for processing (8)
  • src/components/ai-edition/VirtualPreview.playback.test.tsx
  • src/components/ai-edition/VirtualPreview.tsx
  • src/components/ai-edition/v4/V4Timeline.tsx
  • src/lib/ai-edition/timeline/virtual-preview.test.ts
  • src/lib/ai-edition/timeline/virtual-preview.ts
  • src/lib/ai-edition/timeline/zoom-suggestions.test.ts
  • src/lib/ai-edition/timeline/zoom-suggestions.ts
  • technical-documentation/architecture/timeline-model.md

Comment thread src/components/ai-edition/VirtualPreview.playback.test.tsx
Comment thread src/lib/ai-edition/timeline/virtual-preview.test.ts
Comment thread src/lib/ai-edition/timeline/virtual-preview.test.ts
Comment thread src/lib/ai-edition/timeline/virtual-preview.ts
@EtienneLescot EtienneLescot changed the title fix(timeline): lever la même ambiguïté à la lecture et sur l'auto-zoom fix(timeline): close the same ambiguity in playback and auto-zoom Aug 1, 2026
@EtienneLescot
EtienneLescot force-pushed the claude/shared-recording-clips-bugs-a449bf branch from f3f7071 to 5a9334f Compare August 1, 2026 11:43
@EtienneLescot
EtienneLescot changed the base branch from main to release/v1.8.0 August 1, 2026 12:03
v7 gave trims a `clipId`, but three READERS still answered "which clip is this?"
from `(assetId, sourceTime)` — or from POSITION IN THE ARRAY. Source time is per
asset: the moment two clips draw on the same media, it names two places at once.
One cause, the two reported symptoms, and two adjacent defects found while
verifying.

--- Playback stops on reaching the second clip

`isWithinClipBounds` gave a clip an EXCLUSIVE closing edge unless it was the LAST
ELEMENT OF THE ARRAY, and the `preferredClipId` guard shared that edge. In its
final ~50 ms — exactly when the rAF loop decides what comes next — a clip disowned
its own last frames and the ambiguous scan handed them to its twin, placing the
playhead near the END of that twin. `reachedClipEnd` then fired on a clip nothing
follows: pause, playhead parked at the end of the timeline.

Because the exception keyed off array position, the bug DEPENDED ON CLIP ORDER,
which is what the reporter's experiments isolated: `A1 -> A2 -> C3` did not bug
(the last clip belongs to another asset, the filter excludes it, the scan returns
null, and the timeline-order fallback did the right thing by accident);
`A1 -> C3 -> A2`, `C3 -> A1 -> A2` and any pair of twins did. All six layouts are
in the test.

An INCLUSIVE bound when the clip is named — identity beats proximity, the
exclusive bound exists only to break ties in the scan and a named clip has no tie
to break — plus a two-pass scan: strict containment first, closing edge only if
nobody claims the instant. Same answer on a single-asset timeline, reached without
consulting the order.

--- "Automatic zooms" only decorates the first clip

Cursor telemetry is recorded against the ORIGINAL file: `timeMs` is the asset's
SOURCE time (the axis `cursor-track.ts` maps through `locateSourcePosition`).
Zooms are authored in RAW timeline ms. The two axes coincide for one layout only:
a single clip, at 0, covering the whole recording. Anywhere else, suggestions
landed where `[0, asset duration]` falls on the ruler — the first clip. Telemetry
was also read only for `videoSources[0]`, so a second recording was never
consulted.

`buildAutoZoomSuggestionsForClips` does the projection, per clip and by a plain
shift (a raw clip is identity between its source and raw-virtual time). A dwell
replayed by two clips therefore yields one zoom on EACH. Each clip sees only the
samples in its own source window: a dwell a cut splits is no longer one dwell,
which is right — the cursor did not sit still across the cut on the timeline being
watched.

--- Two adjacent defects, found while verifying

A cut was never skipped during playback if a twin kept the stretch: "am I inside a
cut?" scanned every segment by asset. `locateKeptSegment` narrows the question to
the segments of the clip being played — a source time none of them covers is
inside THAT clip's cut, however many other clips keep it.

`findNextKeptSegment`'s source-clock fallback compared source positions across a
whole asset. "Later in source time" only means something within one clip: with a
late slice laid down BEFORE a trimmed early slice, entering that cut answered the
first clip — raw start 0 — so playback jumped to the top of the timeline, fell into
the same cut, and looped. Pre-existing, but the fix above makes that path far more
reachable: leaving it would have traded one bug for a worse one.

--- What is NOT touched

`totalVirtualDuration` (`clips.at(-1)`) and `locateVirtualPosition`'s
`index === clips.length - 1` still depend on array order. `resequenceClips`
maintains that order, so neither is reachable today, and `locateVirtualPosition`
has five callers: that is a separate change, not something to smuggle into a
bugfix.

Tests: every fix was verified FAILING without it. The integration test reproduces
the symptom end to end on the pre-fix code (`pause()` at 9.96 s). No existing test
covered the closing edge of a clip with a twin over the same media.
The three new AxcutTrimRange fixtures omitted `reason` / `origin`. Invisible to
`tsc --noEmit`, which excludes `**/*.test.ts(x)` — `tsconfig.test.json` exists
precisely for what the main config leaves out, and it is the gate CI runs. Caught
by review, not by me: I checked the wrong one of the two typecheck jobs.

Two review points taken while here:

- `isWithinClipBounds` defaulted a missing `sourceEndSec` to 0 (as it always had),
  where `resolvePlaybackSegments` reads the same absent field as a zero-width
  window at the in-point. `locateKeptSegment` now feeds this function that very
  output, so the two sit in series and must not read one field two ways. No
  behaviour changes: a clip only awaits probing with `sourceStartSec === 0`, where
  both defaults coincide. A divergence removed, not a bug.
- The per-asset telemetry fetches ran one IPC round trip after another. Nothing in
  the loop depends on visit order (`existingRegions` is read up front), so they now
  run concurrently; `Promise.all` preserves order, so the output is identical.
@EtienneLescot
EtienneLescot force-pushed the claude/shared-recording-clips-bugs-a449bf branch from 5a9334f to dca3aa5 Compare August 1, 2026 12:49
@EtienneLescot
EtienneLescot merged commit d2428f8 into release/v1.8.0 Aug 1, 2026
15 checks passed
@EtienneLescot
EtienneLescot deleted the claude/shared-recording-clips-bugs-a449bf branch August 1, 2026 12:53
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