Skip to content

[codex] clean up clip-first timeline editing#406

Open
webadderall wants to merge 4 commits intomainfrom
codex/clip-flow-cleanup
Open

[codex] clean up clip-first timeline editing#406
webadderall wants to merge 4 commits intomainfrom
codex/clip-flow-cleanup

Conversation

@webadderall
Copy link
Copy Markdown
Collaborator

@webadderall webadderall commented May 2, 2026

Summary

This PR removes the most confusing clip/trim split-brain paths so the editor behaves more consistently as a clip-first workflow.

What changed

  • centralized clip timeline/source time mapping helpers and used them for scrubbing and timeline conversions
  • migrated trim-only legacy projects into clip regions when duration becomes available
  • removed trim and speed-region authoring shortcuts plus the hidden timeline actions that still created them
  • made zoom placement require a kept clip span with enough room before the clip ends
  • made seek handling skip removed footage even when playback is paused
  • added focused tests for the new mapping helpers and paused seek behavior

Why

The clip feature still exposed legacy trim and speed-region behaviors in shortcuts, playback, and timeline interactions. That made the product feel unpredictable: users could land in removed space, trigger hidden edit modes, or work with timeline semantics that did not match what the UI emphasized.

Impact

This keeps the existing clip cleanup behavior you wanted, while reducing the unintuitive parts around seeking, gap handling, and hidden legacy editing paths.

Validation

  • npx vitest run src/components/video-editor/types.test.ts src/components/video-editor/videoPlayback/videoEventHandlers.test.ts
  • npx tsc --noEmit

Notes

The local worktree had unrelated in-flight changes, so this PR is intentionally scoped to the staged clip/timeline files only.

Summary by CodeRabbit

  • New Features

    • Memory-optimized video export streaming with a fallback for legacy saves
    • Visual ghost playhead and ghost zoom preview; improved zoom placement validation and error feedback
    • Row-level mouse interaction handlers for timeline rows
  • Bug Fixes

    • Audio stays in sync with clip speed changes
    • Playback now skips removed footage after paused seeks
  • Refactor

    • Timeline selection and clip/time mapping reworked for more accurate undo/restore
  • Tests

    • Added mapping and playback unit tests
  • Chores

    • Updated keyboard shortcuts (added clip split; removed trim/speed actions)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Added renderer-side chunked Blob export streaming with fallback, migrated timeline state and time-mapping to a clip-centric model (new mapping utilities), introduced ghost zoom hover/placement UI, updated playback seek/trim behavior, and removed trim/speed selection/shortcuts in favor of clip-oriented controls.

Changes

Export streaming & pending-save

Layer / File(s) Summary
Stream helper
src/components/video-editor/VideoEditor.tsx
Added streamExportBlobToTempFile to write Blob chunks via window.electronAPI.openExportStream/writeExportStreamChunk/closeExportStream with abort/cleanup.
Save orchestration
src/components/video-editor/VideoEditor.tsx
Added saveBlobExport which attempts streaming and falls back to ArrayBuffer-based save; returns result and pending-save payload shape (tempFilePath or arrayBuffer).
Export callers
src/components/video-editor/VideoEditor.tsx
Routed GIF and MP4 legacy-fallback export flows through saveBlobExport and updated pending-save handling to store either streamed temp-file metadata or ArrayBuffer fallback data on dialog cancel.

Timeline & clip-model refactor

Layer / File(s) Summary
Data Shape / Utilities
src/components/video-editor/types.ts
Added sortClipRegions, mapTimelineTimeToSourceTime, mapSourceTimeToTimelineTime, findClipAtTimelineTime, plus helpers to normalize clip speed and clamp to nearest boundaries.
Unit tests
src/components/video-editor/types.test.ts
Added tests covering forward/reverse timeline↔source mapping, boundary/exclusive end behavior, snapping across gaps, and clip lookup.
Initial clip creation
src/components/video-editor/VideoEditor.tsx
When duration becomes known, prefer migrating existing trimRegions into clipRegions via trimsToClips instead of always creating a single full-track clip.
Time mapping wiring
src/components/video-editor/VideoEditor.tsx
Replaced manual per-clip conversion loops with calls to the new resolver functions parameterized by current clipRegions.
Playback / audio sync
src/components/video-editor/VideoEditor.tsx
Audio playback active-speed lookup switched to effectiveSpeedRegions (merging clip-derived speeds) so audio respects clip speed changes.
Selection & history model
src/components/video-editor/VideoEditor.tsx
Removed selectedTrimId and selectedSpeedId from history snapshots and undo/redo; introduced/used selectedClipId as selection anchor and adjusted history restore/selection clearing.
Keyboard / shortcuts
src/lib/shortcuts.ts
Removed addTrim/addSpeed actions and defaults; preserved/added splitClip action and updated labels/config shape.
Delete-key behavior
src/components/video-editor/timeline/TimelineEditor.tsx
Removed trim/speed deletion branches from delete-key flow; delete now targets clip/annotation/audio/zoom based on selection.

Timeline UI & interactions

Layer / File(s) Summary
Row event forwarding
src/components/video-editor/timeline/Row.tsx
Extended RowProps with optional onMouseEnter/onMouseMove/onMouseLeave/onClick and forwarded them to the row DOM element.
Ghost hover & zoom preview
src/components/video-editor/timeline/TimelineEditor.tsx
Added timeline and zoom-row hover state; renders ghost playhead and ghost zoom span at hovered times, gated by canPlaceZoomAtMs.
Zoom placement validation
src/components/video-editor/timeline/TimelineEditor.tsx
Introduced canPlaceZoomAtMs(startMs) requiring placement inside a kept clip with enough space before clip end/next zoom; addZoomAtMs uses validator and shows toast on invalid placement.
Timeline prop cleanup
src/components/video-editor/timeline/TimelineEditor.tsx, src/components/video-editor/VideoEditor.tsx
Removed trim/speed selection/handler props from Timeline/TimelineEditor wiring; added clip split/span/change and clip selection handlers (onClipSplit, onClipSpanChange, selectedClipId, onSelectClip).
Playhead tooltip / visibility
src/components/video-editor/timeline/TimelineEditor.tsx
Playhead tooltip now always rendered; visibility toggled with opacity based on isDragging.

Seek / playback behavior

Layer / File(s) Summary
Seek handling
src/components/video-editor/videoPlayback/videoEventHandlers.ts
handleSeeked now skips past trim regions whenever the seek lands inside one (including when paused) instead of only skipping during active playback.
Seek test
src/components/video-editor/videoPlayback/videoEventHandlers.test.ts
Added test verifying removed-footage skip after a paused seek updates video.currentTime and emits the time update.

Sequence Diagram(s)

sequenceDiagram
  participant Renderer as Renderer (VideoEditor)
  participant Main as Electron Main
  participant FS as Filesystem
  Renderer->>Main: openExportStream(filename, extension)
  Main-->>Renderer: streamId / tempFilePath
  Renderer->>Main: writeExportStreamChunk(streamId, chunk)
  Main->>FS: append chunk to temp file
  Main-->>Renderer: ack
  alt all chunks written
    Renderer->>Main: closeExportStream(streamId)
    Main-->>Renderer: finalize / return tempFilePath
  else streaming unavailable/fails
    Renderer->>Renderer: convert Blob -> ArrayBuffer
    Renderer->>Main: saveArrayBuffer(filePath, arrayBuffer)
    Main->>FS: write full buffer to path
    Main-->>Renderer: success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Checked

"I'm a rabbit in the editor's glade,
Chunks hop along and big files get made,
Clips line up where trims used to be,
Ghost zooms shimmer — hop, look and see! 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] clean up clip-first timeline editing' accurately summarizes the main objective: refactoring the editor to be clip-first and removing legacy trim/speed paths.
Description check ✅ Passed The PR description addresses the template requirements: provides clear Summary, Motivation (Why), and concrete examples of what changed. It includes a Validation section and explanation of scope.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 codex/clip-flow-cleanup
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch codex/clip-flow-cleanup

Review rate limit: 8/10 reviews remaining, refill in 12 minutes.

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

@webadderall webadderall marked this pull request as ready for review May 2, 2026 10:21
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 831-845: The click handler handleZoomRowClick is short-circuiting
invalid clicks by checking canPlaceZoomAtMs and returning early, which prevents
the placement logic and error toast inside onAddZoomAtMs from running; change it
so after computing startMs (using zoomRowHoverMs and videoDurationMs) you always
call onAddZoomAtMs(startMs) when onAddZoomAtMs and zoomRowHoverMs are present,
and remove the early-return guard that calls canPlaceZoomAtMs — keep
event.stopPropagation and the null checks for onAddZoomAtMs/zoomRowHoverMs but
do not pre-validate placement here so addZoomAtMs can handle validation and
feedback.

In `@src/components/video-editor/types.ts`:
- Around line 196-203: The clip-mapper loops (the one iterating sortedClips
using roundedTimeMs and calling getSafeClipSpeed to compute returned time) treat
clip end as inclusive; change the end check to be end-exclusive (use
roundedTimeMs >= clip.endMs rather than >) so boundary times map to the next
clip like findClipAtTimelineTime does, and apply the same half-open fix to the
other similar loop/block later in the file (the block around the other mapper at
the noted second diff).

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2774-2778: When converting legacy trims to clips in the branch
where trimRegions.length > 0 (the block that calls trimsToClips(trimRegions,
totalMs), setClipRegions(...), and marks clipInitializedRef.current = true), you
must advance nextClipIdRef.current so newly created clip ids won’t collide with
the generated "clip-*" ids; after calling trimsToClips, inspect the returned
clip objects to compute the highest numeric suffix used in their id strings
(they are created as "clip-<n>") and set nextClipIdRef.current to that max+1 (or
otherwise ensure nextClipIdRef.current is greater than any existing clip id)
before returning so subsequent split/add operations use unique ids.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e278f63-7c44-4171-afb8-2bfdc9007607

📥 Commits

Reviewing files that changed from the base of the PR and between 5090910 and 3e871b9.

📒 Files selected for processing (8)
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/timeline/Row.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.test.ts
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/videoEventHandlers.test.ts
  • src/components/video-editor/videoPlayback/videoEventHandlers.ts
  • src/lib/shortcuts.ts
💤 Files with no reviewable changes (1)
  • src/lib/shortcuts.ts

Comment thread src/components/video-editor/timeline/TimelineEditor.tsx Outdated
Comment thread src/components/video-editor/types.ts
Comment thread src/components/video-editor/VideoEditor.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2774-2781: When migrating trims to clips in the trimsToClips
branch, clear the auto-full-clip tracking refs so the temporary full-track clip
doesn't get treated as auto-generated later: after calling trimsToClips and
before returning/setting state in the block that uses migratedClips, set
autoFullTrackClipIdRef.current = null and autoFullTrackClipEndMsRef.current =
null (these refs are the ones primed earlier and used by
extendAutoFullTrackClip), ensuring extendAutoFullTrackClip cannot identify a
migrated clip as the auto-generated clip.
- Around line 2898-2917: When creating a new zoom in handleZoomAdded, normalize
selection state by clearing any clip/audio/other selections and switching the
side panel to the zoom inspector so only the new zoom is highlighted; after
setSelectedZoomId(id) (and setSelectedAnnotationId(null)) also call the relevant
setters to clear clip/audio selection (e.g. setSelectedClipId(null),
setSelectedAudioId(null), or their equivalents) and set the side panel/inspector
to the zoom view (e.g. setSidePanel("zoom") or setInspectorPanel("zoom")) so the
inspector shows the new zoom.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: df759516-a926-4ffb-aa51-eb893a75ea42

📥 Commits

Reviewing files that changed from the base of the PR and between 3e871b9 and b79cedb.

📒 Files selected for processing (4)
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.test.ts
  • src/components/video-editor/types.ts
✅ Files skipped from review due to trivial changes (2)
  • src/components/video-editor/types.test.ts
  • src/components/video-editor/types.ts

Comment on lines +2774 to +2781
if (trimRegions.length > 0) {
const migratedClips = trimsToClips(trimRegions, totalMs);
nextClipIdRef.current = deriveNextId(
"clip",
migratedClips.map((clip) => clip.id),
);
setClipRegions(migratedClips);
clipInitializedRef.current = true;
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear the auto-full-clip refs in the trim-migration branch.

Line 2774 returns early after trimsToClips(...), but autoFullTrackClipIdRef.current / autoFullTrackClipEndMsRef.current were already primed with the temporary full-track clip just above. If duration changes again, extendAutoFullTrackClip(...) can misidentify a migrated clip as auto-generated and stretch it.

Suggested fix
 				if (trimRegions.length > 0) {
 					const migratedClips = trimsToClips(trimRegions, totalMs);
+					autoFullTrackClipIdRef.current = null;
+					autoFullTrackClipEndMsRef.current = null;
 					nextClipIdRef.current = deriveNextId(
 						"clip",
 						migratedClips.map((clip) => clip.id),
 					);
 					setClipRegions(migratedClips);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 2774 - 2781, When
migrating trims to clips in the trimsToClips branch, clear the auto-full-clip
tracking refs so the temporary full-track clip doesn't get treated as
auto-generated later: after calling trimsToClips and before returning/setting
state in the block that uses migratedClips, set autoFullTrackClipIdRef.current =
null and autoFullTrackClipEndMsRef.current = null (these refs are the ones
primed earlier and used by extendAutoFullTrackClip), ensuring
extendAutoFullTrackClip cannot identify a migrated clip as the auto-generated
clip.

Comment on lines 2898 to 2917
const handleZoomAdded = useCallback(
(span: Span) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const defaultDepth: ZoomDepth = 2;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
endMs: Math.round(span.end),
depth: DEFAULT_ZOOM_DEPTH,
focus: { cx: 0.5, cy: 0.5 },
mode: "manual",
depth: defaultDepth,
focus: clampFocusToDepth({ cx: 0.5, cy: 0.5 }, defaultDepth),
mode: "auto",
};
if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}
setZoomRegions((prev) => [...prev, newRegion]);
setSelectedZoomId(id);
setSelectedTrimId(null);
setSelectedAnnotationId(null);
extensionHost.emitEvent({
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize selection state when a new zoom is created.

Line 2915 selects the new zoom, but the add flow still leaves clip/audio selection and the current side panel untouched. That can leave multiple items highlighted and keeps the inspector on the previous section instead of the newly created zoom.

Suggested fix
 			setZoomRegions((prev) => [...prev, newRegion]);
 			setSelectedZoomId(id);
+			setSelectedClipId(null);
 			setSelectedAnnotationId(null);
+			setSelectedAudioId(null);
+			setActiveEffectSection("zoom");
 			extensionHost.emitEvent({
 				type: "timeline:region-added",
 				data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs },
 			});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleZoomAdded = useCallback(
(span: Span) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const defaultDepth: ZoomDepth = 2;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
endMs: Math.round(span.end),
depth: DEFAULT_ZOOM_DEPTH,
focus: { cx: 0.5, cy: 0.5 },
mode: "manual",
depth: defaultDepth,
focus: clampFocusToDepth({ cx: 0.5, cy: 0.5 }, defaultDepth),
mode: "auto",
};
if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}
setZoomRegions((prev) => [...prev, newRegion]);
setSelectedZoomId(id);
setSelectedTrimId(null);
setSelectedAnnotationId(null);
extensionHost.emitEvent({
const handleZoomAdded = useCallback(
(span: Span) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const defaultDepth: ZoomDepth = 2;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
endMs: Math.round(span.end),
depth: defaultDepth,
focus: clampFocusToDepth({ cx: 0.5, cy: 0.5 }, defaultDepth),
mode: "auto",
};
if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}
setZoomRegions((prev) => [...prev, newRegion]);
setSelectedZoomId(id);
setSelectedClipId(null);
setSelectedAnnotationId(null);
setSelectedAudioId(null);
setActiveEffectSection("zoom");
extensionHost.emitEvent({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/VideoEditor.tsx` around lines 2898 - 2917, When
creating a new zoom in handleZoomAdded, normalize selection state by clearing
any clip/audio/other selections and switching the side panel to the zoom
inspector so only the new zoom is highlighted; after setSelectedZoomId(id) (and
setSelectedAnnotationId(null)) also call the relevant setters to clear
clip/audio selection (e.g. setSelectedClipId(null), setSelectedAudioId(null), or
their equivalents) and set the side panel/inspector to the zoom view (e.g.
setSidePanel("zoom") or setInspectorPanel("zoom")) so the inspector shows the
new zoom.

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