Skip to content

fix: bound audio waveform to clip row; cascade-delete regions on clip removal#270

Closed
webadderall wants to merge 2 commits intomainfrom
fix/waveform-bounding-cascade-delete
Closed

fix: bound audio waveform to clip row; cascade-delete regions on clip removal#270
webadderall wants to merge 2 commits intomainfrom
fix/waveform-bounding-cascade-delete

Conversation

@webadderall
Copy link
Copy Markdown
Collaborator

@webadderall webadderall commented Apr 18, 2026

Changes\n\n### Waveform bounding\nAdd overflow-hidden to the timeline Row inner container so the audio waveform canvas (absolute inset-0) is properly clipped to its track row and cannot visually overflow into adjacent rows.\n\n### Cascade delete\nWhen a clip region is deleted, all zoom, annotation, trim, speed, and audio regions whose spans fall fully within the deleted clip's time range are also removed. This prevents orphaned timeline items from accumulating after clip deletion.\n\n## Files changed\n- timeline/Row.tsx — added overflow-hidden to row inner container\n- VideoEditor.tsxhandleClipDelete now cascades deletion to child regions within the clip span\n- timeline/TimelineEditor.tsx — existing timeline rendering (no functional change, restoring clean pre-refactor baseline)\n- timeline/AudioWaveform.tsx — existing waveform canvas (no functional change)

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeline rendering and clipping to prevent overflow artifacts.
    • Resolved stale webcam URL fallback behavior so playback uses the correct source.
  • New Features

    • Deleting a clip now also removes any timeline regions fully contained by that clip (annotations, trims, speed changes, audio regions).
  • Style

    • Updated editor dark-theme colors, separators, and thumbnail background.
    • Tweaked export progress display and waveform/row visual behavior.
  • Chores

    • CI/release metadata and packaging version updates.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

VideoEditor internals updated: webcam URL resolution and export progress logic changed, clip deletion now cascade-removes enclosed timeline regions, thumbnail background hard-coded, UI theme/styling adjusted; minor timeline component styling and import-order edits; CI workflow asset naming and package version bumped.

Changes

Cohort / File(s) Summary
VideoEditor Core
src/components/video-editor/VideoEditor.tsx
Removed fallback to resolvedWebcamVideoUrl when deriving webcamUrl; added immediate setResolvedWebcamVideoUrl(null) on webcam.sourcePath changes. Hardcoded thumbnail canvas fill to #111113. Adjusted export finalizing/progress clamp from 100 to 99. Implemented cascade deletion: deleting a clip also removes timeline regions (zoom, annotation, trim, speed, audio) fully contained within the clip.
Timeline UI
src/components/video-editor/timeline/AudioWaveform.tsx, src/components/video-editor/timeline/Row.tsx, src/components/video-editor/timeline/TimelineEditor.tsx
AudioWaveform canvas inline style changed to display: "block". Row inner container gained overflow-hidden. TimelineEditor import order adjusted (dnd-timeline imports moved after icon imports).
Styling / Theme
src/components/video-editor/VideoEditor.tsx
Replaced several design-token classnames/foreground/neutral styles with explicit slate/white tokens and hard-coded background colors; Toaster set to theme="dark" in additional branches. (Visual/style-only changes.)
CI / Release Workflow
.github/workflows/release.yml
Changed macOS artifact naming to *-mac.yml and merged macOS metadata via glob discovery; broadened Windows/Linux upload globs. Merged macOS metadata output path now derived from x64 filename instead of fixed latest-mac.yml.
Package Metadata
package.json
Bumped package version from 1.1.23 to 1.2.0-beta.1 (no other package fields changed).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • webadderall/Recordly#257 — overlaps webcam URL resolution and resolvedWebcamVideoUrl handling.
  • webadderall/Recordly#259 — directly touches the same webcamUrl derivation and webcam.sourcePath effect changes.
  • webadderall/Recordly#262 — modifies export finalizing/progress percentage logic in VideoEditor.

Suggested labels

Checked

Poem

🐰 I nibble bytes and hop through frames so bright,
I trim the clips and chase the fading light,
Dark canvases now hold the scenes I keep,
Regions fall away—soft, tidy, neat—
Hooray! The editor hums into the night.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the main changes with clear sections for each feature, explains the motivation (preventing orphaned items and visual overflow), and lists affected files, but lacks required template sections like Type of Change, Testing Guide, and Screenshots. Add the missing template sections: select a Type of Change checkbox, provide a Testing Guide with steps to verify both features work correctly, and include screenshots/videos if UI changes are visible.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically summarizes the two main changes: waveform bounding to the clip row and cascade deletion of regions on clip removal.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/waveform-bounding-cascade-delete

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

Caution

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

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

5213-5217: ⚠️ Potential issue | 🟠 Major

Wire clip deletion into TimelineEditor.

TimelineEditor’s Delete/Backspace path calls onClipDelete, but this invocation never passes handleClipDelete, so deleting a selected clip from the timeline still does nothing and bypasses the new cascade behavior.

Proposed fix
 						clipRegions={clipRegions}
 						onClipSplit={handleClipSplit}
 						onClipSpanChange={handleClipSpanChange}
+						onClipDelete={handleClipDelete}
 						selectedClipId={selectedClipId}
 						onSelectClip={handleSelectClip}
🤖 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 5213 - 5217,
TimelineEditor is wired to call onClipDelete on Delete/Backspace but the prop
wasn’t passed; update the TimelineEditor props (the JSX block that currently
sets clipRegions, onClipSplit, onClipSpanChange, selectedClipId, onSelectClip)
to include onClipDelete={handleClipDelete} so the component invokes the existing
handleClipDelete cascade logic when a clip is deleted.
🤖 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 2811-2826: The cascade that removes zoom regions inside the
clip-delete block filters zoom state directly, which skips the existing
handleZoomDelete/extensionHost notifications; change the zoom removal to first
compute the list of zoom regions being removed (those where r.startMs >= startMs
&& r.endMs <= endMs or your existing "fully within" predicate), call the same
removal notifier used by handleZoomDelete (e.g., invoke handleZoomDelete or emit
extensionHost.notify('timeline:region-removed', region) for each removed zoom)
and then update setZoomRegions with the filtered result, leaving the other
cascaded filters (setAnnotationRegions, setTrimRegions, setSpeedRegions,
setAudioRegions) unchanged. Ensure you reference clipRegions, setZoomRegions,
startMs, endMs, handleZoomDelete/extensionHost and perform notification before
committing the new zoom state.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 5213-5217: TimelineEditor is wired to call onClipDelete on
Delete/Backspace but the prop wasn’t passed; update the TimelineEditor props
(the JSX block that currently sets clipRegions, onClipSplit, onClipSpanChange,
selectedClipId, onSelectClip) to include onClipDelete={handleClipDelete} so the
component invokes the existing handleClipDelete cascade logic when a clip is
deleted.
🪄 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: 23f02a36-2eb0-4e34-8fd6-2d11d16f8494

📥 Commits

Reviewing files that changed from the base of the PR and between 8cefcea and 753f332.

📒 Files selected for processing (4)
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/timeline/AudioWaveform.tsx
  • src/components/video-editor/timeline/Row.tsx
  • src/components/video-editor/timeline/TimelineEditor.tsx

Comment on lines +2811 to +2826
const deletedClip = clipRegions.find((c) => c.id === id);
setClipRegions((prev) => prev.filter((clip) => clip.id !== id));
if (deletedClip) {
const { startMs, endMs } = deletedClip;
// Cascade: remove all timeline items fully within the deleted clip's span
setZoomRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setAnnotationRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setTrimRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setSpeedRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setAudioRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
}
if (selectedClipId === id) {
setSelectedClipId(null);
}
},
[selectedClipId],
[clipRegions, selectedClipId],
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

Emit removal events for cascaded zoom deletions.

handleZoomDelete notifies extensionHost, but the new cascade removes zooms by filtering state directly. Extensions listening for timeline:region-removed can miss zooms removed via clip deletion.

Proposed fix
 		const handleClipDelete = useCallback(
 			(id: string) => {
 				const deletedClip = clipRegions.find((c) => c.id === id);
 				setClipRegions((prev) => prev.filter((clip) => clip.id !== id));
 				if (deletedClip) {
 					const { startMs, endMs } = deletedClip;
+					const isInsideDeletedClip = (region: { startMs: number; endMs: number }) =>
+						region.startMs >= startMs && region.endMs <= endMs;
+					const removedZoomIds = zoomRegions
+						.filter(isInsideDeletedClip)
+						.map((region) => region.id);
 					// Cascade: remove all timeline items fully within the deleted clip's span
-					setZoomRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
-					setAnnotationRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
-					setTrimRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
-					setSpeedRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
-					setAudioRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
+					setZoomRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
+					setAnnotationRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
+					setTrimRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
+					setSpeedRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
+					setAudioRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
+					removedZoomIds.forEach((removedId) => {
+						extensionHost.emitEvent({
+							type: "timeline:region-removed",
+							data: { id: removedId },
+						});
+					});
 				}
 				if (selectedClipId === id) {
 					setSelectedClipId(null);
 				}
 			},
-			[clipRegions, selectedClipId],
+			[clipRegions, selectedClipId, zoomRegions],
 		);
📝 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 deletedClip = clipRegions.find((c) => c.id === id);
setClipRegions((prev) => prev.filter((clip) => clip.id !== id));
if (deletedClip) {
const { startMs, endMs } = deletedClip;
// Cascade: remove all timeline items fully within the deleted clip's span
setZoomRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setAnnotationRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setTrimRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setSpeedRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
setAudioRegions((prev) => prev.filter((r) => r.startMs < startMs || r.endMs > endMs));
}
if (selectedClipId === id) {
setSelectedClipId(null);
}
},
[selectedClipId],
[clipRegions, selectedClipId],
const handleClipDelete = useCallback(
(id: string) => {
const deletedClip = clipRegions.find((c) => c.id === id);
setClipRegions((prev) => prev.filter((clip) => clip.id !== id));
if (deletedClip) {
const { startMs, endMs } = deletedClip;
const isInsideDeletedClip = (region: { startMs: number; endMs: number }) =>
region.startMs >= startMs && region.endMs <= endMs;
const removedZoomIds = zoomRegions
.filter(isInsideDeletedClip)
.map((region) => region.id);
// Cascade: remove all timeline items fully within the deleted clip's span
setZoomRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
setAnnotationRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
setTrimRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
setSpeedRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
setAudioRegions((prev) => prev.filter((r) => !isInsideDeletedClip(r)));
removedZoomIds.forEach((removedId) => {
extensionHost.emitEvent({
type: "timeline:region-removed",
data: { id: removedId },
});
});
}
if (selectedClipId === id) {
setSelectedClipId(null);
}
},
[clipRegions, selectedClipId, zoomRegions],
);
🤖 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 2811 - 2826, The
cascade that removes zoom regions inside the clip-delete block filters zoom
state directly, which skips the existing handleZoomDelete/extensionHost
notifications; change the zoom removal to first compute the list of zoom regions
being removed (those where r.startMs >= startMs && r.endMs <= endMs or your
existing "fully within" predicate), call the same removal notifier used by
handleZoomDelete (e.g., invoke handleZoomDelete or emit
extensionHost.notify('timeline:region-removed', region) for each removed zoom)
and then update setZoomRegions with the filtered result, leaving the other
cascaded filters (setAnnotationRegions, setTrimRegions, setSpeedRegions,
setAudioRegions) unchanged. Ensure you reference clipRegions, setZoomRegions,
startMs, endMs, handleZoomDelete/extensionHost and perform notification before
committing the new zoom state.

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.

Caution

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

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

303-313: ⚠️ Potential issue | 🟠 Major

Upload the dynamically named merged macOS metadata.

Line 303 writes {channel_filename}, but Lines 308-313 still upload only latest-mac.yml. A beta-mac.yml or other channel release will fail with if-no-files-found: error.

🐛 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 303 - 313, The upload step
currently hardcodes path: release-assets/merged/latest-mac.yml while the
workflow writes release-assets/merged/{channel_filename}; update the "Upload
merged latest-mac.yml artifact" step so its path uses the same dynamic channel
filename variable (the one used when writing channel_filename ->
release-assets/merged/{channel_filename}) instead of the static latest-mac.yml,
ensuring the artifact upload references the matching dynamic file name and
preserves the if-no-files-found behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 303-313: The upload step currently hardcodes path:
release-assets/merged/latest-mac.yml while the workflow writes
release-assets/merged/{channel_filename}; update the "Upload merged
latest-mac.yml artifact" step so its path uses the same dynamic channel filename
variable (the one used when writing channel_filename ->
release-assets/merged/{channel_filename}) instead of the static latest-mac.yml,
ensuring the artifact upload references the matching dynamic file name and
preserves the if-no-files-found behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d703e885-b313-44e3-be82-9c6ea37815d3

📥 Commits

Reviewing files that changed from the base of the PR and between 753f332 and a50f698.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

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