Skip to content

feat: Introduce Layout segments in the editor#905

Merged
ameer2468 merged 25 commits intomainfrom
layouts
Aug 20, 2025
Merged

feat: Introduce Layout segments in the editor#905
ameer2468 merged 25 commits intomainfrom
layouts

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Aug 18, 2025

This PR implements

  • A new Layout segments system in the editor (to toggle the camera, or make it fullscreen for sections of the video)
  • WIP CLAUDE.md file

https://Cap.link/cgt4sy2fptqtp2z

Summary by CodeRabbit

  • New Features

    • Layout track & editor: create/move/resize/select layout segments; per-segment camera-layout editing; layout-driven rendering with smooth camera/screen transitions, camera‑only rendering path, and compositing opacity control.
  • Improvements

    • Zoom track: width-responsive rendering, improved empty-state copy, multi-selection and bulk delete, more stable preview loading/seeking.
    • Clip behavior: single-segment clip clicks now clear selection.
    • Waveform rendering: safer amplitude handling to prevent NaNs.
    • Sidebar: enhanced layout-segment config and preview UX.
  • Bug Fixes

    • More reliable deletions and hover/selection resets; Escape clears selection.
  • Documentation

    • Added comprehensive repository guide (CLAUDE.md).

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 18, 2025

Walkthrough

Adds layout-segment support end-to-end: types (TS/Rust), editor UI (LayoutTrack, ConfigSidebar), editor actions/state, recording init, rendering interpolation and camera-only path, shader opacity, zoom-preview robustness, ambient icons, and repository guidance (CLAUDE.md). No public API removals.

Changes

Cohort / File(s) Summary
Docs
CLAUDE.md
Adds repository guidance covering structure, workflows, conventions, and operational notes; no runtime changes.
Timeline UI & Integration
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx, .../Timeline/index.tsx, .../Timeline/ClipTrack.tsx, .../Timeline/ZoomTrack.tsx
New LayoutTrack (create/drag/resize/select layout segments); conditional Timeline integration when camera metadata present; ClipTrack clears selection for single segment; ZoomTrack hover/reset, width-aware rendering, multi-select toggle, improved empty state.
Editor Context / Actions
apps/desktop/src/routes/editor/context.ts
Fix splice usage; replace single zoom-delete with deleteZoomSegments (multi-delete); add deleteLayoutSegment; expand timeline.selection typing to include layout and zoom indices; reset selection after deletions.
Types & Recording Init (TS/Rust bridge)
apps/desktop/src/utils/tauri.ts, apps/desktop/src-tauri/src/recording.rs
Add LayoutSegment TS type and optional layoutSegments on TimelineConfiguration; initialize layout_segments as empty Vec in recording→project conversion.
Project Configuration (Rust)
crates/project/src/configuration.rs
Add LayoutMode enum and LayoutSegment struct; extend TimelineConfiguration with layout_segments: Vec<LayoutSegment> and serde default.
Rendering core: layout & camera-only path
crates/rendering/src/layout.rs, crates/rendering/src/lib.rs, crates/rendering/src/composite_frame.rs
Add layout interpolation module (LAYOUT_TRANSITION_DURATION, cursor, InterpolatedLayout); wire layout into ProjectUniforms; add camera_only layer; make RendererLayers::render accept &ProjectUniforms; add opacity field to CompositeVideoFrameUniforms with explicit Default.
Shaders
crates/rendering/src/shaders/composite-video-frame.wgsl
Add opacity: f32 uniform and apply to base color alpha.
Config Sidebar & Zoom Preview
apps/desktop/src/routes/editor/ConfigSidebar.tsx
Add LayoutSegmentConfig UI (mode tabs, done/delete); robust zoom-preview canvas load/seek/render with retry and gating; selection gating for layout editing.
Ambient icons / types
packages/ui-solid/src/auto-imports.d.ts
Add ambient global icons IconLucideEyeOff, IconLucideLayout.
Styling / Misc
apps/desktop/src/styles/theme.css, apps/desktop/src/routes/editor/ShadowSettings.tsx
Add top margin to custom scrollbar; tweak hover transition/color on ShadowSettings button.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant LayoutTrack
  participant EditorContext
  participant ProjectState
  participant TauriBridge
  participant Renderer

  User->>LayoutTrack: click/drag to create or modify layout segment
  LayoutTrack->>EditorContext: create/move/resize/select requests
  EditorContext->>ProjectState: immutably update timeline.layoutSegments (sorted)
  ProjectState->>TauriBridge: expose updated TimelineConfiguration (layout_segments)
  TauriBridge->>Renderer: supply layout per-frame
  Renderer->>Renderer: build InterpolatedLayout -> decide layers/opacities
  Renderer-->>User: composited frame (screen / camera / camera_only)
Loading
sequenceDiagram
  autonumber
  participant Timeline
  participant InterpolatedLayout
  participant RendererLayers
  participant WGSL

  Timeline->>InterpolatedLayout: segments + current time
  InterpolatedLayout-->>RendererLayers: camera/screen opacities & mode
  RendererLayers->>RendererLayers: render screen if allowed
  RendererLayers->>RendererLayers: render camera or camera_only per guards
  RendererLayers->>WGSL: send uniforms (including opacity)
  WGSL-->>RendererLayers: shaded/blended frame
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • Brendonovich

Poem

I hopped along the timeline’s edge,
I sketched small lanes where cameras pledge.
Segments slide and previews hum,
opacities fade, transitions come.
A tiny rabbit claps — layout, well-done. 🐰✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch layouts

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@richiemcilroy richiemcilroy changed the title feat: Introduce Layout segments the editor feat: Introduce Layout segments in the editor Aug 18, 2025
Copy link
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: 10

🧹 Nitpick comments (13)
crates/rendering/src/composite_frame.rs (1)

106-117: Avoid per-call sampler creation in bind_group

A new Sampler is created for every bind_group call. Samplers are immutable and cheap to reuse. Consider storing one on CompositeVideoFramePipeline and reusing it.

Possible refactor:

  • Add pub sampler: wgpu::Sampler on the pipeline struct.
  • Create it once in new().
  • Use &self.sampler here and remove this block.
crates/rendering/src/shaders/composite-video-frame.wgsl (1)

85-86: Clarify whether opacity should also fade the shadow

Currently opacity only affects the content alpha; the shadow remains fully visible. If the intended behavior is to fade the entire composite (content + shadow), multiply the shadow alpha by uniforms.opacity too.

Proposed change:

-    let shadow_color = vec4<f32>(0.0, 0.0, 0.0, shadow_strength_final * shadow_opacity);
+    let shadow_color = vec4<f32>(0.0, 0.0, 0.0, shadow_strength_final * shadow_opacity * uniforms.opacity);

If you want shadow independent of content opacity (e.g., camera-only overlays), keep as-is.

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (1)

43-54: Good fix to clear stale hover state; also clear hoveredTime to avoid a ghost preview segment.

When segments are deleted while hovering, resetting hoveringSegment prevents blocked creation. Consider also clearing hoveredTime so the “+” preview doesn’t linger until the next mouse move.

Apply:

 createEffect(() => {
   const segments = project.timeline?.zoomSegments;
   if (!segments || segments.length === 0) {
     setHoveringSegment(false);
+    setHoveredTime(undefined);
   }
 });
CLAUDE.md (2)

373-377: Specify a language for the fenced code block to satisfy markdownlint (MD040).

Use a neutral language like text for the flow diagram.

-```
+```text
 Desktop Recording → Local Files → Upload to S3 → 
 Background Processing (tasks service) → 
 Transcription/AI Enhancement → Database Storage

---

`382-387`: **Replace bare URLs with proper links to satisfy markdownlint (MD034).**

Wrap URLs with angle brackets or full markdown links.


```diff
-- **TanStack Query**: https://tanstack.com/query/latest
-- **React Patterns**: https://react.dev/learn/you-might-not-need-an-effect
-- **Tauri v2**: https://github.com/tauri-apps/tauri
-- **tauri_specta**: https://github.com/oscartbeaumont/tauri-specta
-- **Drizzle ORM**: https://orm.drizzle.team/
-- **SolidJS**: https://solidjs.com/
+- **TanStack Query**: <https://tanstack.com/query/latest>
+- **React Patterns**: <https://react.dev/learn/you-might-not-need-an-effect>
+- **Tauri v2**: <https://github.com/tauri-apps/tauri>
+- **tauri_specta**: <https://github.com/oscartbeaumont/tauri-specta>
+- **Drizzle ORM**: <https://orm.drizzle.team/>
+- **SolidJS**: <https://solidjs.com/>
 
-- **Self-hosting**: https://cap.so/docs/self-hosting
+- **Self-hosting**: <https://cap.so/docs/self-hosting>

Also applies to: 389-389

apps/desktop/src/routes/editor/Timeline/index.tsx (1)

126-138: Also gate selection clear on layout drag state.

The mousedown-up flow only checks zoomSegmentDragState. If a layout segment is being dragged, we can accidentally clear selection.

 createEventListener(e.currentTarget, "mouseup", () => {
   handleUpdatePlayhead(e);
-  if (zoomSegmentDragState.type === "idle") {
+  if (
+    zoomSegmentDragState.type === "idle" &&
+    layoutSegmentDragState.type === "idle"
+  ) {
     setEditorState("timeline", "selection", null);
   }
 });

Also applies to: 130-133

apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2)

32-40: Remove unused WaveformCanvas prop secsPerPixel; context value is used instead.

WaveformCanvas reads secsPerPixel from timeline context and never uses the props.secsPerPixel. Dropping the prop simplifies the interface.

-function WaveformCanvas(props: {
-  systemWaveform?: number[];
-  micWaveform?: number[];
-  segment: { start: number; end: number };
-  secsPerPixel: number;
-}) {
+function WaveformCanvas(props: {
+  systemWaveform?: number[];
+  micWaveform?: number[];
+  segment: { start: number; end: number };
+}) {
   const { project } = useEditorContext();
@@
   return (
     <canvas
@@
-      segment={segment}
-      secsPerPixel={secsPerPixel()}
+      segment={segment}
     />

Also applies to: 130-140, 372-377


222-239: Optional: clarify volume gate semantics for drawing waveforms.

The db thresholds short-circuit rendering (e.g., micVolumeDb < -30). If the intent is “don’t draw when muted/very low volume,” consider making the cutoff a named constant and explicitly handling undefined as “draw.”

- if (project.audio.micVolumeDb && project.audio.micVolumeDb < -30) return;
+ const MIN_DRAW_DB = -30;
+ if (typeof project.audio.micVolumeDb === "number" && project.audio.micVolumeDb < MIN_DRAW_DB) return;

Also applies to: 230-239

apps/desktop/src/routes/editor/context.ts (1)

144-156: Add bounds guard to prevent out-of-range deletions

deleteLayoutSegment should defensively verify the segment index exists before mutating to avoid silent no-ops or surprises if stale indices are passed (e.g., after a sort). Also, short-circuit if the timeline or layoutSegments is absent, mirroring the clip deletion guard.

Consider this tweak:

 deleteLayoutSegment: (segmentIndex: number) => {
   batch(() => {
-    setProject(
+    if (
+      !project.timeline?.layoutSegments ||
+      segmentIndex < 0 ||
+      segmentIndex >= project.timeline.layoutSegments.length
+    ) {
+      return;
+    }
+    setProject(
       "timeline",
       "layoutSegments",
       produce((s) => {
         if (!s) return;
         s.splice(segmentIndex, 1);
       }),
     );
     setEditorState("timeline", "selection", null);
   });
 },
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (2)

317-323: Unify minimum segment duration with hover threshold (0.5s)

Start/end resize constraints use ±1 second, but hover/creation logic uses 0.5 seconds as the minimum viable duration. Align these to avoid UX inconsistencies and “can’t match preview” issues.

Update the constraints:

-                    const maxValue = segment.end - 1;
+                    const maxValue = segment.end - 0.5;
-                    const minValue = segment.start + 1;
+                    const minValue = segment.start + 0.5;

Also applies to: 422-424


59-79: Tighten types for mode parameter

Both helpers accept mode: string | undefined. Narrowing to the actual mode type avoids typos and eases refactors.

If you can import the shared type:

+import type { LayoutSegment as TLayoutSegment } from "~/utils/tauri";
-  const getLayoutIcon = (mode: string | undefined) => {
+  const getLayoutIcon = (mode: TLayoutSegment["mode"]) => {
     switch (mode) {
       case "cameraOnly":
         return <IconLucideVideo class="size-3.5" />;
       case "hideCamera":
         return <IconLucideEyeOff class="size-3.5" />;
       default:
         return <IconLucideMonitor class="size-3.5" />;
     }
   };

-  const getLayoutLabel = (mode: string | undefined) => {
+  const getLayoutLabel = (mode: TLayoutSegment["mode"]) => {
     switch (mode) {
       case "cameraOnly":
         return "Camera Only";
       case "hideCamera":
         return "Hide Camera";
       default:
         return "Default";
     }
   };

Alternatively, define a local union type: type LayoutMode = "default" | "cameraOnly" | "hideCamera" | undefined;

crates/rendering/src/layout.rs (2)

70-81: Unreachable pre-start transition branch (dead code)

Inside the Some(segment) arm, cursor.time ∈ [segment.start, segment.end). The condition cursor.time < segment.start can never be true. This branch is redundant; the “entering next segment” case is already handled in the None/next_segment arm below.

Safe removal:

-            if cursor.time < segment.start && cursor.time >= transition_start {
-                let prev_mode = cursor
-                    .prev_segment
-                    .map(|s| s.mode.clone())
-                    .unwrap_or(LayoutMode::Default);
-                let progress = (cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION;
-                (
-                    prev_mode,
-                    segment.mode.clone(),
-                    ease_in_out(progress as f32) as f64,
-                )
-            } else if cursor.time >= transition_end && cursor.time < segment.end {
+            if cursor.time >= transition_end && cursor.time < segment.end {
                 if let Some(next_seg) = cursor.next_segment() {

83-96: Clamp transition progress to [0, 1] to avoid floating-point drift

Even with range checks, fp error can push progress slightly outside [0, 1]; clamping before easing is cheap and robust.

-                    let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION;
+                    let progress = ((cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION).clamp(0.0, 1.0);
                     (
                         segment.mode.clone(),
                         next_seg.mode.clone(),
                         ease_in_out(progress as f32) as f64,
                     )
                 } else {
-                    let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION;
+                    let progress = ((cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION).clamp(0.0, 1.0);
                     (
                         segment.mode.clone(),
                         LayoutMode::Default,
                         ease_in_out(progress as f32) as f64,
                     )
                 }
-            let transition_start = next_segment.start - LAYOUT_TRANSITION_DURATION;
+            let transition_start = next_segment.start - LAYOUT_TRANSITION_DURATION;
             if cursor.time >= transition_start {
                 let prev_mode = cursor
                     .prev_segment
                     .map(|s| s.mode.clone())
                     .unwrap_or(LayoutMode::Default);
-                let progress = (cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION;
+                let progress = ((cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION).clamp(0.0, 1.0);
                 (
                     prev_mode,
                     next_segment.mode.clone(),
                     ease_in_out(progress as f32) as f64,
                 )

Also applies to: 107-112

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4e11eab and c8c732b.

📒 Files selected for processing (14)
  • CLAUDE.md (1 hunks)
  • apps/desktop/src-tauri/src/recording.rs (1 hunks)
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2 hunks)
  • apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (1 hunks)
  • apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (4 hunks)
  • apps/desktop/src/routes/editor/Timeline/index.tsx (5 hunks)
  • apps/desktop/src/routes/editor/context.ts (3 hunks)
  • apps/desktop/src/utils/tauri.ts (1 hunks)
  • crates/project/src/configuration.rs (1 hunks)
  • crates/rendering/src/composite_frame.rs (1 hunks)
  • crates/rendering/src/layout.rs (1 hunks)
  • crates/rendering/src/lib.rs (16 hunks)
  • crates/rendering/src/shaders/composite-video-frame.wgsl (3 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
crates/project/src/configuration.rs (1)
apps/desktop/src/utils/tauri.ts (4)
  • LayoutSegment (724-728)
  • TimelineConfiguration (730-734)
  • TimelineSegment (735-740)
  • ZoomSegment (780-785)
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (1)
apps/desktop/src/routes/editor/Timeline/Track.tsx (4)
  • TrackRoot (12-26)
  • SegmentRoot (49-87)
  • SegmentHandle (103-125)
  • SegmentContent (89-101)
apps/desktop/src/routes/editor/Timeline/index.tsx (3)
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (2)
  • LayoutSegmentDragState (28-31)
  • LayoutTrack (33-486)
apps/desktop/src/routes/editor/context.ts (1)
  • meta (303-305)
crates/editor/src/editor_instance.rs (1)
  • meta (106-108)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (1)
crates/editor/src/segments.rs (1)
  • segments (6-33)
crates/rendering/src/layout.rs (1)
apps/desktop/src/utils/tauri.ts (1)
  • LayoutSegment (724-728)
crates/rendering/src/lib.rs (2)
crates/rendering/src/layout.rs (2)
  • new (14-43)
  • new (63-164)
crates/rendering/src/layers/camera.rs (1)
  • new (18-42)
🪛 LanguageTool
CLAUDE.md

[grammar] ~9-~9: There might be a mistake here.
Context: ...macOS and Windows). ### Product Context - Core Purpose: Screen recording with in...

(QB_NEW_EN)


[grammar] ~10-~10: There might be a mistake here.
Context: ...ording with instant sharing capabilities - Target Users: Content creators, develo...

(QB_NEW_EN)


[grammar] ~11-~11: There might be a mistake here.
Context: ...elopers, product managers, support teams - Key Features: Instant recording, studi...

(QB_NEW_EN)


[grammar] ~12-~12: There might be a mistake here.
Context: ...nerated captions, collaborative comments - Business Model: Freemium SaaS with usa...

(QB_NEW_EN)


[grammar] ~82-~82: There might be a mistake here.
Context: ...chema managed through Drizzle migrations - Storage: S3-compatible (AWS, Cloudflar...

(QB_NEW_EN)


[grammar] ~85-~85: There might be a mistake here.
Context: ...s ### Auto-generated Bindings (Desktop) - NEVER EDIT: tauri.ts, queries.ts (...

(QB_NEW_EN)


[grammar] ~86-~86: There might be a mistake here.
Context: ...queries.ts(auto-generated on app load) - **NEVER EDIT**: Files underapps/desktop/...

(QB_NEW_EN)


[grammar] ~87-~87: There might be a mistake here.
Context: ...app load) - NEVER EDIT: Files under apps/desktop/src-tauri/gen/ - Icons: Auto-imported in desktop app; d...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...d in desktop app; do not import manually - Regeneration: These files update autom...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ...ange ### Common Development Pain Points - Node Version: Must use Node 20 (specif...

(QB_NEW_EN)


[grammar] ~110-~110: There might be a mistake here.
Context: ...ding, camera, etc. ### Technology Stack - Package Manager: pnpm (pnpm@10.5.2) ...

(QB_NEW_EN)


[grammar] ~111-~111: There might be a mistake here.
Context: ...Package Manager*: pnpm (pnpm@10.5.2) - Build System: Turborepo - **Frontend (...

(QB_NEW_EN)


[grammar] ~112-~112: There might be a mistake here.
Context: ...m@10.5.2`) - Build System: Turborepo - Frontend (Web): React 19 + Next.js 14....

(QB_NEW_EN)


[grammar] ~113-~113: There might be a mistake here.
Context: ...: React 19 + Next.js 14.2.x (App Router) - Desktop: Tauri v2, Rust 2024, SolidSta...

(QB_NEW_EN)


[grammar] ~114-~114: There might be a mistake here.
Context: ...sktop**: Tauri v2, Rust 2024, SolidStart - Styling: Tailwind CSS (web consumes `@...

(QB_NEW_EN)


[grammar] ~115-~115: There might be a mistake here.
Context: ...nd CSS (web consumes @cap/ui/tailwind) - Server State: TanStack Query v5 on web...

(QB_NEW_EN)


[grammar] ~116-~116: There might be a mistake here.
Context: ... web; @tanstack/solid-query on desktop - Database: MySQL (PlanetScale) with Dri...

(QB_NEW_EN)


[grammar] ~117-~117: There might be a mistake here.
Context: ...**: MySQL (PlanetScale) with Drizzle ORM - AI Integration: Groq preferred, OpenAI...

(QB_NEW_EN)


[grammar] ~118-~118: There might be a mistake here.
Context: ...lback; invoked in Next.js Server Actions - Analytics: PostHog - Payments: Str...

(QB_NEW_EN)


[grammar] ~119-~119: There might be a mistake here.
Context: ... Server Actions - Analytics: PostHog - Payments: Stripe ### Critical Archite...

(QB_NEW_EN)


[grammar] ~125-~125: There might be a mistake here.
Context: ...t proper CORS, and revalidate precisely. 4. Desktop IPC: Use tauri_specta for st...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ...ed bindings. #### Desktop event pattern Rust (emit): ```rust use specta::Type; u...

(QB_NEW_EN)


[grammar] ~155-~155: There might be a mistake here.
Context: ...ctices ### Code Organization Principles 1. Follow Local Patterns: Study neighbori...

(QB_NEW_EN)


[grammar] ~223-~223: There might be a mistake here.
Context: ...EB_URL-NEXT_PUBLIC_CAP_AWS_BUCKET, NEXT_PUBLIC_CAP_AWS_REGION-NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST-NEXT_PUB...

(QB_NEW_EN)


[grammar] ~227-~227: There might be a mistake here.
Context: ...tandalone output) ### Server (selected) - Core: DATABASE_URL, WEB_URL, `NEXTAU...

(QB_NEW_EN)


[grammar] ~228-~228: There might be a mistake here.
Context: ...ASE_URL, WEB_URL, NEXTAUTH_SECRET, NEXTAUTH_URL- S3:CAP_AWS_BUCKET, CAP_AWS_REGION`, ...

(QB_NEW_EN)


[grammar] ~229-~229: There might be a mistake here.
Context: ...CRET_KEY, optional CAP_AWS_ENDPOINT, CAP_AWS_BUCKET_URL- AI:GROQ_API_KEY, OPENAI_API_KEY` - E...

(QB_NEW_EN)


[grammar] ~230-~230: There might be a mistake here.
Context: ...P_AWS_BUCKET_URL- AI:GROQ_API_KEY, OPENAI_API_KEY- Email/Analytics:RESEND_API_KEY, RESE...

(QB_NEW_EN)


[grammar] ~231-~231: There might be a mistake here.
Context: ...STHOG_PERSONAL_API_KEY, DUB_API_KEY, DEEPGRAM_API_KEY- OAuth:GOOGLE_CLIENT_ID/SECRET, WORKO...

(QB_NEW_EN)


[grammar] ~232-~232: There might be a mistake here.
Context: ..._CLIENT_ID/SECRET, WORKOS_CLIENT_ID, WORKOS_API_KEY- Stripe:STRIPE_SECRET_KEY_TEST, STRIP...

(QB_NEW_EN)


[grammar] ~233-~233: There might be a mistake here.
Context: ...ET_KEY_TEST, STRIPE_SECRET_KEY_LIVE, STRIPE_WEBHOOK_SECRET- CDN signing:CLOUDFRONT_KEYPAIR_ID, C...

(QB_NEW_EN)


[grammar] ~234-~234: There might be a mistake here.
Context: ...- CDN signing: CLOUDFRONT_KEYPAIR_ID, CLOUDFRONT_KEYPAIR_PRIVATE_KEY - Optional S3 endpoints: `S3_PUBLIC_ENDPOI...

(QB_NEW_EN)


[grammar] ~239-~239: There might be a mistake here.
Context: ...Build Optimization ### Testing Strategy - Package-Specific: Check each `package....

(QB_NEW_EN)


[grammar] ~246-~246: There might be a mistake here.
Context: ...mework for crates ### Build Performance - Turborepo Caching: Aggressive caching ...

(QB_NEW_EN)


[grammar] ~247-~247: There might be a mistake here.
Context: ...: Aggressive caching across all packages - Cache Invalidation: Prefer targeted `-...

(QB_NEW_EN)


[grammar] ~248-~248: There might be a mistake here.
Context: ...targeted --filter over global rebuilds - Docker Builds: `NEXT_PUBLIC_DOCKER_BUI...

(QB_NEW_EN)


[grammar] ~249-~249: There might be a mistake here.
Context: ...ER_BUILD=true` enables standalone output - Development: Incremental builds via Ty...

(QB_NEW_EN)


[grammar] ~252-~252: There might be a mistake here.
Context: ...t references ### Performance Monitoring - Bundle Analysis: Check Next.js bundle ...

(QB_NEW_EN)


[grammar] ~260-~260: There might be a mistake here.
Context: ...ooting Common Issues ### Build Failures - "Cannot find module": Check workspace ...

(QB_NEW_EN)


[grammar] ~263-~263: There might be a mistake here.
Context: ...es - Turbo cache issues: Clear with rm -rf .turbo - Node version mismatch: Ensure Node 20 ...

(QB_NEW_EN)


[grammar] ~266-~266: There might be a mistake here.
Context: ...e Node 20 is active ### Database Issues - Migration failures: Check `packages/da...

(QB_NEW_EN)


[grammar] ~271-~271: There might be a mistake here.
Context: ...tabase db:check` ### Desktop App Issues - IPC binding errors: Restart dev server...

(QB_NEW_EN)


[grammar] ~277-~277: There might be a mistake here.
Context: ... capture permissions ### Web App Issues - Auth failures: Check NextAuth configur...

(QB_NEW_EN)


[grammar] ~287-~287: There might be a mistake here.
Context: ...omponents and let React Query take over. - Mutations should call Server Actions dir...

(QB_NEW_EN)


[grammar] ~329-~329: There might be a mistake here.
Context: ...riptions/timers. ### Next.js App Router - Prefer Server Components for SEO/initial...

(QB_NEW_EN)


[grammar] ~330-~330: There might be a mistake here.
Context: ...rate interactivity in client components. - Co-locate feature components, keep compo...

(QB_NEW_EN)


[grammar] ~345-~345: There might be a mistake here.
Context: ...y. - Directory naming: lower-case-dashed - Components: PascalCase; hooks: camelCase...

(QB_NEW_EN)


[grammar] ~346-~346: There might be a mistake here.
Context: ...calCase; hooks: camelCase starting with use - Strict TypeScript; avoid any; leverage...

(QB_NEW_EN)


[grammar] ~347-~347: There might be a mistake here.
Context: ...ript; avoid any; leverage shared types - Use Biome for linting/formatting; match ...

(QB_NEW_EN)


[grammar] ~352-~352: There might be a mistake here.
Context: ...rivacy Considerations ### Data Handling - Video Storage: S3-compatible storage w...

(QB_NEW_EN)


[grammar] ~353-~353: There might be a mistake here.
Context: ...: S3-compatible storage with signed URLs - Database: MySQL with connection poolin...

(QB_NEW_EN)


[grammar] ~354-~354: There might be a mistake here.
Context: ... with connection pooling via PlanetScale - Authentication: NextAuth with custom D...

(QB_NEW_EN)


[grammar] ~355-~355: There might be a mistake here.
Context: ...**: NextAuth with custom Drizzle adapter - API Security: CORS policies, rate limi...

(QB_NEW_EN)


[grammar] ~358-~358: There might be a mistake here.
Context: ...ia Hono middleware ### Privacy Controls - Recording Permissions: Platform-specif...

(QB_NEW_EN)


[grammar] ~359-~359: There might be a mistake here.
Context: ...ecific (macOS Screen Recording, Windows) - Data Retention: User-controlled deleti...

(QB_NEW_EN)


[grammar] ~360-~360: There might be a mistake here.
Context: ...: User-controlled deletion of recordings - Sharing Controls: Password protection,...

(QB_NEW_EN)


[grammar] ~361-~361: There might be a mistake here.
Context: ...protection, expiry dates on shared links - Analytics: PostHog with privacy-focuse...

(QB_NEW_EN)


[grammar] ~366-~366: There might be a mistake here.
Context: ...sing Pipeline ### AI Integration Points - Transcription: Deepgram API for captio...

(QB_NEW_EN)


[grammar] ~367-~367: There might be a mistake here.
Context: ...**: Deepgram API for captions generation - Metadata Generation: Groq (primary) + ...

(QB_NEW_EN)


[grammar] ~368-~368: There might be a mistake here.
Context: ...penAI (fallback) for titles/descriptions - Processing Location: All AI calls in N...

(QB_NEW_EN)


[grammar] ~369-~369: There might be a mistake here.
Context: ... AI calls in Next.js Server Actions only - Privacy: Transcripts stored in databas...

(QB_NEW_EN)


[grammar] ~381-~381: There might be a mistake here.
Context: ...s & Documentation ### Core Technologies - TanStack Query: https://tanstack.com/q...

(QB_NEW_EN)


[grammar] ~382-~382: There might be a mistake here.
Context: ...ery**: https://tanstack.com/query/latest - React Patterns: https://react.dev/lear...

(QB_NEW_EN)


[grammar] ~383-~383: There might be a mistake here.
Context: ...t.dev/learn/you-might-not-need-an-effect - Tauri v2: https://github.com/tauri-app...

(QB_NEW_EN)


[grammar] ~384-~384: There might be a mistake here.
Context: ...2**: https://github.com/tauri-apps/tauri - tauri_specta: https://github.com/oscar...

(QB_NEW_EN)


[grammar] ~385-~385: There might be a mistake here.
Context: ...//github.com/oscartbeaumont/tauri-specta - Drizzle ORM: https://orm.drizzle.team/...

(QB_NEW_EN)


[grammar] ~386-~386: There might be a mistake here.
Context: ...Drizzle ORM**: https://orm.drizzle.team/ - SolidJS: https://solidjs.com/ ### Cap...

(QB_NEW_EN)


[grammar] ~389-~389: There might be a mistake here.
Context: ...: https://solidjs.com/ ### Cap-Specific - Self-hosting: https://cap.so/docs/self...

(QB_NEW_EN)


[grammar] ~390-~390: There might be a mistake here.
Context: ...ting**: https://cap.so/docs/self-hosting - API Documentation: Generated from Type...

(QB_NEW_EN)


[grammar] ~391-~391: There might be a mistake here.
Context: ...n**: Generated from TypeScript contracts - Architecture Decisions: See individual...

(QB_NEW_EN)

🪛 markdownlint-cli2 (0.17.2)
CLAUDE.md

373-373: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


382-382: Bare URL used

(MD034, no-bare-urls)


383-383: Bare URL used

(MD034, no-bare-urls)


384-384: Bare URL used

(MD034, no-bare-urls)


385-385: Bare URL used

(MD034, no-bare-urls)


386-386: Bare URL used

(MD034, no-bare-urls)


387-387: Bare URL used

(MD034, no-bare-urls)


390-390: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (17)
crates/project/src/configuration.rs (1)

480-482: LGTM: timeline.layout_segments defaulting is correct

Using #[serde(default)] ensures older projects load without errors and the field remains optional from the UI perspective.

packages/ui-solid/src/auto-imports.d.ts (1)

7-94: Formatting-only changes; no API impact

Ambient declarations and the export statement were reformatted. No functional changes detected.

apps/desktop/src-tauri/src/recording.rs (1)

988-989: LGTM: initialize layout_segments for new projects

Adding layout_segments: Vec::new() aligns the default project config with the new timeline field and avoids missing-field issues when rendering or in the editor.

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

172-175: Empty-state UX improvement LGTM.

The neutral background, helper subtitle, and pointer-events-none reduce accidental interactions and clarify the CTA. No further changes needed.


367-395: Responsive content by width is sensible and readable.

The tiered rendering (icon-only → icon+amount → full content) is a pragmatic way to keep segments readable at various scales. No functional issues noted.

apps/desktop/src/routes/editor/Timeline/index.tsx (2)

237-244: Gating LayoutTrack by hasCamera is a good call.

This avoids exposing layout controls when there’s no camera feed, keeping the UI contextual.


103-105: Deletion path for layout selections is wired correctly.

Routing Delete/Backspace to projectActions.deleteLayoutSegment on selection.type === "layout" aligns with the new selection variant.

apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (1)

336-369: Single-clip selection behavior tweak is reasonable.

Clearing selection when only one segment exists avoids opening an empty config panel. The mouseup-root/dispose pattern remains correct and prevents leaks.

Also applies to: 351-367

apps/desktop/src/routes/editor/context.ts (3)

125-128: Good fix: use in-place splice for deletion

Switching to in-place splice avoids accidentally replacing the array with the removed items. Clearing the selection afterward is consistent with other delete actions.


138-143: Consistent deletion flow and selection reset

Using splice for zoomSegments and clearing selection keeps behavior aligned with clip deletions. Looks good.


254-256: Layout selection handling verified
All downstream consumers of editorState.timeline.selection have been updated to handle { type: "layout" }, including:

  • Timeline delete key shortcuts in apps/desktop/src/routes/editor/Timeline/index.tsx
  • Selection checks in ZoomTrack.tsx, ClipTrack.tsx, and LayoutTrack.tsx

No missing handlers found.

crates/rendering/src/lib.rs (6)

568-576: Interpolate layout per-frame from timeline segments — LGTM

Computing InterpolatedLayout via LayoutSegmentsCursor against project.timeline.layout_segments is clean and aligns with the zoom interpolation pattern.


638-639: Screen opacity driven by layout — LGTM

Feeding layout.screen_opacity into CompositeVideoFrameUniforms.opacity cleanly gates the display layer.


645-662: Camera uniforms integrate layout state and shape-aware cropping — LGTM

  • Properly gate camera with should_render_camera().
  • Apply layout.camera_scale to zoomed_size.
  • Handle Square vs Source crop_bounds correctly; avoids stretching.
  • Opacity from regular_camera_transition_opacity covers transitions gracefully.

Also applies to: 707-716, 747-750


752-811: Camera-only path is well-isolated and fills output with safe cropping

The camera_only uniforms ignore shadow/rounding and use aspect-aware cropping to fill the output without distortion — appropriate for fullscreen transitions. Opacity driven by camera_only_transition_opacity is a good separation of responsibilities.


1009-1030: Render guards match layout semantics

  • Screen and cursor are gated by should_render_screen (hidden in camera-only).
  • camera_only renders during any CameraOnly involvement.
  • Regular camera renders only when needed and with layout-driven opacity.

This staging prevents double-render and blends transitions predictably.


1169-1173: Confirm white fallback in parse_color_component

The implementation in crates/rendering/src/lib.rs (fn parse_color_component, lines 1158–1173) now defaults R/G/B to 1.0 (white) whenever parsing fails. Previously this would have produced black (0.0) channels, so this change may lead to white text or backgrounds if an invalid or too-short hex string is passed.

Please confirm whether switching the fallback from black to white was intentional (and update any docs or add tests), or consider reverting to the original black fallback behavior.

• File: crates/rendering/src/lib.rs
fn parse_color_component(hex_color: &str, index: usize) -> f32
– match index { 0 => 1.0, 1 => 1.0, 2 => 1.0, _ => 1.0 }

Comment on lines +724 to +729
export type LayoutSegment = {
start: number;
end: number;
mode?: "default" | "cameraOnly" | "hideCamera";
};

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Type bridging looks correct; double-check defaulting/serde behavior for layoutSegments/mode.

The TS types for LayoutSegment and TimelineConfiguration.layoutSegments align with the PR’s Rust-side intent. Since this file is generated, no action is needed here. Two checks to avoid runtime/type drift:

  • Ensure Rust TimelineConfiguration has layout_segments with serde(default) so older projects without this field deserialize to an empty list, not None.
  • Confirm the optional LayoutSegment.mode in TS corresponds to either Option in Rust or a LayoutMode with a default. If omitted in TS, the backend should treat it as Default.

Run this to confirm Rust-side defaults and field names:

Also applies to: 730-734


🏁 Script executed:

#!/bin/bash
# Verify layout_segments presence and serde(default)
rg -nP --type=rust -C3 '\blayout_segments\b' | sed -n '1,200p'

# Verify LayoutSegment struct and LayoutMode enum
rg -nP --type=rust -C3 'struct\s+LayoutSegment|enum\s+LayoutMode' | sed -n '1,200p'

Length of output: 2843


Ensure defaulting for LayoutSegment.mode to avoid deserialization errors

We’ve verified:

  • layout_segments: Vec<LayoutSegment> is annotated with #[serde(default)] (crates/project/src/configuration.rs:481), so missing the entire field falls back to an empty list.
  • LayoutSegment is defined without a default on its mode field (crates/project/src/configuration.rs:469–472):
    #[derive(Type, Serialize, Deserialize, Clone, Debug)]
    #[serde(rename_all = "camelCase")]
    pub struct LayoutSegment {
        pub start: f64,
        pub end: f64,
        pub mode: LayoutMode,
    }
  • LayoutMode is a non-optional enum (Default, CameraOnly, HideCamera) with no Default impl.

Because the generated TS type makes mode optional (mode?: …), omitting it in JSON will cause Rust deserialization to fail. To align behaviors, please update the Rust side by either:

  • Adding #[serde(default)] to the mode field and deriving or implementing Default for LayoutMode (so missing modes map to LayoutMode::Default), or
  • Changing mode to Option<LayoutMode> if you prefer explicit optionality.

No TS changes are needed—the generator can continue producing mode?: ….

🤖 Prompt for AI Agents
In apps/desktop/src/utils/tauri.ts around lines 724 to 729, the generated
TypeScript makes LayoutSegment.mode optional but the Rust LayoutSegment
currently requires a non-optional LayoutMode, causing deserialization failures
when mode is omitted; fix by updating the Rust type: either add
#[serde(default)] to the mode field and implement/derive Default for LayoutMode
so missing values map to LayoutMode::Default, or change the mode field to
Option<LayoutMode> to allow explicit omission—apply one of these two changes in
crates/project/src/configuration.rs where LayoutSegment and LayoutMode are
defined.

Copy link
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

♻️ Duplicate comments (2)
apps/desktop/src/routes/editor/Timeline/index.tsx (2)

79-90: Pause playhead updates while dragging layout segments too.

This was already flagged in a prior review; please gate on both drag states to prevent jitter while moving layout segments.

Apply:

-  if (zoomSegmentDragState.type !== "moving") {
+  if (
+    zoomSegmentDragState.type !== "moving" &&
+    layoutSegmentDragState.type !== "moving"
+  ) {

141-152: Don’t clear selection when either segment type is being dragged.

Also previously noted: extend the guard to include layoutSegmentDragState to avoid clearing selection mid-drag.

Apply:

-              if (zoomSegmentDragState.type === "idle") {
+              if (
+                zoomSegmentDragState.type === "idle" &&
+                layoutSegmentDragState.type === "idle"
+              ) {
                 setEditorState("timeline", "selection", null);
               }
🧹 Nitpick comments (5)
apps/desktop/src/routes/editor/context.ts (2)

144-160: Bulk delete: dedupe and bounds-check indices to avoid accidental removals.

Current approach sorts and splices descending, which handles shifting, but duplicates or out-of-range indices can still lead to surprises. Consider deduping and filtering to valid indices before splicing.

Apply this diff inside the produce block to harden the routine:

-              // Sort indices in descending order to avoid index shifting issues
-              const sortedIndices = [...segmentIndices].sort((a, b) => b - a);
-              sortedIndices.forEach((index) => {
-                s.splice(index, 1);
-              });
+              // Dedupe, bounds-check, and sort descending to avoid index shifting issues
+              const uniqueDesc = Array.from(new Set(segmentIndices))
+                .filter((i) => Number.isInteger(i) && i >= 0 && i < s.length)
+                .sort((a, b) => b - a);
+              uniqueDesc.forEach((index) => {
+                s.splice(index, 1);
+              });

161-172: Add parity: consider a bulk delete for layout segments.

You added deleteLayoutSegment; mirroring deleteZoomSegments with deleteLayoutSegments would help parity and simplify callers that operate on multi-select.

I can draft an implementation if you want to support multi-select layout deletions in this PR.

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (3)

43-54: Hover-reset effect addresses stuck state after deletions.

This resolves the “hoveringSegment stays true after deletion” issue for the empty-list case.

If you also want to handle “delete hovered segment while others remain,” consider clearing hoveringSegment whenever hoveredTime becomes undefined, or when the specific DOM element is removed (e.g., track state or selection changes).


213-273: Multi-select toggle: ensure uniqueness and stable ordering.

The toggle logic is correct but can accumulate duplicates and produce unsorted arrays depending on user actions. Normalize indices for stability.

Apply this diff within the non-moved click path:

-                  if (e.ctrlKey || e.metaKey) {
+                  if (e.ctrlKey || e.metaKey) {
                     if (currentSelection?.type === "zoom") {
                       // If we already have zoom selections
                       if (
                         "indices" in currentSelection &&
                         Array.isArray(currentSelection.indices)
                       ) {
-                        // Toggle this segment in the selection
-                        const newIndices = currentSelection.indices.includes(
-                          segmentIndex,
-                        )
-                          ? currentSelection.indices.filter(
-                              (idx) => idx !== segmentIndex,
-                            )
-                          : [...currentSelection.indices, segmentIndex];
+                        // Toggle with uniqueness + sorted order
+                        const set = new Set(currentSelection.indices);
+                        if (set.has(segmentIndex)) set.delete(segmentIndex);
+                        else set.add(segmentIndex);
+                        const newIndices = Array.from(set).sort((a, b) => a - b);
                         setEditorState(
                           "timeline",
                           "selection",
                           newIndices.length > 0
                             ? { type: "zoom", indices: newIndices }
                             : null,
                         );
                       } else if (
                         "index" in currentSelection &&
                         typeof currentSelection.index === "number"
                       ) {
-                        // Convert single selection to multi-selection
-                        const newIndices =
-                          currentSelection.index === segmentIndex
-                            ? [] // Deselect if clicking the same segment
-                            : [currentSelection.index, segmentIndex];
+                        // Convert single selection to multi-selection (dedup + sorted)
+                        const base = new Set<number>();
+                        if (currentSelection.index !== segmentIndex) {
+                          base.add(currentSelection.index);
+                          base.add(segmentIndex);
+                        }
+                        const newIndices = Array.from(base).sort((a, b) => a - b);
                         setEditorState(
                           "timeline",
                           "selection",
                           newIndices.length > 0
                             ? { type: "zoom", indices: newIndices }
                             : null,
                         );
                       }

310-333: Minor perf nit: reuse the reactive index rather than findIndex.

For isSelected, you can use i() directly instead of findIndex over zoomSegments each time; keeps computations O(1).

Example:

- const segmentIndex = project.timeline?.zoomSegments?.findIndex(
-   (s) => s.start === segment.start && s.end === segment.end,
- );
+ const segmentIndex = i();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c8c732b and eaac5fb.

📒 Files selected for processing (4)
  • apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (1 hunks)
  • apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (6 hunks)
  • apps/desktop/src/routes/editor/Timeline/index.tsx (6 hunks)
  • apps/desktop/src/routes/editor/context.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/Timeline/index.tsx (3)
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (2)
  • LayoutSegmentDragState (27-30)
  • LayoutTrack (32-488)
apps/desktop/src/routes/editor/context.ts (1)
  • meta (321-323)
crates/editor/src/editor_instance.rs (1)
  • meta (106-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (9)
apps/desktop/src/routes/editor/context.ts (2)

123-129: Deletion fix is correct (splice without assigning).

Good catch mutating the draft with splice instead of assigning the splice result. This avoids replacing the array with the removed items.


138-143: Consistent deletion + selection reset.

Zoom segment deletion mirrors clip deletion semantics and correctly clears the selection. Looks good.

apps/desktop/src/routes/editor/Timeline/index.tsx (5)

99-111: Bulk vs single zoom deletion handling is solid.

Correct branching for multi-select (indices) vs single selection (index), delegating to the new context actions.


113-115: Layout deletion wiring looks correct.

Keyboard delete flows through to deleteLayoutSegment as expected.


120-123: Escape-to-deselect is a nice UX addition.

Clear and consistent with the selection model.


251-258: Conditional render of LayoutTrack is appropriate.

Gating the LayoutTrack behind meta().hasCamera and wiring drag state + playhead handler aligns with the overall design.


284-284: Marking dot class tweak is fine.

No functional impact; style looks consistent with surrounding code.

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

172-177: Empty-state UX improvements look good.

Pointer-events: none keeps the track clickable; the message is clearer.


441-468: Ignore missing import suggestion for IconLucideSearch
IconLucideSearch is provided by our icon auto-import setup (see packages/ui-solid/src/auto-imports.d.ts) and does not require an explicit import here. No changes needed.

Likely an incorrect or invalid review comment.

Copy link
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

🧹 Nitpick comments (4)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (4)

597-615: Add single-selection/index guard for Layout config (align with Zoom)

Unlike the Zoom guard, the Layout guard doesn’t validate index presence/type. Adding it avoids accidental rendering when multi-selected or when selection shape differs.

Apply this diff:

 <Show
   when={(() => {
     const layoutSelection = selection();
     if (layoutSelection.type !== "layout") return;
 
+    // Only show config panel for single layout segment selection
+    if (
+      !("index" in layoutSelection) ||
+      typeof layoutSelection.index !== "number"
+    ) {
+      return;
+    }
+
     const segment =
       project.timeline?.layoutSegments?.[layoutSelection.index];
     if (!segment) return;
 
     return { selection: layoutSelection, segment };
   })()}
 >

1864-1873: Reset preview loading state when switching video sources

When video.src changes, loaded() remains true until the next onloadeddata, so the “Loading preview…” overlay won’t show for new sources. Reset it to false on source change.

Apply this diff (uses a microtask to avoid ordering issues):

 createEffect(() => {
   const path = convertFileSrc(
     // TODO: this shouldn't be so hardcoded
     `${editorInstance.path}/content/segments/segment-${segmentIndex()}/display.mp4`,
   );
-  video.src = path;
-  // Force reload if video fails to load
-  video.load();
+  // Reset loading state until the new source is ready
+  queueMicrotask(() => setLoaded(false));
+  video.src = path;
+  // Force reload to ensure the new source is loaded
+  video.load();
 });

Alternatively (cleaner), move the const [loaded, setLoaded] = createSignal(false) declaration above this effect and call setLoaded(false) directly.


1884-1889: Avoid accumulating canplay listeners; use { once: true }

If relativeTime() updates before the video becomes ready, multiple canplay listeners can accrue. Use a one-off listener.

-    video.addEventListener("canplay", handleCanPlay);
+    video.addEventListener("canplay", handleCanPlay, { once: true });

1937-1944: Consider capping video load retries to avoid tight error loops

On persistent failures, the current logic will keep retrying every 100ms. Add a retry cap.

For example:

// Near the video element creation
let loadRetries = 0;
const MAX_RETRIES = 3;

// ...
video.onerror = (e) => {
  console.error("Failed to load video for zoom preview:", e);
  if (loadRetries < MAX_RETRIES) {
    loadRetries++;
    setTimeout(() => video.load(), 100);
  } else {
    console.error("Giving up after MAX_RETRIES");
  }
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eaac5fb and 8b89c59.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)
apps/desktop/src/utils/tauri.ts (1)
  • LayoutSegment (724-728)
apps/desktop/src/routes/editor/ui.tsx (2)
  • EditorButton (352-412)
  • Field (25-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (3)

576-582: Good: single-selection guard for Zoom segment config

Guarding Zoom config behind a single-segment selection prevents ambiguous edits and matches the intended UX.


2027-2042: Nice UX: loading overlay + opacity transition for zoom preview

The loading gate, fade-in, and absolute overlay improve perceived responsiveness and prevent janky renders.


2156-2261: LayoutSegmentConfig: solid addition with clear UX

  • Done/Delete actions follow existing patterns.
  • Mode tabs map cleanly to LayoutSegment["mode"], with sensible default fallback.
  • Indicator/description update based on mode is a nice touch.

Only follow-ups are the import fix (see earlier comment) and the optional selection guard (see layout gating comment).

Comment on lines +46 to 52
type LayoutSegment,
type StereoMode,
type TimelineSegment,
type ZoomSegment,
} from "~/utils/tauri";
import IconLucideMonitor from "~icons/lucide/monitor";
import IconLucideSparkles from "~icons/lucide/sparkles";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing IconLucideLayout import will break rendering

<IconLucideLayout /> is used in LayoutSegmentConfig but not imported. This will cause a runtime/build error.

Apply this diff to add the missing import:

 import {
   type BackgroundSource,
   type CameraShape,
   commands,
   type LayoutSegment,
   type StereoMode,
   type TimelineSegment,
   type ZoomSegment,
 } from "~/utils/tauri";
+import IconLucideLayout from "~icons/lucide/layout";
 import IconLucideMonitor from "~icons/lucide/monitor";
📝 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
type LayoutSegment,
type StereoMode,
type TimelineSegment,
type ZoomSegment,
} from "~/utils/tauri";
import IconLucideMonitor from "~icons/lucide/monitor";
import IconLucideSparkles from "~icons/lucide/sparkles";
import {
type BackgroundSource,
type CameraShape,
commands,
type LayoutSegment,
type StereoMode,
type TimelineSegment,
type ZoomSegment,
} from "~/utils/tauri";
import IconLucideLayout from "~icons/lucide/layout";
import IconLucideMonitor from "~icons/lucide/monitor";
import IconLucideSparkles from "~icons/lucide/sparkles";
🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/ConfigSidebar.tsx around lines 46 to 52, the
component uses <IconLucideLayout /> but the import is missing; add the missing
import statement for IconLucideLayout from the icon package (consistent with the
other icon imports, e.g., import IconLucideLayout from "~icons/lucide/layout";)
so the LayoutSegmentConfig can render without build/runtime errors.

Copy link
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: 0

♻️ Duplicate comments (1)
apps/desktop/src/routes/editor/Timeline/index.tsx (1)

81-84: Pause playhead during any non-idle drag state (not just "moving").

Condition currently only blocks updates while type === "moving". If other drag states exist (e.g., resizing), the playhead will still update and may cause jitter. Prefer checking explicitly for idle.

Apply this diff:

-    if (
-      zoomSegmentDragState.type !== "moving" &&
-      layoutSegmentDragState.type !== "moving"
-    ) {
+    if (
+      zoomSegmentDragState.type === "idle" &&
+      layoutSegmentDragState.type === "idle"
+    ) {
🧹 Nitpick comments (4)
crates/rendering/src/composite_frame.rs (1)

32-53: Default now sets opacity = 1.0 — nice; consider a concise impl using Zeroable::zeroed()

Functionally perfect. Minor readability/maintenance tweak: leverage Zeroable to avoid listing every field, which helps when fields are added/removed later.

 impl Default for CompositeVideoFrameUniforms {
     fn default() -> Self {
-        Self {
-            crop_bounds: Default::default(),
-            target_bounds: Default::default(),
-            output_size: Default::default(),
-            frame_size: Default::default(),
-            velocity_uv: Default::default(),
-            target_size: Default::default(),
-            rounding_px: Default::default(),
-            mirror_x: Default::default(),
-            motion_blur_amount: Default::default(),
-            camera_motion_blur_amount: Default::default(),
-            shadow: Default::default(),
-            shadow_size: Default::default(),
-            shadow_opacity: Default::default(),
-            shadow_blur: Default::default(),
-            opacity: 1.0,
-            _padding: Default::default(),
-        }
+        Self { opacity: 1.0, ..bytemuck::Zeroable::zeroed() }
     }
 }
apps/desktop/src/routes/editor/Timeline/index.tsx (3)

102-114: Deletion flow: clear selection after zoom deletions and prevent default browser behavior.

  • After deleting zoom segments, selection can point to stale indices. Clear it.
  • Also prevent default Backspace/Delete behavior to avoid unintended navigation or OS-level actions (especially in Tauri/Web contexts).

Apply this diff to clear selection after deletions:

           if ("indices" in selection && Array.isArray(selection.indices)) {
             // Use bulk deletion for better performance
             projectActions.deleteZoomSegments(selection.indices);
+            setEditorState("timeline", "selection", null);
           } else if (
             "index" in selection &&
             typeof selection.index === "number"
           ) {
             // Handle single zoom segment deletion
             projectActions.deleteZoomSegment(selection.index);
+            setEditorState("timeline", "selection", null);
           }

Additionally, consider guarding against active text inputs and preventing default at the top of the keydown handler (outside the changed range), e.g.:

createEventListener(window, "keydown", (e) => {
  const hasNoModifiers = !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey;

  const target = e.target as Element | null;
  const isTypingContext =
    !!target &&
    (target.closest("input, textarea, [contenteditable=''], [contenteditable='true']") !== null);

  if (isTypingContext) return;

  if ((e.code === "Backspace" || e.code === "Delete") && hasNoModifiers) {
    e.preventDefault();
    e.stopPropagation();
    // ... existing deletion logic
  }
});

116-118: Also clear selection after layout-segment deletion.

Resetting selection avoids dangling references and keeps UX consistent with zoom deletions.

-        projectActions.deleteLayoutSegment(selection.index);
+        projectActions.deleteLayoutSegment(selection.index);
+        setEditorState("timeline", "selection", null);

144-151: Don’t clear selection on mouseup when a layout segment was just dragged.

This block only checks the zoom drag state; if a layout drag occurred, selection will be cleared incorrectly.

Update the guard to consider both drag states:

-            if (zoomSegmentDragState.type === "idle") {
+            if (
+              zoomSegmentDragState.type === "idle" &&
+              layoutSegmentDragState.type === "idle"
+            ) {
               setEditorState("timeline", "selection", null);
             }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8b89c59 and 5a3cd26.

📒 Files selected for processing (7)
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2 hunks)
  • apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (1 hunks)
  • apps/desktop/src/routes/editor/Timeline/index.tsx (6 hunks)
  • crates/project/src/configuration.rs (1 hunks)
  • crates/rendering/src/composite_frame.rs (2 hunks)
  • crates/rendering/src/shaders/composite-video-frame.wgsl (2 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/ui-solid/src/auto-imports.d.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/project/src/configuration.rs
  • crates/rendering/src/shaders/composite-video-frame.wgsl
  • apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
crates/rendering/src/composite_frame.rs (2)

11-11: No inadvertent zeroed() calls for CompositeVideoFrameUniforms found

I ran the search and didn’t find any uses of CompositeVideoFrameUniforms::zeroed() or mem::zeroed() in the codebase. Keeping Pod + Zeroable alongside an explicit Default is safe—no unintended zero‐initialization occurs.

Approved.


28-30: Opacity doc added and WGSL alignment confirmed

  • Added one-line doc to clarify the range and semantics of opacity.
  • Verified that in crates/rendering/src/shaders/composite-video-frame.wgsl, the Uniforms struct declares opacity: f32 immediately after shadow_blur: f32 (lines 15–16), matching the Rust layout.
  • Optional: consider setting min_binding_size on the uniform’s #[repr(C)] layout to catch any future drift at runtime.

Applied diff:

-    pub opacity: f32,
+    /// Final alpha multiplier in [0.0, 1.0]; applied in the composite shader.
+    pub opacity: f32,
apps/desktop/src/routes/editor/Timeline/index.tsx (6)

14-14: LayoutTrack import and public type usage look correct.

Importing both LayoutTrack and its public LayoutSegmentDragState type is consistent with the new feature integration.


29-29: Good use of meta() from editor context for feature-gating.

Pulling meta here enables conditional rendering of the layout track later. Looks clean.


77-77: Non-reactive local drag state is fine here.

Using a let for layoutSegmentDragState mirrors the existing zoom approach and is adequate for gating event-driven logic without introducing reactivity.


123-125: Escape to clear selection: LGTM.

Simple and expected UX to deselect all.


254-261: Conditional LayoutTrack rendering and wiring look correct.

Gating on meta().hasCamera and forwarding onDragStateChanged + handleUpdatePlayhead matches the intended UX and keeps interactions consistent across tracks.


287-287: Marking dot style tweak: LGTM.

Using bg-current keeps the marker color aligned with the parent text color.

Copy link
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: 0

🧹 Nitpick comments (2)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2)

61-64: Good NaN hardening; consider centralizing min-dB and guarding gain (nit).

You fixed the sample-NaN source. As a minor improvement, DRY the -60 sentinel and also guard gain to avoid accidental NaN if it ever becomes non-finite.

-    const norm = (w: number) => {
-      const ww = Number.isFinite(w) ? w : -60;
-      return 1.0 - Math.max(ww + gain, -60) / -60;
-    };
+    const MIN_DB = -60;
+    const norm = (w: number) => {
+      const ww = Number.isFinite(w) ? w : MIN_DB;
+      const gg = Number.isFinite(gain) ? gain : 0;
+      return 1.0 - Math.max(ww + gg, MIN_DB) / MIN_DB;
+    };

If you prefer keeping MIN_DB outside this block, you can define it once near WaveformCanvas and reuse it:

const MIN_DB = -60;

358-373: Confirm UX intent: Click on single-segment clips now clears selection instead of opening config.

This changes sidebar behavior; in single-clip timelines, users can’t open the clip config panel via click anymore. If that’s the intended UX (because there’s “nothing to configure”), all good. Otherwise, we might want a different rule (e.g., only avoid opening when we’re in a default/fallback single segment).

Optionally make the “single segment” check explicit against real timeline data (not the computed fallback) for clarity:

-            const hasOnlyOneSegment = segments().length === 1;
+            const hasOnlyOneSegment =
+              (project.timeline?.segments?.length ?? 0) <= 1;

Suggested QA:

  • With exactly one real clip segment, click the clip: sidebar deselects and does not open clip config.
  • With multiple segments, click a clip: sidebar selects { type: "clip", index } and opens config.
  • Ensure clicking a clip also clears any existing layout/zoom selection as intended and that props.handleUpdatePlayhead(e) still advances the playhead correctly.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 01e550c and 8872dca.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2)
crates/media/src/feeds/audio.rs (1)
  • gain (157-159)
crates/editor/src/segments.rs (1)
  • segments (6-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (5)

74-77: LGTM: current sample fallback prevents NaN amplitudes.

Using a finite guard with -60 dB sentinel here is solid and avoids NaNs in amplitude.


82-85: LGTM: previous-sample fallback is correct.

This mirrors the current-sample guard and ensures cubic path control points remain finite at segment boundaries.


269-269: LGTM: class reordering only.

No behavioral impact; safe UI tidy-up.


307-307: LGTM: wrapper class reorder only.

Styling-only change, no functional differences.


560-577: LGTM: style-only changes.

Wrapper class reorder around the right marker has no functional impact.

Copy link
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

🧹 Nitpick comments (8)
crates/rendering/src/lib.rs (4)

345-350: ProjectUniforms extended with camera_only and layout: solid addition.

The added fields make layout-driven rendering explicit. Ensure any consumers cloning ProjectUniforms are aware of the increased payload (minor).


643-651: Avoid preparing the regular camera when it won’t render (perf).

layout.should_render_camera() can be true in CameraOnly segments depending on base values; the render pass later correctly gates with regular_camera_transition_opacity(). To avoid unnecessary .prepare() work, gate the camera Option on the same opacity heuristic used at render-time.

Apply this diff:

-        let camera = options
-            .camera_size
-            .filter(|_| !project.camera.hide && layout.should_render_camera())
+        let camera = options
+            .camera_size
+            // Only prepare when the regular camera overlay will actually be visible
+            .filter(|_| !project.camera.hide && layout.regular_camera_transition_opacity() > 0.01)
             .map(|camera_size| {
                 let output_size = [output_size.0 as f32, output_size.1 as f32];

Also applies to: 658-663, 707-716, 747-750


752-813: Camera-only pass looks correct and robust.

  • The output-aspect-driven crop prevents unwanted zoom when the user has “Square” camera shape – nice.
  • Opacity, blur, and zoom are tied to layout progress and feel consistent.

Minor optional: you could similarly avoid preparing this layer when camera_only_transition_opacity() is ~0 to shave some cycles during non-relevant frames.


1174-1179: Nit: default color component fallback.

Falling back to 1.0 yields white on invalid hex input. If that’s intentional, ignore. Otherwise consider 0.0 (black) or surfacing an error upstream to avoid silent whitening.

crates/rendering/src/layout.rs (4)

69-116: Remove unnecessary clone() calls on Copy types (LayoutMode).

Clippy is right here; LayoutMode is Copy, so the .clone() calls are redundant and add noise.

Apply this diff:

-                let prev_mode = cursor
-                    .prev_segment
-                    .map(|s| s.mode.clone())
+                let prev_mode = cursor
+                    .prev_segment
+                    .map(|s| s.mode)
                     .unwrap_or(LayoutMode::Default);
@@
-                    prev_mode,
-                    segment.mode.clone(),
+                    prev_mode,
+                    segment.mode,
                     ease_in_out(progress as f32) as f64,
                 )
             } else if cursor.time >= transition_end && cursor.time < segment.end {
                 if let Some(next_seg) = cursor.next_segment() {
                     let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION;
                     (
-                        segment.mode.clone(),
-                        next_seg.mode.clone(),
+                        segment.mode,
+                        next_seg.mode,
                         ease_in_out(progress as f32) as f64,
                     )
                 } else {
                     let progress = (cursor.time - transition_end) / LAYOUT_TRANSITION_DURATION;
                     (
-                        segment.mode.clone(),
+                        segment.mode,
                         LayoutMode::Default,
                         ease_in_out(progress as f32) as f64,
                     )
                 }
             } else {
-                (segment.mode.clone(), segment.mode.clone(), 1.0)
+                (segment.mode, segment.mode, 1.0)
             }
         } else if let Some(next_segment) = cursor.next_segment() {
             let transition_start = next_segment.start - LAYOUT_TRANSITION_DURATION;
             if cursor.time >= transition_start {
-                let prev_mode = cursor
-                    .prev_segment
-                    .map(|s| s.mode.clone())
+                let prev_mode = cursor
+                    .prev_segment
+                    .map(|s| s.mode)
                     .unwrap_or(LayoutMode::Default);
                 let progress = (cursor.time - transition_start) / LAYOUT_TRANSITION_DURATION;
                 (
-                    prev_mode,
-                    next_segment.mode.clone(),
+                    prev_mode,
+                    next_segment.mode,
                     ease_in_out(progress as f32) as f64,
                 )

125-135: Collapse nested else-if and remove redundant clones.

Simplify control flow per Clippy and drop .clone() on LayoutMode.

Apply this diff:

-        } else {
-            if let Some(prev_segment) = cursor.prev_segment {
-                if cursor.time < prev_segment.end + 0.05 {
-                    (prev_segment.mode.clone(), LayoutMode::Default, 1.0)
-                } else {
-                    (LayoutMode::Default, LayoutMode::Default, 1.0)
-                }
-            } else {
-                (LayoutMode::Default, LayoutMode::Default, 1.0)
-            }
-        };
+        } else if let Some(prev_segment) = cursor.prev_segment {
+            if cursor.time < prev_segment.end + 0.05 {
+                (prev_segment.mode, LayoutMode::Default, 1.0)
+            } else {
+                (LayoutMode::Default, LayoutMode::Default, 1.0)
+            }
+        } else {
+            (LayoutMode::Default, LayoutMode::Default, 1.0)
+        };

200-205: Drop clone() in layout_mode selection (Copy type).

Small cleanliness improvement.

Apply this diff:

-            layout_mode: if transition_progress > 0.5 {
-                next_mode.clone()
-            } else {
-                current_mode.clone()
-            },
+            layout_mode: if transition_progress > 0.5 { next_mode } else { current_mode },

45-49: Potential future-proofing: next_segment assumes sorted, non-overlapping segments.

If segments are ever unsorted, find(|s| s.start > time) could pick an arbitrary “next”. If unsorted segments are possible, consider storing the current index in the cursor and returning segments[idx + 1] when available, or sorting once upfront.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8872dca and 9aa5747.

📒 Files selected for processing (3)
  • crates/rendering/src/layout.rs (1 hunks)
  • crates/rendering/src/lib.rs (17 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui-solid/src/auto-imports.d.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
crates/rendering/src/layout.rs (2)
apps/desktop/src/utils/tauri.ts (1)
  • LayoutSegment (724-728)
crates/rendering/src/lib.rs (6)
  • new (63-133)
  • new (298-335)
  • new (516-827)
  • new (843-848)
  • new (895-905)
  • new (1045-1075)
crates/rendering/src/lib.rs (2)
crates/rendering/src/layout.rs (2)
  • new (14-43)
  • new (66-212)
crates/rendering/src/layers/camera.rs (1)
  • new (18-42)
🪛 GitHub Check: Clippy
crates/rendering/src/layout.rs

[warning] 113-113: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:113:21
|
113 | next_segment.mode.clone(),
| ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: next_segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 108-108: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:108:30
|
108 | .map(|s| s.mode.clone())
| ^^^^^^^^^^^^^^ help: try removing the clone call: s.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 125-135: this else { if .. } block can be collapsed
warning: this else { if .. } block can be collapsed
--> crates/rendering/src/layout.rs:125:16
|
125 | } else {
| _______^
126 | | if let Some(prev_segment) = cursor.prev_segment {
127 | | if cursor.time < prev_segment.end + 0.05 {
128 | | (prev_segment.mode.clone(), LayoutMode::Default, 1.0)
... |
135 | | };
| |
^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_else_if
= note: #[warn(clippy::collapsible_else_if)] on by default
help: collapse nested if block
|
125 ~ } else if let Some(prev_segment) = cursor.prev_segment {
126 + if cursor.time < prev_segment.end + 0.05 {
127 + (prev_segment.mode.clone(), LayoutMode::Default, 1.0)
128 + } else {
129 + (LayoutMode::Default, LayoutMode::Default, 1.0)
130 + }
131 + } else {
132 + (LayoutMode::Default, LayoutMode::Default, 1.0)
133 ~ };
|


[warning] 101-101: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:101:40
|
101 | (segment.mode.clone(), segment.mode.clone(), 1.0)
| ^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 101-101: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:101:18
|
101 | (segment.mode.clone(), segment.mode.clone(), 1.0)
| ^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 95-95: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:95:25
|
95 | segment.mode.clone(),
| ^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 89-89: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:89:25
|
89 | next_seg.mode.clone(),
| ^^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: next_seg.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 88-88: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:88:25
|
88 | segment.mode.clone(),
| ^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 81-81: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:81:21
|
81 | segment.mode.clone(),
| ^^^^^^^^^^^^^^^^^^^^ help: try removing the clone call: segment.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy


[warning] 76-76: using clone on type LayoutMode which implements the Copy trait
warning: using clone on type LayoutMode which implements the Copy trait
--> crates/rendering/src/layout.rs:76:30
|
76 | .map(|s| s.mode.clone())
| ^^^^^^^^^^^^^^ help: try removing the clone call: s.mode
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy
= note: #[warn(clippy::clone_on_copy)] on by default

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (5)
crates/rendering/src/lib.rs (5)

29-29: Good modularization: new layout module is cleanly integrated.

Bringing layout in as a private module and importing its API locally keeps the public surface minimal. No concerns here.

Also applies to: 39-39


568-575: Per-frame InterpolatedLayout construction: LGTM.

Using the per-frame time and timeline layout_segments to drive layout interpolation is appropriate and matches the new feature’s intent.


620-621: Display uniforms driven by layout are correct.

  • Incorporating layout.screen_blur into motion_blur_amount and setting opacity from layout.screen_opacity aligns the shader behavior with layout transitions.

Also applies to: 638-639


907-966: Preparation path extended to camera_only: LGTM.

The pattern mirroring the regular camera path using an Option triple keeps the code cohesive.


970-976: Verified: No external RendererLayers::render calls remain
The only invocation is in crates/rendering/src/lib.rs at line 1151, which already passes &uniforms. No other call sites in the workspace rely on the old signature.

Comment on lines +214 to +220
fn get_layout_values(mode: &LayoutMode) -> (f64, f64, f64) {
match mode {
LayoutMode::Default => (1.0, 1.0, 1.0),
LayoutMode::CameraOnly => (1.0, 1.0, 1.0),
LayoutMode::HideCamera => (0.0, 1.0, 1.0),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix CameraOnly base values: screen should be hidden.

Currently get_layout_values(LayoutMode::CameraOnly) returns a screen_opacity of 1.0, causing the screen to render behind the full-frame camera and breaking gating decisions. Set it to 0.0 so the screen is actually hidden in CameraOnly.

Apply this diff:

     fn get_layout_values(mode: &LayoutMode) -> (f64, f64, f64) {
         match mode {
             LayoutMode::Default => (1.0, 1.0, 1.0),
-            LayoutMode::CameraOnly => (1.0, 1.0, 1.0),
+            // Camera visible, screen hidden, neutral scale
+            LayoutMode::CameraOnly => (1.0, 0.0, 1.0),
             LayoutMode::HideCamera => (0.0, 1.0, 1.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
fn get_layout_values(mode: &LayoutMode) -> (f64, f64, f64) {
match mode {
LayoutMode::Default => (1.0, 1.0, 1.0),
LayoutMode::CameraOnly => (1.0, 1.0, 1.0),
LayoutMode::HideCamera => (0.0, 1.0, 1.0),
}
}
fn get_layout_values(mode: &LayoutMode) -> (f64, f64, f64) {
match mode {
LayoutMode::Default => (1.0, 1.0, 1.0),
// Camera visible, screen hidden, neutral scale
LayoutMode::CameraOnly => (1.0, 0.0, 1.0),
LayoutMode::HideCamera => (0.0, 1.0, 1.0),
}
}
🤖 Prompt for AI Agents
In crates/rendering/src/layout.rs around lines 214 to 220, get_layout_values
currently returns (1.0, 1.0, 1.0) for LayoutMode::CameraOnly which leaves the
screen visible; change the returned tuple for CameraOnly to (1.0, 0.0, 1.0) so
the screen_opacity is 0.0 and the screen is actually hidden in CameraOnly mode.

Comment on lines +230 to +232
pub fn should_render_screen(&self) -> bool {
true
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Implement effective gating: don’t always render the screen.

This always returns true, so the renderer never skips the display/cursor passes even when screen_opacity is ~0. Use a threshold on the computed opacity.

Apply this diff:

-    pub fn should_render_screen(&self) -> bool {
-        true
-    }
+    pub fn should_render_screen(&self) -> bool {
+        self.screen_opacity > 0.01
+    }
📝 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
pub fn should_render_screen(&self) -> bool {
true
}
pub fn should_render_screen(&self) -> bool {
self.screen_opacity > 0.01
}
🤖 Prompt for AI Agents
In crates/rendering/src/layout.rs around lines 230 to 232, the method currently
unconditionally returns true which prevents skipping rendering when screen
opacity is effectively zero; replace the hardcoded true with a calculation of
the effective screen opacity (derive it from the existing fields used to
determine visual opacity, e.g., screen_opacity possibly multiplied by any
parent/background opacity), clamp the result to 0.0..1.0, guard against NaN, and
return opacity > a small threshold (e.g., 0.01) so the screen/cursor passes are
skipped when opacity is negligible.

Comment on lines +1012 to 1021
if uniforms.layout.should_render_screen() {
let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load);
self.display.render(&mut pass);
}

{
if uniforms.layout.should_render_screen() {
let mut pass = render_pass!(session.current_texture_view(), wgpu::LoadOp::Load);
self.cursor.render(&mut pass);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Screen and cursor gating depends on should_render_screen(), which currently always returns true.

As implemented, the screen and cursor always render, even during CameraOnly. That defeats the gating intent and can cause unnecessary overdraw and cursor visibility during camera-only fades. See my fix suggestion in layout.rs for should_render_screen() and get_layout_values().

🤖 Prompt for AI Agents
In crates/rendering/src/lib.rs around lines 1012 to 1021, the screen and cursor
rendering are gated by uniforms.layout.should_render_screen() but that function
currently always returns true; update this code to rely on the corrected
should_render_screen() implementation from layout.rs (and any related
get_layout_values() changes) so that in CameraOnly mode both the screen and
cursor blocks are skipped; ensure you import/use the updated layout logic and
leave the two render calls gated by the revised predicate so they no longer
render during CameraOnly.

Copy link
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

Caution

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

⚠️ Outside diff range comments (3)
apps/desktop/src/routes/editor/Timeline/index.tsx (1)

134-145: Don’t clear selection after layout drags

Selection clear on mouseup only checks zoom state. During a layout drag, zoom is idle, so selection may be inadvertently cleared post-drag. Check both drag states before clearing.

-            if (zoomSegmentDragState.type === "idle") {
+            if (
+              zoomSegmentDragState.type === "idle" &&
+              layoutSegmentDragState.type === "idle"
+            ) {
               setEditorState("timeline", "selection", null);
             }
apps/desktop/src/routes/editor/context.ts (2)

107-117: Bug: Misplaced negation breaks the guard in deleteClipSegment

The condition !segment.recordingSegment === undefined is always false due to operator precedence, so the early-return doesn’t trigger when recordingSegment is undefined. This risks deleting the only clip representing a recording segment.

Fix the check:

-          !segment.recordingSegment === undefined ||
+          segment.recordingSegment === undefined ||

148-160: Add bounds check for layout deletion

Splicing with an invalid index (e.g., -1) will remove the wrong element. Validate the index first.

       setProject(
         "timeline",
         "layoutSegments",
         produce((s) => {
           if (!s) return;
-          s.splice(segmentIndex, 1);
+          if (
+            Number.isInteger(segmentIndex) &&
+            segmentIndex >= 0 &&
+            segmentIndex < s.length
+          ) {
+            s.splice(segmentIndex, 1);
+          }
         }),
       );
🧹 Nitpick comments (2)
apps/desktop/src/routes/editor/Timeline/index.tsx (2)

80-92: Minor: handleUpdatePlayhead should guard missing bounds

timelineBounds.left can be undefined early in mount, producing NaN calculations. Early-return when left is undefined.

   const { left } = timelineBounds;
+  if (left == null) return;

102-108: Zoom deletion needs a fallback for legacy single-index selections

It looks like Timeline/ZoomTrack.tsx still emits the legacy shape { type: "zoom"; index: number } in its “regular single selection” branch. Since the delete handler in apps/desktop/src/routes/editor/Timeline/index.tsx unconditionally does:

if (selection.type === "zoom") {
  projectActions.deleteZoomSegments(selection.indices);
}

a single-index selection will call deleteZoomSegments(undefined) and silently no-op.

You have two options:

  • Fully migrate all callers (e.g. in ZoomTrack.tsx) to emit only { type: "zoom"; indices: number[] }.

  • Or add a lightweight fallback in the delete branch:

    --- a/apps/desktop/src/routes/editor/Timeline/index.tsx
    +++ b/apps/desktop/src/routes/editor/Timeline/index.tsx
     @@ -102,7 +102,13 @@ function onKeyDown(e: KeyboardEvent) {
           } else if (selection.type === "layout") {
             projectActions.deleteLayoutSegment(selection.index);
           }
    -      } else if (selection.type === "zoom") {
    -        projectActions.deleteZoomSegments(selection.indices);
    +      } else if (selection.type === "zoom") {
    +        const indices = Array.isArray(selection.indices)
    +          ? selection.indices
    +          : typeof (selection as any).index === "number"
    +          ? [(selection as any).index]
    +          : [];
    +        projectActions.deleteZoomSegments(indices);
           }
         }
       }

This ensures the delete behavior works for both new and legacy selections.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4776699 and 4a9fdd8.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/editor/Timeline/index.tsx (6 hunks)
  • apps/desktop/src/routes/editor/context.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/Timeline/index.tsx (3)
apps/desktop/src/routes/editor/Timeline/LayoutTrack.tsx (2)
  • LayoutSegmentDragState (27-30)
  • LayoutTrack (32-493)
apps/desktop/src/routes/editor/context.ts (1)
  • meta (307-309)
crates/editor/src/editor_instance.rs (1)
  • meta (106-108)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (6)
apps/desktop/src/routes/editor/Timeline/index.tsx (4)

14-14: LayoutTrack import and typing look good

Importing LayoutTrack alongside its drag-state type is consistent with the new track’s contract. No issues spotted.


113-116: Escape-to-deselect is a nice UX touch

Clear and consistent with other editors.


29-29: Gating LayoutTrack on meta().hasCamera is correct

Conditionally rendering the layout track only when camera metadata is present is appropriate; drag-state wiring and time sync props are consistent with ZoomTrack.

Also applies to: 244-251


277-277: Styling change for timeline markings is fine

Class tweak is harmless and doesn’t affect behavior. LGTM.

apps/desktop/src/routes/editor/context.ts (2)

125-126: LGTM on splice usage

Switching to in-place splice for clip deletion is correct given surrounding batching and selection reset.


255-260: Selection union updates look consistent with UI changes

Including layout selection and switching zoom to indices[] aligns with the new Timeline behaviors.

}

let zoomSegmentDragState = { type: "idle" } as ZoomSegmentDragState;
let layoutSegmentDragState = { type: "idle" } as LayoutSegmentDragState;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard should also treat "movePending" as dragging to avoid playhead jitter

Currently the playhead is only paused when drag state is "moving". While the drag is "movePending" (mouse down before movement), playhead updates can still occur, causing subtle jitter and selection oddities. Prefer updating playbackTime only when both zoom and layout drag states are idle.

Apply this diff:

-    if (
-      zoomSegmentDragState.type !== "moving" &&
-      layoutSegmentDragState.type !== "moving"
-    ) {
+    if (
+      zoomSegmentDragState.type === "idle" &&
+      layoutSegmentDragState.type === "idle"
+    ) {

Also applies to: 81-84

🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/Timeline/index.tsx around lines 77 and also
affecting lines 81-84, the drag-state guard only treats "moving" as an active
drag so when state is "movePending" playhead updates still occur causing jitter;
change the checks so both "movePending" and "moving" are considered active
dragging (i.e., treat any state !== "idle" as dragging or explicitly test for
both "movePending" and "moving") and update the conditional that pauses/blocks
playbackTime updates (and the zoom + layout combined idle check) to require both
zoomState === "idle" and layoutSegmentDragState === "idle" before allowing
playbackTime to update.

Copy link
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 (2)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

1638-1642: Type mismatch in KSelect.Value generic (StereoMode vs CameraShape)

The “Shape” selector is typed as CameraShape everywhere else, but the Value component uses StereoMode. This will cause a TS error.

- <KSelect.Value<{
-   name: string;
-   value: StereoMode;
- }> class="flex-1 text-sm text-left truncate text-[--gray-500] font-normal">
+ <KSelect.Value<{
+   name: string;
+   value: CameraShape;
+ }> class="flex-1 text-sm text-left truncate text-[--gray-500] font-normal">
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (1)

94-121: findIndex(-1) not handled → incorrect “next segment” logic near track end

When no “next” segment exists, findIndex returns -1, but the code treats any number as truthy and then accesses index -1 (last element). This can clamp hoveredTime incorrectly and block adding segments near the end.

Apply this diff to guard against -1 and compute prevSegmentIndex safely:

-const nextSegmentIndex = project.timeline?.zoomSegments?.findIndex(
-  (s) => time < s.start,
-);
-
-if (nextSegmentIndex !== undefined) {
-  const prevSegmentIndex = nextSegmentIndex - 1;
+const nextSegmentIndex =
+  project.timeline?.zoomSegments?.findIndex((s) => time < s.start);
+
+if (typeof nextSegmentIndex === "number" && nextSegmentIndex >= 0) {
+  const prevSegmentIndex =
+    nextSegmentIndex > 0 ? nextSegmentIndex - 1 : undefined;
   if (prevSegmentIndex === undefined) return;
 
   const nextSegment =
     project.timeline?.zoomSegments?.[nextSegmentIndex];
 
   if (prevSegmentIndex !== undefined && nextSegment) {
     const prevSegment =
       project.timeline?.zoomSegments?.[prevSegmentIndex];
 
     if (prevSegment) {
       const availableTime = nextSegment?.start - prevSegment?.end;
 
       if (availableTime < 1) return;
     }
   }
 
   if (nextSegment && nextSegment.start - time < 1) {
     time = nextSegment.start - 1;
   }
 }
♻️ Duplicate comments (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

46-52: Missing IconLucideLayout import (breaks build where used in LayoutSegmentConfig)

<IconLucideLayout /> is referenced later but not imported here. This will fail at build/runtime.

Apply this diff to add the missing import next to the other lucide icon imports:

 import {
   type BackgroundSource,
   type CameraShape,
   commands,
   type LayoutSegment,
   type StereoMode,
   type TimelineSegment,
   type ZoomSegment,
 } from "~/utils/tauri";
+import IconLucideLayout from "~icons/lucide/layout";
 import IconLucideMonitor from "~icons/lucide/monitor";
🧹 Nitpick comments (7)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (3)

1585-1592: Typo in class name: use shrink-0 (current: shink-0)

This breaks the intended flex behavior (no shrinking) of the position control dots.

- "cursor-pointer size-6 shink-0 rounded-[0.375rem] bg-gray-5 absolute flex justify-center items-center ui-checked:bg-blue-9 focus-visible:outline peer-focus-visible:outline outline-2 outline-blue-9 outline-offset-2 outline-blue-9 transition-colors duration-100",
+ "cursor-pointer size-6 shrink-0 rounded-[0.375rem] bg-gray-5 absolute flex justify-center items-center ui-checked:bg-blue-9 focus-visible:outline peer-focus-visible:outline outline-2 outline-blue-9 outline-offset-2 outline-blue-9 transition-colors duration-100",

1959-1966: Bound the retry loop on video load errors to avoid infinite reloads

If the video file is missing or inaccessible, this will continually retry. Cap retries.

- // Add error handling
- video.onerror = (e) => {
-   console.error("Failed to load video for zoom preview:", e);
-   // Try to reload after a short delay
-   setTimeout(() => {
-     video.load();
-   }, 100);
- };
+ // Add error handling with bounded retry to avoid infinite reload loops
+ let reloadAttempts = 0;
+ video.onerror = (e) => {
+   console.error("Failed to load video for zoom preview:", e);
+   if (reloadAttempts < 2) {
+     reloadAttempts += 1;
+     setTimeout(() => {
+       video.load();
+     }, 150);
+   }
+ };

2118-2221: LayoutSegmentConfig UX and state updates look good

  • Clear “Done/Delete” affordances consistent with other segment configs.
  • Mode tabs map cleanly to TimelineConfiguration: "default", "cameraOnly", "hideCamera".
  • The descriptive helper below the tabs is a nice touch.

Small type-safety tweak (optional): instead of casting the mode on update, you can lean on the declared type to avoid string drift.

- v as "default" | "cameraOnly" | "hideCamera",
+ v as LayoutSegment["mode"],

Also: this component depends on IconLucideLayout; ensure the import is added (see earlier comment).

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (4)

43-54: Good fix for stuck hover; also clear hoveredTime when segments are removed

The effect correctly resets the hover gate when segments are deleted. To fully clear transient UI state, also reset hoveredTime so the ghost “+” preview doesn’t linger if it was showing.

Apply this diff:

 createEffect(() => {
   const segments = project.timeline?.zoomSegments;
   if (!segments || segments.length === 0) {
     setHoveringSegment(false);
+    setHoveredTime(undefined);
   }
 });

291-306: isSelected supports both single and multi; consider stabilizing selection across reorders

The dual support for index and indices is good. However, storing selection by array positions will drift when segments are sorted/moved, causing highlights to jump. Consider tracking selection by a stable identifier (e.g., segment id) or by object identity and updating indices after sort.

I can sketch a minimal migration plan to add ids to zoomSegments and adapt selection/update flows if helpful.


88-93: Prefer currentTarget for bounding box math

Using e.target can yield a child element’s rect and miscompute time offsets. currentTarget is the TrackRoot element you attached the handler to.

Apply this diff:

-const bounds = e.target.getBoundingClientRect()!;
+const bounds = e.currentTarget.getBoundingClientRect()!;

413-441: IconLucideSearch is auto-imported; no manual import needed. Add aria-hidden for accessibility.

• The project’s auto-import setup (see packages/ui-solid/src/auto-imports.d.ts) already provides IconLucideSearch, so you don’t need to add an import statement.
• To avoid unnecessary screen-reader announcements for decorative icons, add aria-hidden="true" to each usage in apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx:

--- ZoomTrack.tsx
@@ (width < 40) block
-   <IconLucideSearch class="size-3.5 text-gray-1 dark:text-gray-12" />
+   <IconLucideSearch aria-hidden="true" class="size-3.5 text-gray-1 dark:text-gray-12" />

@@ (width < 100) block
-   <IconLucideSearch class="size-3" />
+   <IconLucideSearch aria-hidden="true" class="size-3" />

@@ (else) block
-   <IconLucideSearch class="size-3.5" />
+   <IconLucideSearch aria-hidden="true" class="size-3.5" />
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4a9fdd8 and b4054b5.

📒 Files selected for processing (3)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (14 hunks)
  • apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (6 hunks)
  • apps/desktop/src/routes/editor/context.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/routes/editor/context.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)
apps/desktop/src/utils/tauri.ts (2)
  • ZoomSegment (780-785)
  • LayoutSegment (724-728)
apps/desktop/src/routes/editor/ui.tsx (2)
  • EditorButton (352-412)
  • Field (25-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (actions)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

593-641: Zoom segment selection panel looks solid

Good single-selection gating, batch delete handling, and contextual “Done” workflow. This matches the UX of similar panels and keeps the state updates scoped.


643-652: Layout segment selection gating and handoff to LayoutSegmentConfig is clean

The guard ensures only valid, single layout selections render the config UI. The handoff of both the segment and index keeps the editor actions straightforward.

Also applies to: 655-659

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

9-15: SolidJS import for createEffect looks correct

Importing createEffect is appropriate for the new hover-reset effect below.


172-177: Empty-state UX improvement LGTM

The two-line, gray-themed empty state with pointer-events-none is a solid improvement; it avoids intercepting clicks when adding the first segment.

Copy link
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

♻️ Duplicate comments (1)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (1)

213-250: Ctrl/Cmd multi-select toggle now normalizes legacy selection — fixes the crash path

You correctly normalize single-index selections into an indices array before toggling. Also good call keeping handleUpdatePlayhead(e) invocation afterward.

🧹 Nitpick comments (4)
apps/desktop/src/styles/theme.css (2)

151-155: Use logical property with fallback to avoid undefined var issues; consider scroll-gutter instead of margins

  • --margin-top-scroll isn’t defined in this file. Without a fallback, the declaration is ignored when the variable is unset. Also, margin-top may margin-collapse depending on context. Prefer margin-block-start with a 0 fallback.
  • If the intent is to reserve space for the scrollbar/edges, scrollbar-gutter: stable; (or stable both-edges) is a more robust, cross-OS approach than relying on margins.

Apply within this block:

 .custom-scroll {
   margin-right: 3px; /* Add margin to create space between scrollbar and window edge */
   overflow-y: scroll;
-  margin-top: var(--margin-top-scroll);
+  margin-block-start: var(--margin-top-scroll, 0);
+  /* Optional: reserve layout space for scrollbars to avoid layout shift */
+  /* scrollbar-gutter: stable; */
 }

Additionally, define a safe default (outside this hunk):

:root,
:root.dark {
  --margin-top-scroll: 0;
}

If the goal is to offset content inside the scroll container (not the element itself), consider padding-block-start instead of margin.


157-166: Duplicate WebKit scrollbar rules with conflicting widths (7px vs 4px)

You define .custom-scroll::-webkit-scrollbar twice with different width values. The latter (4px) wins in the cascade, making the earlier 7px config dead code and potentially confusing.

Consider consolidating to a single definition to avoid ambiguity. For example, remove the earlier block:

-.custom-scroll::-webkit-scrollbar {
-  width: 7px;
-  background-color: transparent;
-}

And keep the later, consistent 4px version. If the intent was contextual (e.g., theme/OS), make that explicit with a parent selector or media query.

Also applies to: 174-188

apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

142-149: Insertion index can break sort order when multiple segments start after time

Scanning from the end picks the last segment with start > time, which can insert after earlier segments that also satisfy start > time, leaving the array unsorted (e.g., existing starts [10,20], inserting at time=5 yields [10,5,20]). Prefer a forward findIndex and fallback to append.

-                  let index = zoomSegments.length;
-
-                  for (let i = zoomSegments.length - 1; i >= 0; i--) {
-                    if (zoomSegments[i].start > time) {
-                      index = i;
-                      break;
-                    }
-                  }
+                  const found = zoomSegments.findIndex((s) => s.start > time);
+                  const index = found === -1 ? zoomSegments.length : found;
 
                   zoomSegments.splice(index, 0, {
                     start: time,
                     end: time + 1,
                     amount: 1.5,
                     mode: {
                       manual: {
                         x: 0.5,
                         y: 0.5,
                       },
                     },
                   });

Also applies to: 151-163


88-93: Use currentTarget for bounds to avoid child-element targeting quirks

If the event originates from a child, e.target may not be the TrackRoot, skewing the computed position. currentTarget is the element with the handler.

-        const bounds = e.target.getBoundingClientRect()!;
+        const bounds = (e.currentTarget as HTMLElement).getBoundingClientRect();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b4054b5 and 02caf83.

📒 Files selected for processing (4)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (16 hunks)
  • apps/desktop/src/routes/editor/ShadowSettings.tsx (1 hunks)
  • apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (6 hunks)
  • apps/desktop/src/styles/theme.css (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/src/routes/editor/ShadowSettings.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (3)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (3)

43-53: Clearing hover-state leak after deletions — good preventive effect

The createEffect that resets hoveringSegment when zoomSegments becomes empty addresses the stuck-hover issue cleanly. Nice.


172-176: Empty-state UX improvements look solid

The two-line, subtle empty state with pointer-events-none reads well and lets clicks pass through to TrackRoot for segment creation.


296-311: Selection predicate supports both index and indices

The isSelected logic handles both legacy and new selection shapes without branching elsewhere. Looks good.

Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (2)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (1)

143-152: Fix insertion index: descending scan inserts incorrectly for times before the first segment

The descending loop chooses the last segment that satisfies start > time. For time earlier than all segments, this inserts near the end, leaving the list unsorted until some later sort. Use a forward scan with findIndex and fallback to append.

Apply this diff:

-									let index = zoomSegments.length;
-
-									for (let i = zoomSegments.length - 1; i >= 0; i--) {
-										if (zoomSegments[i].start > time) {
-											index = i;
-											break;
-										}
-									}
+									let index = zoomSegments.findIndex((s) => s.start > time);
+									if (index === -1) index = zoomSegments.length;
 
 									zoomSegments.splice(index, 0, {
 										start: time,
 										end: time + 1,
 										amount: 1.5,
 										mode: {
 											manual: {
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

1368-1401: BUG: projectHistory.pause() is never resumed on gradient angle drag

You pause history but do not call the resumeHistory() you captured. This can leave history paused beyond the drag, breaking undo/redo aggregation.

 createRoot((dispose) =>
   createEventListenerMap(window, {
-    mouseup: () => dispose(),
+    mouseup: () => {
+      resumeHistory();
+      dispose();
+    },
     mousemove: (moveEvent) => {
       const rawNewAngle =
         Math.round(start + (downEvent.clientY - moveEvent.clientY)) % max;
       const newAngle = moveEvent.shiftKey
         ? rawNewAngle
         : Math.round(rawNewAngle / 45) * 45;
       ...
     },
   }),
 );
♻️ Duplicate comments (4)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

214-251: Multi-select toggle normalization is solid; resolves the legacy single-index crash

This addresses the earlier crash by normalizing to indices[] before toggling and preserves single-select behavior when modifiers aren’t held. Good fix.


419-447: Build break: IconLucideSearch is used but not imported

JSX references IconLucideSearch, but there’s no import. This will fail at compile/runtime.

Add an import for your icon library (adjust path if your project uses a wrapper):

 import { cx } from "cva";
+import { Search as IconLucideSearch } from "lucide-solid";
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

1807-1810: Icon usage requires import (covered above)

<IconLucideSearch /> used here relies on the missing import noted in the import block comment. Once added, this section is fine.

After adding the import, ensure the build passes.


46-52: Missing icon imports: IconLucideLayout (duplicate) and IconLucideSearch

<IconLucideLayout /> and <IconLucideSearch /> are used but not imported in this file. This will cause a compile/runtime error.

Apply this diff to add the missing imports alongside the other lucide imports:

 import {
   type BackgroundSource,
   type CameraShape,
   commands,
   type LayoutSegment,
   type StereoMode,
   type TimelineSegment,
   type ZoomSegment,
 } from "~/utils/tauri";
+import IconLucideLayout from "~icons/lucide/layout";
+import IconLucideSearch from "~icons/lucide/search";
 import IconLucideMonitor from "~icons/lucide/monitor";
 import IconLucideSparkles from "~icons/lucide/sparkles";
🧹 Nitpick comments (7)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (2)

43-55: Hover state reset only triggers when all segments are deleted; verify partial-delete behavior

The effect fixes the stale-hover issue when the last segment is removed. However, if the hovered segment is deleted while other segments remain, hoveringSegment can still get stuck true (since onMouseLeave won’t fire). That keeps onMouseMove short-circuited and blocks new segment creation until a full reset occurs.

Consider also clearing hoveringSegment when the hovered element disappears mid-hover (e.g., detect if the mouse is no longer over any segment in onMouseMove before early-returning, or track hovered segment identity). If you want, I can sketch a lightweight approach that avoids DOM queries by tagging SegmentRoot with a data attribute and checking e.target.closest().


297-312: Harden selection check: guard against -1 from findIndex

segmentIndex is always a number; -1 indicates “not found”. Use a >= 0 check before includes to avoid accidental lookups with -1.

Apply this diff:

-						if (
-							"indices" in selection &&
-							Array.isArray(selection.indices) &&
-							segmentIndex !== undefined
-						) {
+						if (
+							"indices" in selection &&
+							Array.isArray(selection.indices) &&
+							segmentIndex !== undefined &&
+							segmentIndex >= 0
+						) {
 							return selection.indices.includes(segmentIndex);
 						} else if (
 							"index" in selection &&
 							typeof selection.index === "number"
 						) {
 							return segmentIndex === selection.index;
 						}
apps/desktop/src/routes/editor/ConfigSidebar.tsx (5)

303-306: Inline CSS var could be centralized (nit)

The --margin-top-scroll style is duplicated later; consider hoisting into a class to avoid repetition.

- style={{
-   "--margin-top-scroll": "5px",
- }}
+ class={cx("mt-scroll-5px")}

Outside this hunk, define .mt-scroll-5px in your styles to set the CSS var once.


971-1004: Avoid unnecessary type assertion when deriving selected wallpaper (nit)

You’re already in the wallpaper branch; the extra cast is noisy. A null-safe access is sufficient.

- const selectedWallpaper = wallpapers()?.find((w) =>
-   (project.background.source as { path?: string }).path?.includes(w.id),
- );
+ const selectedWallpaper = wallpapers()?.find((w) =>
+   project.background.source.path?.includes(w.id),
+ );

1898-1907: Video reload and source path setup is sound

Using convertFileSrc and calling video.load() covers stale resource issues. Consider setting preload = "auto" for more eager readiness if needed.

 video.src = path;
+video.preload = "auto";
 // Force reload if video fails to load
 video.load();

1970-1977: Retry loop on video.onerror needs a cap/backoff

As written, persistent failures will retry every 100ms indefinitely. Add a max retry count and/or exponential backoff.

- video.onerror = (e) => {
-   console.error("Failed to load video for zoom preview:", e);
-   // Try to reload after a short delay
-   setTimeout(() => {
-     video.load();
-   }, 100);
- };
+ {
+   let retryCount = 0;
+   const maxRetries = 5;
+   video.onerror = (e) => {
+     console.error("Failed to load video for zoom preview:", e);
+     if (retryCount < maxRetries) {
+       const delay = 100 * 2 ** retryCount++;
+       setTimeout(() => video.load(), delay);
+     }
+   };
+ }

2129-2233: Layout segment config: small nits and missing import

  • Missing <IconLucideLayout /> import (covered in import block comment).
  • Prefer nullish coalescing for default mode to avoid unintended fallbacks.
  • You duplicate the indicator “left” computation twice; centralize to a helper or computed value to avoid inconsistencies.
- value={props.segment.mode || "default"}
+ value={props.segment.mode ?? "default"}

Optionally, extract the left position:

// outside JSX, inside component
const indicatorLeft = () =>
  props.segment.mode === "cameraOnly" ? "50%" :
  props.segment.mode === "hideCamera" ? "83.33%" : "16.67%";

Then in both style blocks:

- style={{ left: props.segment.mode === "cameraOnly" ? "50%" : props.segment.mode === "hideCamera" ? "83.33%" : "16.67%" }}
+ style={{ left: indicatorLeft() }}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 02caf83 and 05fe322.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (16 hunks)
  • apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (7 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)
apps/desktop/src/utils/tauri.ts (2)
  • ZoomSegment (780-785)
  • LayoutSegment (724-728)
apps/desktop/src/routes/editor/ui.tsx (2)
  • EditorButton (352-412)
  • Field (25-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (16)
apps/desktop/src/routes/editor/Timeline/ZoomTrack.tsx (3)

9-9: LGTM: createEffect import is appropriate

The import aligns with the new effect usage below.


89-94: LGTM: Use currentTarget for bounds

Switching to e.currentTarget.getBoundingClientRect() ensures calculations are relative to the track, not a child element. Correct change.


173-178: LGTM: Empty-state UX improvements

The empty/fallback state looks good. The pointer-events-none ensures track-level interactions still work (click-to-add) through the overlay.

apps/desktop/src/routes/editor/ConfigSidebar.tsx (13)

33-33: Switch to createStore looks good

State management for selectedTab via createStore is appropriate and consistent with the rest of the file’s Solid store usage.


46-50: Type import for LayoutSegment is correct

Good to see the explicit type wired from ~/utils/tauri. This aligns the layout segment editor with the shared timeline types.


217-225: Broader context from useEditorContext is wired correctly

Pulling setEditorState, projectActions, and others into scope is required for the new selection overlays and segment actions. Looks good.


579-649: Zoom selection overlay: solid gating and actions

  • Correctly guards for selection type and filters undefined segments.
  • Provides “Done” and bulk “Delete” with appropriate indices mapping.

This block is cohesive and aligns with existing Clip selection UX.


651-660: Layout selection gating is correct

The selection guard and segment lookup for layout segments mirror the zoom flow and should avoid null access issues.


663-666: Layout segment config routing looks right

Passing segment and segmentIndex from the gated selection is consistent with other segment configs.


1593-1601: Camera position radio control – focus/selection styling and handlers look good

The improved focus-visible outlines and absolute positioning logic are consistent and accessible.


1873-1895: Relative time calc is correct

Computes per-clip seek offset robustly and guards missing segment. Clean addition.


1910-1924: Defensive seek only after readiness is solid

The canplay fallback avoids invalid currentTime sets. Good guard against race conditions.


1933-1936: Render trigger is gated on loaded()

Subscribes to the right dependencies and avoids redundant draws. LGTM.


1941-1960: Canvas render pipeline is correct

Readiness checks, clearing, and draw parameters are sensible; disabling smoothing suits pixel-perfect previews.


2016-2047: Manual zoom drag updates are correct

Event scoping via createRoot, bounds math, and constrained [0,1] normalization are clean. The path-based setProject call to update mode.manual is correct within the manual tab.


2050-2068: Loading overlay UX is a nice touch

Data-attribute driven fade-in keeps it smooth; good conditional rendering while the video decodes.

Copy link
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)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

2067-2080: Fix TDZ crash: calling croppedPosition/croppedSize before they’re defined

createEffect(on(() => { croppedPosition(); croppedSize(); }, ...)) executes immediately; at this point croppedPosition/croppedSize aren’t initialized yet (const), causing a runtime ReferenceError.

Move the effect below the declarations of croppedPosition and croppedSize.

Remove the early effect:

-                createEffect(
-                  on(
-                    () => {
-                      croppedPosition();
-                      croppedSize();
-                    },
-                    () => {
-                      if (loaded()) {
-                        render();
-                      }
-                    },
-                  ),
-                );

Then add this effect after croppedPosition and croppedSize are declared (just below line 2141):

// Re-render when crop changes
createEffect(
  on(
    () => [croppedPosition().x, croppedPosition().y, croppedSize().x, croppedSize().y],
    () => {
      if (loaded()) render();
    },
  ),
);
♻️ Duplicate comments (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

47-53: Missing IconLucideLayout import will break build

<IconLucideLayout /> is used in LayoutSegmentConfig but not imported.

Apply this diff to add the missing import next to the other lucide icons:

 import {
   type BackgroundSource,
   type CameraShape,
   commands,
   type LayoutSegment,
   type StereoMode,
   type TimelineSegment,
   type ZoomSegment,
 } from "~/utils/tauri";
+import IconLucideLayout from "~icons/lucide/layout";
 import IconLucideMonitor from "~icons/lucide/monitor";
🧹 Nitpick comments (2)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

1019-1021: Avoid substring collisions when resolving selected wallpaper

Using includes(w.id) can match the wrong wallpaper id (e.g., "ventura" vs "ventura-dark"). Prefer an exact suffix match.

Apply this diff:

-                                                      ).path?.includes(w.id),
+                                                      ).path?.endsWith(`${w.id}.jpg`),

1889-1893: Release video resources on unmount to avoid leaks

Both previews attach event handlers and keep a video element alive without cleanup. Add onCleanup to pause and clear the source when the component unmounts.

Add onCleanup import:

// add to existing solid-js imports
import { ..., onCleanup } from "solid-js";

Then in both components (ZoomSegmentPreview and ZoomSegmentConfig) after setting up video and its handlers:

onCleanup(() => {
  try { video.pause(); } catch {}
  video.src = "";
  // Force release of resources
  video.load();
});

Also applies to: 2112-2118

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 05fe322 and f37d896.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (17 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)
apps/desktop/src/utils/tauri.ts (2)
  • ZoomSegment (780-785)
  • LayoutSegment (724-728)
apps/desktop/src/routes/editor/ui.tsx (2)
  • EditorButton (352-412)
  • Field (25-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Clippy
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (2)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

592-607: Nice: defensive selection handling for zoom segments

Good use of guards to derive valid zoom segments from the selection and clear it when nothing resolves. This prevents dangling/invalid UI states.


2276-2379: LGTM: LayoutSegmentConfig UX and state updates are consistent

  • Good use of controlled KTabs with a clear 3-state layout mode.
  • Done/Delete actions align with the rest of the editor UI and use projectActions.deleteLayoutSegment correctly.
  • Fallback to "default" when mode is unset matches the type’s optional mode.

@ameer2468 ameer2468 merged commit 21568c8 into main Aug 20, 2025
13 of 15 checks passed
@ameer2468 ameer2468 deleted the layouts branch August 20, 2025 08:53
Copy link
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)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (1)

1667-1673: Type mismatch: KSelect.Value expects CameraShape, not StereoMode

This select is for camera “Shape” but the Value’s type param is StereoMode. This will cause TS errors and/or incorrect type inference.

Apply this diff:

         <KSelect.Value<{
           name: string;
-          value: StereoMode;
+          value: CameraShape;
         }> class="flex-1 text-sm text-left truncate text-[--gray-500] font-normal">
♻️ Duplicate comments (2)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

2192-2199: Reactive fix for zoom handle position looks good

Switching from states.manual.* to mode().* makes the handle reactive to both drag updates and external changes. This resolves the earlier non-reactivity issue.


44-53: Missing IconLucideLayout import will break LayoutSegmentConfig

LayoutSegmentConfig uses (Line 2303), but there’s no corresponding import. This will fail at build/runtime.

Apply this diff to add the missing import:

 import {
   type BackgroundSource,
   type CameraShape,
   commands,
   type LayoutSegment,
   type StereoMode,
   type TimelineSegment,
   type ZoomSegment,
 } from "~/utils/tauri";
+import IconLucideLayout from "~icons/lucide/layout";
 import IconLucideMonitor from "~icons/lucide/monitor";
🧹 Nitpick comments (3)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (3)

2294-2301: Clear selection after deleting a layout segment

After deleting the selected layout segment, the selection remains truthy, leaving an empty overlay. Clear the selection to match the zoom flow’s UX.

Apply this diff:

         <EditorButton
           variant="danger"
           onClick={() => {
             projectActions.deleteLayoutSegment(props.segmentIndex);
+            setEditorState("timeline", "selection", null);
           }}
           leftIcon={<IconCapTrash />}
         >

2000-2000: Remove invalid/empty tabIndex prop

tabIndex expects a number; an empty string is unnecessary and can cause typing/DOM issues. Remove it or set a number explicitly (-1 or 0).

Apply this diff:

-          <KTabs.Content value="manual" tabIndex="">
+          <KTabs.Content value="manual">

2283-2379: LayoutSegmentConfig: clear, minimal controls; consider indicating camera availability

The “Camera Layout” tabs are straightforward. Small enhancement idea: disable or annotate “Camera Only” when no camera is present in the underlying recording segments, similar to how the Camera tab is disabled when no camera exists.

If desired, I can wire a disabled state based on editorInstance.recordings.segments.every((s) => s.camera === null).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06adbf5 and 2391ad0.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/editor/ConfigSidebar.tsx (17 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)
apps/desktop/src/utils/tauri.ts (2)
  • ZoomSegment (780-785)
  • LayoutSegment (724-728)
apps/desktop/src/routes/editor/ui.tsx (2)
  • EditorButton (352-412)
  • Field (25-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (2)
apps/desktop/src/routes/editor/ConfigSidebar.tsx (2)

1806-1924: ZoomSegmentPreview: solid, resilient preview pipeline

Good robustness improvements: relativeTime seeking, canplay gating, explicit load(), error retry, and render gating with a loading overlay. The canvas cropping and smoothing settings are sensible.


592-671: Zoom multi-select UX and guardrails are sound

  • Properly maps indices to existing zoomSegments and filters out undefined.
  • Clears selection if none remain, avoiding stale overlays.
  • Nice fallback preview grid with for performance; after fixing index()/prop, the labels will be correct.

Comment on lines +645 to +651
<Index each={value().segments}>
{(item, index) => (
<div class="p-2.5 rounded-lg border border-gray-4 bg-gray-3">
<ZoomSegmentPreview
segment={item().segment}
segmentIndex={index}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Index accessor usage: pass index() rather than the accessor itself

Solid’s provides an accessor function for index. Passing the function causes label formatting bugs (e.g., “Zoom [object Function]1”). Use index() to pass a number.

Apply this diff:

-                              <ZoomSegmentPreview
-                                segment={item().segment}
-                                segmentIndex={index}
-                              />
+                              <ZoomSegmentPreview
+                                segment={item().segment}
+                                segmentIndex={index()}
+                              />
📝 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
<Index each={value().segments}>
{(item, index) => (
<div class="p-2.5 rounded-lg border border-gray-4 bg-gray-3">
<ZoomSegmentPreview
segment={item().segment}
segmentIndex={index}
/>
<Index each={value().segments}>
{(item, index) => (
<div class="p-2.5 rounded-lg border border-gray-4 bg-gray-3">
<ZoomSegmentPreview
segment={item().segment}
segmentIndex={index()}
/>
🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/ConfigSidebar.tsx around lines 645 to 651, the
Index child is passing the index accessor function (index) into
ZoomSegmentPreview which leads to label formatting bugs; call the accessor
(index()) and pass the resulting number instead. Update the prop segmentIndex to
use index() rather than index so ZoomSegmentPreview receives a numeric index.
Ensure any other usages in this mapped block also call index() if they expect a
number.

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.

2 participants