Skip to content

fix: clip coordinate model with sourceStartMs, remove trim/speed UI#272

Closed
webadderall wants to merge 3 commits intomainfrom
fix/clip-sourcestart-coordinates
Closed

fix: clip coordinate model with sourceStartMs, remove trim/speed UI#272
webadderall wants to merge 3 commits intomainfrom
fix/clip-sourcestart-coordinates

Conversation

@webadderall
Copy link
Copy Markdown
Collaborator

@webadderall webadderall commented Apr 19, 2026

Summary

Fixes critical coordinate-space bugs in the clip editing system and removes trim/speed as standalone user-editable concepts (clips are now the only editing primitive).

Coordinate Model Fix: sourceStartMs

The bug: When splitting a clip with speed ≠ 1, the right half pointed to the wrong source footage. clip.startMs was serving double duty as both timeline position and source video position — these diverge as soon as speed changes or clips are rearranged.

The fix: Added sourceStartMs to ClipRegion to explicitly track where in the source video each clip starts (defaults to startMs for backward compat). All coordinate conversion now uses getClipSourceStartMs():

  • mapTimelineTimeToSourceTime / mapSourceTimeToTimelineTime — use sourceStartMs for source range checks and mapping
  • effectiveSpeedRegionsstartMs field uses source coordinates
  • handleClipSplit — computes rightSourceStart so the right half maps to correct source footage
  • Project persistence preserves sourceStartMs on save/load

Audio Region Sync Fix

currentTime (from video.currentTime) is in source coordinates, but audioRegion.startMs/endMs are timeline coordinates. The sync effect was comparing them directly. Now converts source time → timeline time via mapSourceTimeToTimelineTime before the comparison.

Cascade Fix

Clip move (handleClipSpanChange) and clip speed change (handleClipSpeedChange) now cascade position/scale changes to annotations and audio regions, not just zoom regions.

Trim/Speed Removal

Removed trim and speed as standalone user-editable region types:

  • Removed from SettingsPanel, TimelineEditor, and keyboard shortcuts
  • trimRegions and speedRegions are now derived from clips via useMemo (for export pipeline consumption)
  • Export pipeline unchanged — still consumes TrimRegion/SpeedRegion as before

Verification

  • Zero TypeScript errors (npx tsc --noEmit)
  • Zero Biome errors

Summary by CodeRabbit

Release Notes

  • Chores

    • Version bumped to 1.2.0-beta.1 for beta release.
  • Bug Fixes

    • Removed trim deletion UI from the Settings Panel.
    • Removed speed region editing controls from the Settings Panel.
    • Removed trim and speed editing actions from Timeline Editor.
  • Style

    • Updated canvas element display styling in audio waveform.
    • Added overflow clipping to timeline row containers.

…s user concepts

- Add sourceStartMs to ClipRegion for explicit source video position tracking
- Fix split+speed bug: splitting a speed≠1 clip now correctly maps the right
  half to the correct source footage position
- Fix audio region sync: convert source time → timeline time before comparing
  against audio region positions (was using mismatched coordinate spaces)
- Fix cascade: clip move and speed change now cascade to annotations + audio
  regions (was only cascading to zoom regions)
- Update effectiveSpeedRegions to use source coordinates via getClipSourceStartMs
- Update mapTimelineTimeToSourceTime/mapSourceTimeToTimelineTime to use sourceStartMs
- Preserve sourceStartMs through project save/load
- Remove trim and speed as standalone user-editable concepts (clips are the
  only editing primitive; trims and speed regions are derived for export)
- Remove trim/speed UI from SettingsPanel, TimelineEditor, and shortcuts
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

This PR refactors the video editor's region state management architecture. Trim and speed regions transition from standalone state to derived values computed from clip regions. A new useEditorRegions hook centralizes all region state and mutation handlers. The settings panel and timeline editor UI are simplified by removing trim/speed deletion controls. Workflow updates improve macOS artifact handling, and the package version bumps to 1.2.0-beta.1.

Changes

Cohort / File(s) Summary
Workflow & Version
.github/workflows/release.yml, package.json
Refactored macOS artifact uploads to use dynamic *-mac.yml patterns instead of hardcoded latest-mac.yml; bumped package version to 1.2.0-beta.1.
Region State Centralization
src/components/video-editor/hooks/useEditorRegions.ts
Added new 684-line hook that manages zoom, clip, annotation, and audio region state; derives trim regions from clip regions and speed regions from clip speeds; provides handlers for region CRUD, time mapping, and cascading updates on clip changes.
VideoEditor Refactoring
src/components/video-editor/VideoEditor.tsx
Removed standalone trimRegions/speedRegions state; trim/speed now derived via useMemo from clipRegions; updated time mapping to consistently use source time; added sourceStartMs propagation in clip splitting; generalized child-region transforms on clip moves/deletes; simplified webcam URL resolution.
Region Props Removal
src/components/video-editor/SettingsPanel.tsx, src/components/video-editor/timeline/TimelineEditor.tsx
Removed trim/speed deletion props and UI from SettingsPanel (89 lines deleted); removed all trim/speed region handlers and keyboard shortcuts from TimelineEditor (239 lines deleted).
Type & Persistence Updates
src/components/video-editor/types.ts, src/components/video-editor/projectPersistence.ts
Extended ClipRegion with optional sourceStartMs; added getClipSourceStartMs helper; updated clipsToTrims to sort by source start; normalized sourceStartMs in project persistence.
Shortcut Configuration
src/lib/shortcuts.ts
Removed addTrim and addSpeed from SHORTCUT_ACTIONS, DEFAULT_SHORTCUTS, and SHORTCUT_LABELS.
Minor Layout/Style Updates
src/components/video-editor/timeline/AudioWaveform.tsx, src/components/video-editor/timeline/Row.tsx
Changed AudioWaveform canvas from zIndex: 0 to display: "block"; added overflow-hidden class to Row container.

Sequence Diagram

sequenceDiagram
    participant Clip as Clip State
    participant Hook as useEditorRegions
    participant VE as VideoEditor
    participant UI as Timeline/Settings UI

    Clip->>Hook: clipRegions update
    Hook->>Hook: derive trimRegions via clipsToTrims()
    Hook->>Hook: derive speedRegions from clip speeds
    Hook->>VE: provide derived regions + handlers
    VE->>VE: useMemo computes trim/speed from clips
    VE->>UI: pass derived regions to Timeline/Settings
    UI->>UI: render without trim/speed deletion controls
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

  • #143: Modifies the same bottom zoom/trim/speed UI in SettingsPanel.tsx, with overlapping changes to region visibility controls.
  • #249: Directly overlaps on video-editor surface changes (SettingsPanel, VideoEditor, TimelineEditor, types, projectPersistence) with region-handling logic modifications.
  • #122: Modifies the same video-editor components (SettingsPanel, VideoEditor, TimelineEditor, types.ts, Row, AudioWaveform) indicating shared code areas and editor functionality changes.

Suggested labels

architecture, refactoring, video-editor

Poem

🐰 Trim regions once stood proudly alone,
Now nestled in clips, they've found their true home!
Derived from the source, no standalone state—
Simpler, cleaner code—oh, how delightfully great!
Speed regions follow, a harmonious dance,
Where clip speed and source time lead the advance.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: introducing sourceStartMs for coordinate model fixes and removing trim/speed UI. It is concise, specific, and clearly conveys the primary purpose of the changeset.
Description check ✅ Passed The PR description is comprehensive and well-organized with clear sections covering Summary, coordinate model fix, audio sync fix, cascade fix, and trim/speed removal. It directly addresses the template's Description and Motivation requirements with substantial technical detail.

✏️ 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 fix/clip-sourcestart-coordinates

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 and usage tips.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
.github/workflows/release.yml (1)

263-313: ⚠️ Potential issue | 🟠 Major

Upload the dynamically named merged macOS metadata.

The merge now writes release-assets/merged/${channel_filename}, but the artifact upload still requires release-assets/merged/latest-mac.yml. For beta metadata, this produces beta-mac.yml, so the upload step can fail or omit the updater metadata.

🔧 Proposed fix
-      - name: Upload merged latest-mac.yml artifact
+      - name: Upload merged macOS metadata artifact
         uses: actions/upload-artifact@v4
         with:
           name: macos-merged-metadata
-          path: release-assets/merged/latest-mac.yml
+          path: release-assets/merged/*-mac.yml
           if-no-files-found: error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 263 - 313, The upload step
currently hardcodes release-assets/merged/latest-mac.yml but the Python block
writes release-assets/merged/{channel_filename} (variable channel_filename),
causing missing-artifact failures when the file is named e.g. beta-mac.yml;
modify the merge step to emit channel_filename as a step output (write
"channel_filename=..." to $GITHUB_OUTPUT or otherwise persist it) and then
update the "Upload merged latest-mac.yml artifact" step to use that step output
for the path (reference the step that runs the Python merge and use its output
instead of the static latest-mac.yml); keep the step name "Upload merged
latest-mac.yml artifact" but change its with.path to the dynamic
release-assets/merged/${{ steps.<merge-step>.outputs.channel_filename }}.
src/components/video-editor/VideoEditor.tsx (2)

2630-2664: ⚠️ Potential issue | 🟠 Major

Left-edge clip resize doesn't update sourceStartMs, corrupting source↔timeline mapping.

handleClipSpanChange only cascades on move (startDelta ≈ endDelta). On a resize, it just writes the new startMs/endMs and leaves sourceStartMs untouched. Because getClipSourceEndMs = sourceStart + (endMs − startMs) × speed, a left-edge trim (startMs ↑, endMs fixed) shrinks the source range from its end instead of its start — the user visually crops the beginning of the clip but the exported source frames are cut from the tail. This directly undermines the coordinate-model fix that is the stated goal of this PR, and gets baked into saved projects.

The same issue exists in src/components/video-editor/hooks/useEditorRegions.ts (lines 324‑353).

🛠️ Suggested fix: shift `sourceStartMs` proportionally on left-edge resize
 		const oldClip = clipRegions.find((c) => c.id === id);
 		const newStart = Math.round(span.start);
 		const newEnd = Math.round(span.end);

 		if (oldClip) {
 			const startDelta = newStart - oldClip.startMs;
 			const endDelta = newEnd - oldClip.endMs;
 			const isMove = Math.abs(startDelta - endDelta) < 1 && Math.abs(startDelta) > 0;

 			if (isMove) {
 				const delta = startDelta;
 				const moveContained = <T extends { startMs: number; endMs: number }>(
 					regions: T[],
 				): T[] =>
 					regions.map((r) =>
 						r.startMs >= oldClip.startMs && r.endMs <= oldClip.endMs
 							? { ...r, startMs: r.startMs + delta, endMs: r.endMs + delta }
 							: r,
 					);
 				setZoomRegions((prev) => moveContained(prev));
 				setAnnotationRegions((prev) => moveContained(prev));
 				setAudioRegions((prev) => moveContained(prev));
 			}
 		}

 		setClipRegions((prev) =>
 			prev.map((clip) => {
-				return clip.id === id ? { ...clip, startMs: newStart, endMs: newEnd } : clip;
+				if (clip.id !== id) return clip;
+				const oldStart = clip.startMs;
+				const leftDelta = newStart - oldStart;
+				const isLeftResize =
+					Math.abs(leftDelta) > 0 && Math.abs(leftDelta - (newEnd - clip.endMs)) >= 1;
+				const speed =
+					Number.isFinite(clip.speed) && clip.speed > 0 ? clip.speed : 1;
+				const sourceStart = clip.sourceStartMs ?? clip.startMs;
+				const nextSourceStart = isLeftResize
+					? Math.max(0, Math.round(sourceStart + leftDelta * speed))
+					: sourceStart;
+				return {
+					...clip,
+					startMs: newStart,
+					endMs: newEnd,
+					sourceStartMs: nextSourceStart,
+				};
 			}),
 		);

2666-2699: ⚠️ Potential issue | 🟠 Major

Subsequent clips don't auto-shift when changing clip speed — creating timeline gaps or overlaps.

handleClipSpeedChange updates only the selected clip's endMs. Any following clip with startMs === oldEndMs (which handleClipSplit creates) stays fixed, leaving a gap if speed increases or overlapping the right half if speed decreases. The timeline UI will render this broken layout; the export pipeline's source-time sorting may partially mask the issue, but the UX is broken.

handleClipSpanChange already implements the shift pattern correctly — use the same approach to shift subsequent clips by (newEndMs − oldEndMs), preserving their source ranges via sourceStartMs. Alternatively, disallow speed changes on non-trailing clips and document this constraint.

🤖 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 2666 - 2699,
handleClipSpeedChange currently updates only the selected clip's speed/endMs
which leaves subsequent clips fixed and creates gaps/overlaps; change its logic
(the function handleClipSpeedChange and the setClipRegions update) to compute
oldEndMs and newEndMs for the selected clip, then apply a delta = newEndMs -
oldEndMs to shift all following clipRegions (those with startMs >= oldEndMs) by
that delta while preserving their source timing via sourceStartMs/sourceEndMs
adjustments as handleClipSpanChange does; update setClipRegions to map over prev
regions and for the selected id replace speed/endMs, and for subsequent regions
shift startMs/endMs (and adjust sourceStartMs if needed) by delta so timeline
remains contiguous (alternative: explicitly reject speed changes for
non-trailing clips if shifting is undesired).
🧹 Nitpick comments (2)
src/components/video-editor/hooks/useEditorRegions.ts (1)

238-258: handleZoomDepthChange / handleZoomModeChange should bail out when no zoom is selected.

VideoEditor.tsx guards both with if (!selectedZoomId) return;. Here the selectedZoomId === null branch quietly maps over every region (all non-matches) and still produces a new zoomRegions array, defeating React's referential equality and invalidating downstream memos (e.g. effectiveZoomRegions, export config) on every call.

 const handleZoomDepthChange = useCallback(
 	(depth: ZoomDepth) => {
+		if (!selectedZoomId) return;
 		setZoomRegions((prev) =>
 			prev.map((r) =>
 				r.id === selectedZoomId
 					? { ...r, depth, focus: clampFocusToDepth(r.focus, depth) }
 					: r,
 			),
 		);
 	},
 	[selectedZoomId],
 );

 const handleZoomModeChange = useCallback(
 	(mode: ZoomMode) => {
+		if (!selectedZoomId) return;
 		setZoomRegions((prev) =>
 			prev.map((r) => (r.id === selectedZoomId ? { ...r, mode } : r)),
 		);
 	},
 	[selectedZoomId],
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/hooks/useEditorRegions.ts` around lines 238 -
258, The handlers handleZoomDepthChange and handleZoomModeChange currently
always call setZoomRegions even when selectedZoomId is null, causing a new array
to be produced and breaking referential equality; update both callbacks to bail
out early when selectedZoomId is null (or falsy) and do nothing, or within
setZoomRegions return the previous state unchanged when no region matches, so
that no new array reference is created; target the functions
handleZoomDepthChange, handleZoomModeChange and the setZoomRegions usage to
implement the early-return/no-op behavior.
src/components/video-editor/VideoEditor.tsx (1)

570-582: speedRegions is a zero-value alias over effectiveSpeedRegions.

Only one alias is referenced downstream (the two audio-sync effects) and both could read effectiveSpeedRegions directly. Dropping const speedRegions = effectiveSpeedRegions; (and the matching references at lines 3165, 3207) removes a redundant indirection now that standalone speed regions are gone.

🤖 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 570 - 582, The
variable speedRegions is an unnecessary alias of effectiveSpeedRegions; remove
the declaration const speedRegions = effectiveSpeedRegions; and update any
downstream uses (currently the two audio-sync effects that reference
speedRegions) to read effectiveSpeedRegions directly so only
effectiveSpeedRegions (computed from clipRegions via
getClipSourceStartMs/getClipSourceEndMs) remains.
🤖 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/hooks/useEditorRegions.ts`:
- Around line 165-214: Replace the hardcoded depth values in handleZoomAdded and
handleZoomSuggested with the named constants DEFAULT_ZOOM_DEPTH and
DEFAULT_AUTO_ZOOM_DEPTH (import them from ./types), and update the
clampFocusToDepth call in handleZoomSuggested to use DEFAULT_AUTO_ZOOM_DEPTH
instead of 2; specifically, set newRegion.depth in handleZoomAdded to
DEFAULT_ZOOM_DEPTH, set newRegion.depth in handleZoomSuggested to
DEFAULT_AUTO_ZOOM_DEPTH, and call clampFocusToDepth(focus,
DEFAULT_AUTO_ZOOM_DEPTH) so the hook stays in sync with VideoEditor.tsx.
- Around line 45-50: The new hook useEditorRegions is implemented but not used
in VideoEditor.tsx, leaving duplicate inline region state/handlers; replace
VideoEditor.tsx's inline region state (zoom/clip/annotation/audio and their
handlers, including setActiveEffectSection usage) with the useEditorRegions hook
by importing and calling useEditorRegions, and ensure depth constants
align—either update useEditorRegions to consume DEFAULT_ZOOM_DEPTH and
DEFAULT_AUTO_ZOOM_DEPTH or pass those constants into the hook so both
implementations use the same depth values to avoid drift.

In `@src/components/video-editor/timeline/TimelineEditor.tsx`:
- Around line 997-1002: The current hasAnyTimelineBlocks boolean includes
trimRegions which now represent derived, non-rendered/non-deletable trims;
remove trimRegions from the OR-list used to compute hasAnyTimelineBlocks (i.e.,
only consider zoomRegions, clipRegions, annotationRegions, and audioRegions) or
replace trimRegions with a filtered visibleTrimRegions that excludes derived
trims so select-all/delete reflects only visible, deletable blocks; update the
expression computing hasAnyTimelineBlocks in TimelineEditor (symbol:
hasAnyTimelineBlocks) accordingly.

In `@src/components/video-editor/types.ts`:
- Around line 173-179: The trim cursor can move backward when clips overlap; in
the loop over sorted clips (variables: clip, cursor, trims, trimId) replace the
assignment cursor = getClipSourceEndMs(clip) with advancing the cursor
monotonically (e.g. cursor = Math.max(cursor, getClipSourceEndMs(clip))) so
overlapping source ranges can't create a backward cursor and incorrect
subsequent trims; keep the existing gap check that uses
getClipSourceStartMs(clip).

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2682-2696: The scaleInClip helper currently checks only r.startMs
against clip bounds causing regions that extend beyond clip.endMs to be
mis-scaled; update scaleInClip (used by setZoomRegions, setAnnotationRegions,
setAudioRegions) to either require full containment (r.startMs >= clip.startMs
&& r.endMs <= clip.endMs) before scaling or, if partial overlaps must be
preserved, compute the scaled start and end then clamp the resulting endMs to
the scaled clip end (and ensure endMs >= startMs after rounding) so no child
region ends up outside the new clip bounds or inverted.

---

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 263-313: The upload step currently hardcodes
release-assets/merged/latest-mac.yml but the Python block writes
release-assets/merged/{channel_filename} (variable channel_filename), causing
missing-artifact failures when the file is named e.g. beta-mac.yml; modify the
merge step to emit channel_filename as a step output (write
"channel_filename=..." to $GITHUB_OUTPUT or otherwise persist it) and then
update the "Upload merged latest-mac.yml artifact" step to use that step output
for the path (reference the step that runs the Python merge and use its output
instead of the static latest-mac.yml); keep the step name "Upload merged
latest-mac.yml artifact" but change its with.path to the dynamic
release-assets/merged/${{ steps.<merge-step>.outputs.channel_filename }}.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2666-2699: handleClipSpeedChange currently updates only the
selected clip's speed/endMs which leaves subsequent clips fixed and creates
gaps/overlaps; change its logic (the function handleClipSpeedChange and the
setClipRegions update) to compute oldEndMs and newEndMs for the selected clip,
then apply a delta = newEndMs - oldEndMs to shift all following clipRegions
(those with startMs >= oldEndMs) by that delta while preserving their source
timing via sourceStartMs/sourceEndMs adjustments as handleClipSpanChange does;
update setClipRegions to map over prev regions and for the selected id replace
speed/endMs, and for subsequent regions shift startMs/endMs (and adjust
sourceStartMs if needed) by delta so timeline remains contiguous (alternative:
explicitly reject speed changes for non-trailing clips if shifting is
undesired).

---

Nitpick comments:
In `@src/components/video-editor/hooks/useEditorRegions.ts`:
- Around line 238-258: The handlers handleZoomDepthChange and
handleZoomModeChange currently always call setZoomRegions even when
selectedZoomId is null, causing a new array to be produced and breaking
referential equality; update both callbacks to bail out early when
selectedZoomId is null (or falsy) and do nothing, or within setZoomRegions
return the previous state unchanged when no region matches, so that no new array
reference is created; target the functions handleZoomDepthChange,
handleZoomModeChange and the setZoomRegions usage to implement the
early-return/no-op behavior.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 570-582: The variable speedRegions is an unnecessary alias of
effectiveSpeedRegions; remove the declaration const speedRegions =
effectiveSpeedRegions; and update any downstream uses (currently the two
audio-sync effects that reference speedRegions) to read effectiveSpeedRegions
directly so only effectiveSpeedRegions (computed from clipRegions via
getClipSourceStartMs/getClipSourceEndMs) remains.
🪄 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

Run ID: 1f653b7c-865e-4b6d-8c52-55a74f401b0c

📥 Commits

Reviewing files that changed from the base of the PR and between e4a780a and 99c94ed.

📒 Files selected for processing (11)
  • .github/workflows/release.yml
  • package.json
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/hooks/useEditorRegions.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/timeline/AudioWaveform.tsx
  • src/components/video-editor/timeline/Row.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.ts
  • src/lib/shortcuts.ts
💤 Files with no reviewable changes (1)
  • src/lib/shortcuts.ts

Comment on lines +45 to +50
export function useEditorRegions({
duration,
currentTime,
videoPath,
setActiveEffectSection,
}: UseEditorRegionsParams) {
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C2 '\buseEditorRegions\b'

Repository: webadderall/Recordly

Length of output: 780


🏁 Script executed:

rg -n "useEditorRegions" src/components/video-editor/VideoEditor.tsx

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

rg -n "import.*useEditorRegions" --type=ts

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

cat -n src/components/video-editor/VideoEditor.tsx | head -100

Repository: webadderall/Recordly

Length of output: 3743


🏁 Script executed:

rg "import.*useEditorRegions|from.*useEditorRegions" --type=ts

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

rg "useState.*region|useState.*clip|useState.*annotation|useState.*audio" src/components/video-editor/VideoEditor.tsx -n

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

wc -l src/components/video-editor/VideoEditor.tsx

Repository: webadderall/Recordly

Length of output: 112


🏁 Script executed:

rg "useEditorRegions" --type=ts

Repository: webadderall/Recordly

Length of output: 272


🏁 Script executed:

cat -n src/components/video-editor/hooks/useEditorRegions.ts

Repository: webadderall/Recordly

Length of output: 26354


🏁 Script executed:

rg "useState" src/components/video-editor/VideoEditor.tsx | head -20

Repository: webadderall/Recordly

Length of output: 1479


🏁 Script executed:

rg "zoomRegions|clipRegions|annotationRegions|audioRegions" src/components/video-editor/VideoEditor.tsx -n

Repository: webadderall/Recordly

Length of output: 4916


🏁 Script executed:

rg "const \[.*Regions" src/components/video-editor/VideoEditor.tsx

Repository: webadderall/Recordly

Length of output: 352


🏁 Script executed:

fd "VideoEditor" src/components/video-editor --type f

Repository: webadderall/Recordly

Length of output: 107


🏁 Script executed:

rg "handleZoom|handleClip|handleAnnotation|handleAudio" src/components/video-editor/VideoEditor.tsx | head -30

Repository: webadderall/Recordly

Length of output: 1863


🏁 Script executed:

rg "depth.*3|depth.*2|depth.*1" src/components/video-editor/VideoEditor.tsx -C1 | head -40

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

rg "depth: [0-9]|depth=[0-9]" src/components/video-editor/hooks/useEditorRegions.ts -n

Repository: webadderall/Recordly

Length of output: 99


🏁 Script executed:

rg "depth: [0-9]|depth=[0-9]" src/components/video-editor/VideoEditor.tsx -n

Repository: webadderall/Recordly

Length of output: 46


🏁 Script executed:

sed -n '165,214p' src/components/video-editor/hooks/useEditorRegions.ts

Repository: webadderall/Recordly

Length of output: 1560


🏁 Script executed:

rg -n "const handleZoomAdded|const handleZoomSuggested" src/components/video-editor/VideoEditor.tsx -A 25

Repository: webadderall/Recordly

Length of output: 1993


Hook is defined but not imported by VideoEditor.tsx.

useEditorRegions ships as dead code. VideoEditor.tsx declares its own region state (zoom, clip, annotation, audio) and all associated handlers inline. Both implementations currently differ in depth constants (hardcoded 3/2 in the hook vs. DEFAULT_ZOOM_DEPTH/DEFAULT_AUTO_ZOOM_DEPTH in VideoEditor) and documentation, creating drift risk.

Either integrate the hook into VideoEditor.tsx in this PR or document in the commit/PR description that the migration is deferred, so reviewers know both copies must remain synchronized until then.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/hooks/useEditorRegions.ts` around lines 45 - 50,
The new hook useEditorRegions is implemented but not used in VideoEditor.tsx,
leaving duplicate inline region state/handlers; replace VideoEditor.tsx's inline
region state (zoom/clip/annotation/audio and their handlers, including
setActiveEffectSection usage) with the useEditorRegions hook by importing and
calling useEditorRegions, and ensure depth constants align—either update
useEditorRegions to consume DEFAULT_ZOOM_DEPTH and DEFAULT_AUTO_ZOOM_DEPTH or
pass those constants into the hook so both implementations use the same depth
values to avoid drift.

Comment on lines +165 to +214
const handleZoomAdded = useCallback(
(span: Span) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
endMs: Math.round(span.end),
depth: 3,
focus: { cx: 0.5, cy: 0.5 },
mode: "manual",
};
if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}
setZoomRegions((prev) => [...prev, newRegion]);
setSelectedZoomId(id);

setSelectedAnnotationId(null);
extensionHost.emitEvent({
type: "timeline:region-added",
data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs },
});
},
[videoPath],
);

const handleZoomSuggested = useCallback(
(span: Span, focus: ZoomFocus) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
endMs: Math.round(span.end),
depth: 2,
focus: clampFocusToDepth(focus, 2),
mode: "auto",
};
if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
autoSuggestedVideoPathRef.current = videoPath;
pendingFreshRecordingAutoZoomPathRef.current = null;
}
setZoomRegions((prev) => [...prev, newRegion]);
extensionHost.emitEvent({
type: "timeline:region-added",
data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs },
});
},
[videoPath],
);
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

Hardcoded zoom-depth magic numbers diverge from VideoEditor.tsx defaults.

handleZoomAdded uses depth: 3 and handleZoomSuggested uses depth: 2 / clampFocusToDepth(focus, 2). VideoEditor.tsx imports DEFAULT_ZOOM_DEPTH and DEFAULT_AUTO_ZOOM_DEPTH from ./types for the same paths. Once this hook is wired up, the editor will silently change its default zoom strengths unless the constants coincidentally line up. Reuse the named constants to keep the two paths in lock-step.

🔧 Suggested change
-import {
+import {
 	type AnnotationRegion,
 	type AudioRegion,
 	type ClipRegion,
 	clampFocusToDepth,
 	clipsToTrims,
 	DEFAULT_ANNOTATION_POSITION,
 	DEFAULT_ANNOTATION_SIZE,
 	DEFAULT_ANNOTATION_STYLE,
+	DEFAULT_AUTO_ZOOM_DEPTH,
 	DEFAULT_FIGURE_DATA,
+	DEFAULT_ZOOM_DEPTH,
 	type EditorEffectSection,
 	...
 } from "../types";
@@
-				depth: 3,
+				depth: DEFAULT_ZOOM_DEPTH,
 				focus: { cx: 0.5, cy: 0.5 },
 				mode: "manual",
@@
-				depth: 2,
-				focus: clampFocusToDepth(focus, 2),
+				depth: DEFAULT_AUTO_ZOOM_DEPTH,
+				focus: clampFocusToDepth(focus, DEFAULT_AUTO_ZOOM_DEPTH),
 				mode: "auto",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/hooks/useEditorRegions.ts` around lines 165 -
214, Replace the hardcoded depth values in handleZoomAdded and
handleZoomSuggested with the named constants DEFAULT_ZOOM_DEPTH and
DEFAULT_AUTO_ZOOM_DEPTH (import them from ./types), and update the
clampFocusToDepth call in handleZoomSuggested to use DEFAULT_AUTO_ZOOM_DEPTH
instead of 2; specifically, set newRegion.depth in handleZoomAdded to
DEFAULT_ZOOM_DEPTH, set newRegion.depth in handleZoomSuggested to
DEFAULT_AUTO_ZOOM_DEPTH, and call clampFocusToDepth(focus,
DEFAULT_AUTO_ZOOM_DEPTH) so the hook stays in sync with VideoEditor.tsx.

Comment on lines 997 to 1002
const hasAnyTimelineBlocks =
zoomRegions.length > 0 ||
trimRegions.length > 0 ||
clipRegions.length > 0 ||
annotationRegions.length > 0 ||
speedRegions.length > 0 ||
audioRegions.length > 0;
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

Exclude derived trims from “select all blocks”.

trimRegions are no longer rendered or user-deletable here, so counting them can enable select-all/delete behavior even when no visible block corresponds to those trims.

🔧 Proposed fix
 		const hasAnyTimelineBlocks =
 			zoomRegions.length > 0 ||
-			trimRegions.length > 0 ||
 			clipRegions.length > 0 ||
 			annotationRegions.length > 0 ||
 			audioRegions.length > 0;
📝 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 hasAnyTimelineBlocks =
zoomRegions.length > 0 ||
trimRegions.length > 0 ||
clipRegions.length > 0 ||
annotationRegions.length > 0 ||
speedRegions.length > 0 ||
audioRegions.length > 0;
const hasAnyTimelineBlocks =
zoomRegions.length > 0 ||
clipRegions.length > 0 ||
annotationRegions.length > 0 ||
audioRegions.length > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` around lines 997 -
1002, The current hasAnyTimelineBlocks boolean includes trimRegions which now
represent derived, non-rendered/non-deletable trims; remove trimRegions from the
OR-list used to compute hasAnyTimelineBlocks (i.e., only consider zoomRegions,
clipRegions, annotationRegions, and audioRegions) or replace trimRegions with a
filtered visibleTrimRegions that excludes derived trims so select-all/delete
reflects only visible, deletable blocks; update the expression computing
hasAnyTimelineBlocks in TimelineEditor (symbol: hasAnyTimelineBlocks)
accordingly.

Comment on lines 173 to 179
for (const clip of sorted) {
if (clip.startMs > cursor) {
trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: clip.startMs });
const sourceStart = getClipSourceStartMs(clip);
if (sourceStart > cursor) {
trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: sourceStart });
}
cursor = getClipSourceEndMs(clip);
}
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

Keep the trim cursor monotonic for overlapping source ranges.

If two clips overlap in source coordinates, Line 178 can move cursor backward and generate an incorrect later trim that removes already-kept footage. Advance with Math.max instead.

🐛 Proposed fix
-		cursor = getClipSourceEndMs(clip);
+		cursor = Math.max(cursor, getClipSourceEndMs(clip));
📝 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
for (const clip of sorted) {
if (clip.startMs > cursor) {
trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: clip.startMs });
const sourceStart = getClipSourceStartMs(clip);
if (sourceStart > cursor) {
trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: sourceStart });
}
cursor = getClipSourceEndMs(clip);
}
for (const clip of sorted) {
const sourceStart = getClipSourceStartMs(clip);
if (sourceStart > cursor) {
trims.push({ id: `trim-gap-${trimId++}`, startMs: cursor, endMs: sourceStart });
}
cursor = Math.max(cursor, getClipSourceEndMs(clip));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/types.ts` around lines 173 - 179, The trim cursor
can move backward when clips overlap; in the loop over sorted clips (variables:
clip, cursor, trims, trimId) replace the assignment cursor =
getClipSourceEndMs(clip) with advancing the cursor monotonically (e.g. cursor =
Math.max(cursor, getClipSourceEndMs(clip))) so overlapping source ranges can't
create a backward cursor and incorrect subsequent trims; keep the existing gap
check that uses getClipSourceStartMs(clip).

Comment on lines +2682 to +2696
// Scale all child regions within this clip proportionally
const scaleInClip = <T extends { startMs: number; endMs: number }>(regions: T[]): T[] =>
regions.map((r) => {
if (r.startMs < clip.startMs || r.startMs >= clip.endMs) return r;
return {
...zoom,
...r,
startMs: Math.round(
clip.startMs + (zoom.startMs - clip.startMs) * scaleFactor,
clip.startMs + (r.startMs - clip.startMs) * scaleFactor,
),
endMs: Math.round(clip.startMs + (zoom.endMs - clip.startMs) * scaleFactor),
endMs: Math.round(clip.startMs + (r.endMs - clip.startMs) * scaleFactor),
};
}),
);
});
setZoomRegions((prev) => scaleInClip(prev));
setAnnotationRegions((prev) => scaleInClip(prev));
setAudioRegions((prev) => scaleInClip(prev));
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

scaleInClip containment check only looks at r.startMs.

A child region that starts inside the clip but ends past clip.endMs will have both endpoints scaled using clip.startMs as the pivot, and its endMs can land past the new clip.endMs (or even before its new startMs when speeding up). Worth either filtering by full containment (r.endMs <= clip.endMs) or clamping the scaled endMs to the new clip bound.

🤖 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 2682 - 2696, The
scaleInClip helper currently checks only r.startMs against clip bounds causing
regions that extend beyond clip.endMs to be mis-scaled; update scaleInClip (used
by setZoomRegions, setAnnotationRegions, setAudioRegions) to either require full
containment (r.startMs >= clip.startMs && r.endMs <= clip.endMs) before scaling
or, if partial overlaps must be preserved, compute the scaled start and end then
clamp the resulting endMs to the scaled clip end (and ensure endMs >= startMs
after rounding) so no child region ends up outside the new clip bounds or
inverted.

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