Skip to content

Conversation

@oscartbeaumont
Copy link
Contributor

@oscartbeaumont oscartbeaumont commented Oct 17, 2025

Click and drag instead of having to move around the box around.

TODO:

  • Default box. This needs to lock to the primary monitor which is not something the current model handles well but this feels like the best UX.

Summary by CodeRabbit

  • New Features

    • Enhanced target-selection overlay with live-draw creation, draggable controls after creation, multiple resize handles, dimmed non-selected areas, improved creation/resizing workflows, and area-too-small feedback
    • Improved multi-display behavior: overlay initializes and positions based on hovered display
    • Two new UI icons for maximize and ratio controls
  • Chores

    • Added a dependency to enable inter-window broadcast communication

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Walkthrough

Added a broadcast-channel dependency, passed an isHoveredDisplay query param when creating the TargetSelectOverlay window, refactored the overlay to support display-aware initial bounds, interactive area creation/resizing (handles, occluders, draggable controls), and added two Lucide icon type declarations.

Changes

Cohort / File(s) Change Summary
Broadcast Channel Dependency
apps/desktop/package.json
Added @solid-primitives/broadcast-channel@^0.1.1 to dependencies for inter-component messaging.
Tauri Window Creation
apps/desktop/src-tauri/src/windows.rs
Computes whether cursor display equals target display and appends isHoveredDisplay as a query parameter to the TargetSelectOverlay window URL.
Target Select Overlay
apps/desktop/src/routes/target-select-overlay.tsx
Large refactor: added MIN_WIDTH/MIN_HEIGHT, EMPTY_BOUNDS/DEFAULT_BOUNDS, broadcast-channel reset on mousedown, creation lifecycle (mousedown→mousemove→mouseup), live overlay + occluders, hasArea tracking, modular ResizeHandles and Occluders, constrained drag/resize logic, and area-too-small UI feedback.
Icon Typings
packages/ui-solid/src/auto-imports.d.ts
Added global ambient declarations for IconLucideMaximize and IconLucideRatio.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Tauri as Tauri Window
    participant Overlay as Target Select Overlay
    participant BC as Broadcast Channel

    User->>Tauri: open target-select overlay
    Tauri->>Tauri: determine cursor vs target display
    Tauri->>Overlay: create window with ?isHoveredDisplay=(true|false)
    Overlay->>Overlay: init bounds (DEFAULT or EMPTY)

    rect rgba(245,250,255,0.9)
    Note over User,Overlay: Area creation flow
    User->>Overlay: mousedown (root)
    Overlay->>BC: post reset
    Overlay->>Overlay: set creating=true, record start
    User->>Overlay: mousemove
    Overlay->>Overlay: update live bounds, render occluders
    User->>Overlay: mouseup
    Overlay->>Overlay: finalize bounds, set hasArea if valid
    end

    rect rgba(245,255,245,0.9)
    Note over User,Overlay: Post-creation interactions
    User->>Overlay: interact with ResizeHandles / drag controls
    Overlay->>Overlay: apply constrained transforms, update occluders & placement
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hop across displays with whiskered care,
A tiny broadcast hum splits the air,
I drag the corners, darken all around,
I mark the bounds where your capture is found,
Click — and the overlay holds the chosen ground.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Improve area selection" is directly related to the main changes in the changeset. The primary modifications are concentrated in target-select-overlay.tsx, which implements a significant rework of the area selection workflow from a static interaction model to a drag-based creation lifecycle with live overlay feedback. This aligns with the stated PR objective to "change selection interaction to click-and-drag instead of moving the box around." While the title is somewhat broad and doesn't specify the exact nature of the improvement (click-and-drag interaction), it clearly captures the main focus of the changeset and would help a teammate understand that this PR concerns the area selection feature.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-area-select2

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

❤️ Share

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

@oscartbeaumont oscartbeaumont marked this pull request as ready for review October 22, 2025 05:41
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: 4

🧹 Nitpick comments (5)
apps/desktop/src/routes/target-select-overlay.tsx (5)

7-7: Derive hasArea from bounds; avoid writable memo to prevent desync on cross-window resets.

Writable memo can get “stuck” when other events (e.g., broadcast reset) change bounds. A pure memo tied to bounds is safer; hide the box while creating via creating().

- import { createWritableMemo } from "@solid-primitives/memo";
+ // (no longer needed)

@@
-  const [hasArea, setHasArea] = createWritableMemo(
-    () => bounds.size.width > 0 && bounds.size.height > 0,
-  );
+  const hasArea = createMemo(
+    () => bounds.size.width > 0 && bounds.size.height > 0,
+  );

@@
-  setCreating(true);
-  setHasArea(false);
+  setCreating(true);

@@
-  mouseup: () => {
-    setCreating(false);
-    // Only set hasArea if we created a meaningful area
-    if (bounds.size.width > 10 && bounds.size.height > 10) {
-      setHasArea(true);
-    } else {
-      // Reset to no area if too small
-      setBounds({
-        position: { x: 0, y: 0 },
-        size: { width: 0, height: 0 },
-      });
-    }
-    dispose();
-  },
+  mouseup: () => {
+    setCreating(false);
+    if (!(bounds.size.width > 10 && bounds.size.height > 10)) {
+      setBounds(structuredClone(EMPTY_BOUNDS));
+    }
+    dispose();
+  },

@@
-  <Show when={hasArea()}>
+  <Show when={hasArea() && !creating()}>

@@
-  <Show when={!creating()}>
+  <Show when={!creating()}>
     <Show when={!hasArea()}>
       <p class="z-10 text-xl pointer-events-none text-white">Click and drag to select area</p>
     </Show>
-    <Show when={hasArea()}>
+    <Show when={hasArea()}>
       <p class="z-10 text-xl pointer-events-none text-white absolute bottom-4">Click and drag to create new area</p>
     </Show>
   </Show>

Also add createMemo to the Solid import list and remove the createWritableMemo import.

Also applies to: 285-291, 704-712, 796-807, 661-662, 684-697


42-51: Introduce an explicit Bounds type instead of using typeof bounds to improve typing at IPC boundaries.

Relying on typeof bounds (a Store proxy type) is brittle and obscures the contract with tauri commands.

@@
-const EMPTY_BOUNDS = {
+type Bounds = {
+  position: { x: number; y: number };
+  size: { width: number; height: number };
+};
+
+const EMPTY_BOUNDS: Bounds = {
   position: { x: 0, y: 0 },
   size: { width: 0, height: 0 },
 };
@@
-const DEFAULT_BOUNDS = {
+const DEFAULT_BOUNDS: Bounds = {
   position: { x: 100, y: 100 },
   size: { width: 400, height: 300 },
 };
@@
-const [bounds, _setBounds] = createStore(
-  structuredClone(isPrimaryDisplay ? DEFAULT_BOUNDS : EMPTY_BOUNDS),
-);
+const [bounds, _setBounds] = createStore<Bounds>(
+  structuredClone(isPrimaryDisplay ? DEFAULT_BOUNDS : EMPTY_BOUNDS),
+);
@@
-const setBounds = (newBounds: typeof bounds) => {
+const setBounds = (newBounds: Bounds) => {
   const clampedBounds: Bounds = {
     position: {
       x: Math.max(0, newBounds.position.x),
       y: Math.max(0, newBounds.position.y),
     },
     size: {
       width: Math.max(
         150,
         Math.min(
           window.innerWidth - Math.max(0, newBounds.position.x),
           newBounds.size.width,
         ),
       ),
       height: Math.max(
         150,
         Math.min(
           window.innerHeight - Math.max(0, newBounds.position.y),
           newBounds.size.height,
         ),
       ),
     },
   };
   _setBounds(clampedBounds);
};

Also applies to: 112-115, 124-149


611-651: Occluders depend on window.innerWidth/innerHeight; consider making them reactive or using CSS calc.

Unlikely to matter (overlay usually fixed), but a small tweak improves correctness on DPI/resize changes.

Example (Right occluder):

- style={{
-   width: `${window.innerWidth - (bounds.size.width + bounds.position.x)}px`,
- }}
+ style={{
+   right: 0,
+   width: `calc(100vw - ${bounds.size.width + bounds.position.x}px)`,
+ }}

Apply similarly for bottom occluder with 100vh.


65-70: Follow-up to Rust change: rename isPrimaryDisplay param to isCursorDisplay (or implement true primary).

If you adopt the rename in Rust, update the search params and local variable here accordingly.

- const [params] = useSearchParams<{ displayId: DisplayId; isPrimaryDisplay: string }>();
- const isPrimaryDisplay = params.isPrimaryDisplay === "true";
+ const [params] = useSearchParams<{ displayId: DisplayId; isCursorDisplay: string }>();
+ const isPrimaryDisplay = params.isCursorDisplay === "true"; // or rename this local too

Please confirm intended behavior: default box only on system primary display, or on the display under the cursor at overlay launch? Based on PR description, it sounds like “primary”.


42-51: Nit: centralize min size as a constant to keep behavior consistent across setBounds and all handles.

You repeat 150 several times; a single MIN_SIZE constant reduces mistakes.

+const MIN_SIZE = 150;
...
- width: Math.max(150, /*...*/)
+ width: Math.max(MIN_SIZE, /*...*/)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a0b685 and cb66cc6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/desktop/package.json (1 hunks)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • apps/desktop/src/routes/target-select-overlay.tsx (5 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Format Rust code using rustfmt and ensure all Rust code passes workspace-level clippy lints.
Rust modules should be named with snake_case, and crate directories should be in kebab-case.

Files:

  • apps/desktop/src-tauri/src/windows.rs
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (2)
apps/desktop/src-tauri/src/windows.rs (3)
crates/scap-targets/src/platform/macos.rs (2)
  • get_containing_cursor (78-92)
  • display (519-539)
crates/scap-targets/src/platform/win.rs (2)
  • get_containing_cursor (152-165)
  • display (1044-1051)
crates/scap-targets/src/lib.rs (2)
  • get_containing_cursor (36-38)
  • display (146-148)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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)
packages/ui-solid/src/auto-imports.d.ts (1)

79-79: LGTM – new icon auto-imports.

No issues; aligns with auto-imported icon pattern.

Also applies to: 83-83

apps/desktop/src/routes/target-select-overlay.tsx (1)

124-149: Guard setBounds against invalid inputs at call sites sourced from IPC.

When calling setBounds(windowUnderCursor.bounds), ensure the shape matches Bounds to avoid runtime surprises from platform differences (e.g., fractional vs integer values).

Add a narrow zod schema check (dev-only) or a TypeScript type assertion where bounds come from Tauri.

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 (1)
apps/desktop/src-tauri/src/windows.rs (1)

303-317: Compute true OS primary display — the cursor-display comparison is semantically incorrect and causes wrong bounds initialization.

The code currently assigns is_primary_display by checking if the cursor is over the current display, but the variable name and its usage in the TypeScript overlay initialization (controlling bounds) expect the OS primary display. scap_targets::Display::primary() exists and is public—use it instead.

-                let is_primary_display = scap_targets::Display::get_containing_cursor()
-                    .map(|d| d.id())
-                    == Some(display.id());
+                let is_primary_display = scap_targets::Display::primary().id() == display.id();

This fix ensures the overlay uses DEFAULT_BOUNDS initialization on the actual OS primary display, rather than only when the cursor happens to be there.

🧹 Nitpick comments (3)
apps/desktop/src/routes/target-select-overlay.tsx (3)

124-149: Avoid magic numbers for min area; centralize to a constant.

150 is repeated and hard-coded. Define a single MIN_AREA constant and reuse in clamps and handle math.

+const MIN_AREA = 150;
...
-        width: Math.max(
-          150,
+        width: Math.max(
+          MIN_AREA,
           Math.min(
             window.innerWidth - Math.max(0, newBounds.position.x),
             newBounds.size.width,
           ),
         ),
-        height: Math.max(
-          150,
+        height: Math.max(
+          MIN_AREA,
           Math.min(
             window.innerHeight - Math.max(0, newBounds.position.y),
             newBounds.size.height,
           ),
         ),

Propagate MIN_AREA into ResizeHandles computations as well.


655-701: Emit “reset” when creation actually starts; drop global window mousedown emitter.

Posting reset on every window mousedown can cause self-resets and unnecessary chatter. Emit once at the start of area creation here and remove the global emitter.

-  onMouseDown={(downEvent) => {
+  onMouseDown={(downEvent) => {
       // Start creating a new area
       downEvent.preventDefault();
+      postMessage?.({ type: "reset", senderId });
       setCreating(true);
       setHasArea(false);

And remove the earlier window-level mousedown post (see broadcast-channel comment).


679-699: Creation size clamps during drag may force 150px immediately.

Because setBounds enforces MIN_AREA, the selection jumps to min size on the first move. If you want true “rubber band” UX, defer min-size enforcement to mouseup and allow smaller preview during creation. Optional.

Example approach:

  • During creating, write directly via _setBounds without min clamp.
  • On mouseup, if width/height < MIN_AREA, expand to MIN_AREA anchored to drag origin.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a0b685 and cb66cc6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • apps/desktop/package.json (1 hunks)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • apps/desktop/src/routes/target-select-overlay.tsx (5 hunks)
  • packages/ui-solid/src/auto-imports.d.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • packages/ui-solid/src/auto-imports.d.ts
  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Format Rust code using rustfmt and ensure all Rust code passes workspace-level clippy lints.
Rust modules should be named with snake_case, and crate directories should be in kebab-case.

Files:

  • apps/desktop/src-tauri/src/windows.rs
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (2)
apps/desktop/src-tauri/src/windows.rs (4)
apps/desktop/src-tauri/src/target_select_overlay.rs (1)
  • scap_targets (52-55)
crates/scap-targets/src/platform/macos.rs (2)
  • get_containing_cursor (78-92)
  • display (519-539)
crates/scap-targets/src/platform/win.rs (2)
  • get_containing_cursor (152-165)
  • display (1044-1051)
crates/scap-targets/src/lib.rs (2)
  • get_containing_cursor (36-38)
  • display (146-148)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
🔇 Additional comments (2)
packages/ui-solid/src/auto-imports.d.ts (1)

79-79: LGTM! Auto-generated icon imports are correctly formatted.

These icon declarations were automatically generated by unplugin-auto-import and are correctly formatted and alphabetically ordered within the Lucide icons section. The new Maximize and Ratio icons align with the area selection improvements described in the PR.

Also applies to: 83-83

apps/desktop/package.json (1)

24-24: No issues found — BroadcastChannel support and cleanup verified.

Verification confirms BroadcastChannel is supported in Safari 15.4+ (macOS/WebKit) and WebView2 supports BroadcastChannel as a Chromium-based runtime. Your Tauri versions (2.3.0–2.5.0) have no compatibility issues.

The @solid-primitives/broadcast-channel library automatically cleans up the channel on owner cleanup, so no manual close/dispose calls are needed. The single usage in target-select-overlay.tsx:116 follows the correct pattern and requires no changes.

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 (2)
apps/desktop/src/routes/target-select-overlay.tsx (2)

116-122: Critical: Global mousedown resets bounds on every click, breaking UI interactions and potentially causing zero-size recordings.

The window-level mousedown listener broadcasts a reset on every click—including clicks on UI buttons like "Start Recording." The onMessage handler unconditionally clears bounds without filtering by message type. This can:

  • Clear the selection area when clicking controls
  • Start a recording with zero-size bounds if the reset happens mid-click
  • Trigger resets from the sender's own messages

Apply this diff to fix:

 const { postMessage, onMessage } = makeBroadcastChannel(
   "target_select_overlay",
 );
-createEventListener(window, "mousedown", () =>
-  postMessage({ type: "reset" }),
-);
-onMessage(() => _setBounds(structuredClone(EMPTY_BOUNDS)));
+const senderId = crypto.randomUUID();
+onMessage((msg: unknown) => {
+  const data = msg as { type?: string; senderId?: string };
+  if (data?.type === "reset" && data?.senderId !== senderId) {
+    _setBounds(structuredClone(EMPTY_BOUNDS));
+  }
+});
+onCleanup(() => {
+  // Close broadcast channel on cleanup
+});

Then in the area creation mousedown handler (line 657), add:

postMessage({ type: "reset", senderId });

308-322: Resize listener leaks because createRoot is never disposed.

The createRoot here creates a disposal context, but dispose() is never invoked—onCleanup inside an undisposed root never executes. This leaks the window resize handler on every render.

Apply this diff to fix:

-createRoot((dispose) => {
-  const onResize = () => {
-    const ctrlH = controlsEl?.offsetHeight ?? 64;
-    const margin = 16;
-    const wouldOverflow =
-      bounds.position.y + bounds.size.height + margin + ctrlH >
-      window.innerHeight;
-    setPlaceControlsAbove(wouldOverflow);
-  };
-  window.addEventListener("resize", onResize);
-  onCleanup(() => {
-    window.removeEventListener("resize", onResize);
-    dispose();
-  });
-});
+createEventListener(window, "resize", () => {
+  const ctrlH = controlsEl?.offsetHeight ?? 64;
+  const margin = 16;
+  const wouldOverflow =
+    bounds.position.y + bounds.size.height + margin + ctrlH > window.innerHeight;
+  setPlaceControlsAbove(wouldOverflow);
+});
🧹 Nitpick comments (2)
apps/desktop/src/routes/target-select-overlay.tsx (2)

47-50: Consider making DEFAULT_BOUNDS display-aware.

The hardcoded position (100, 100) and size (400, 300) may not work well on smaller displays or if the primary monitor isn't positioned at origin. Consider deriving these from window.innerWidth and window.innerHeight to ensure the default box always fits within the visible viewport.


687-695: Consider extracting the minimum area threshold to a constant.

The 10px threshold for meaningful area selection is hardcoded. Consider extracting to a named constant like MIN_AREA_THRESHOLD = 10 for clarity and maintainability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb66cc6 and 08b0ea0.

📒 Files selected for processing (2)
  • apps/desktop/src-tauri/src/windows.rs (2 hunks)
  • apps/desktop/src/routes/target-select-overlay.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src-tauri/src/windows.rs
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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 (6)
apps/desktop/src/routes/target-select-overlay.tsx (6)

124-149: LGTM! Bounds clamping logic is correct.

The clamping logic properly ensures bounds stay within the window and meet minimum size constraints (150px).


324-357: LGTM! Drag handler correctly manages lifecycle and uses requestAnimationFrame.

The createRoot/dispose pattern here is correct: dispose() is called on mouseup, properly cleaning up the event listeners.


359-609: LGTM! Resize handles correctly implement 8-directional resizing with proper constraints.

All corner and edge handles properly enforce the 150px minimum size and adjust positions to compensate for clamped dimensions.


611-652: LGTM! Occluder overlay correctly covers non-selected screen areas.

The four occluder divs properly partition the screen around the selected bounds.


816-975: LGTM! Recording controls properly handle authentication, mode selection, and recording initialization.

The component correctly gates instant mode behind authentication and uses the typed mutation pattern.


997-1012: LGTM! ResizeHandle component correctly centers handles and accepts style overrides.

The transform translation and prop spreading are implemented correctly.

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 (3)
apps/desktop/src/routes/target-select-overlay.tsx (3)

66-70: Verify param name and semantics match the backend implementation.

The param is named isHoveredDisplay, but ensure this aligns with what the backend sends (see past comments suggesting isCursorDisplay or a true isPrimaryDisplay computed in Rust). Confirm the parameter key and the derived boolean accurately reflect the backend's intent.

Based on learnings from past review comments.


310-316: Prefer createEventListener for the window resize handler.

The current createEffect + manual addEventListener + onCleanup pattern works, but createEventListener (already imported) provides automatic lifecycle management and is the idiomatic Solid primitive for event handling.

Apply this diff:

-createEffect(() => {
-  setControlsHeight(controlsEl?.offsetHeight);
-  const onResize = () => setControlsHeight(controlsEl?.offsetHeight);
-
-  window.addEventListener("resize", onResize);
-  onCleanup(() => window.removeEventListener("resize", onResize));
-});
+createEffect(() => {
+  setControlsHeight(controlsEl?.offsetHeight);
+});
+createEventListener(window, "resize", () => {
+  setControlsHeight(controlsEl?.offsetHeight);
+});

Based on learnings from past review comments.


116-123: Critical: Broadcast reset on every window mousedown breaks UI interactions and lacks proper message filtering.

The global window mousedown listener broadcasts reset on every click, including clicks on the Start Recording button, gear icon, and other controls. This clears bounds mid-interaction and can start recordings with zero-size areas. Additionally:

  • onMessage doesn't filter by type, so any broadcast triggers a reset.
  • No channel cleanup on unmount.
  • No sender ID to prevent self-reset during multi-step interactions.

Fix:

  1. Remove the global window mousedown listener.
  2. Broadcast reset only when starting a new area selection (inside the overlay surface mousedown handler around line 651).
  3. Filter onMessage by msg?.type === "reset".
  4. Add a sender ID to outgoing messages and ignore messages from self.
  5. Close the channel on cleanup.

Apply this pattern (based on past review guidance):

-const { postMessage, onMessage } = makeBroadcastChannel<{ type: "reset" }>(
+const { postMessage, onMessage, close } = makeBroadcastChannel<{ type: "reset"; senderId?: string }>(
   "target_select_overlay",
 );
-createEventListener(window, "mousedown", () =>
-  postMessage({ type: "reset" }),
-);
-onMessage(() => setBounds(structuredClone(EMPTY_BOUNDS)));
+const senderId = crypto.randomUUID();
+onMessage((msg) => {
+  if (msg?.type === "reset" && msg?.senderId !== senderId) {
+    setBounds(structuredClone(EMPTY_BOUNDS));
+  }
+});
+onCleanup(() => close?.());

Then in the overlay surface mousedown handler (around line 651), add:

 onMouseDown={(downEvent) => {
   // Start creating a new area
   downEvent.preventDefault();
+  postMessage({ type: "reset", senderId });
   setState("creating");
   setHasArea(false);

Based on learnings from past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08b0ea0 and a9d0da8.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/target-select-overlay.tsx (6 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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: Analyze (rust)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
🔇 Additional comments (5)
apps/desktop/src/routes/target-select-overlay.tsx (5)

2-2: LGTM: Imports align with new broadcast channel and writable memo usage.

The new imports are properly utilized throughout the refactored area selection flow.

Also applies to: 7-7


43-51: LGTM: Well-defined layout constants.

EMPTY_BOUNDS and DEFAULT_BOUNDS provide clear initial states for display-aware initialization.


318-351: LGTM: Well-structured drag helper with proper cleanup.

The createOnMouseDown helper correctly:

  • Captures startBounds at interaction start.
  • Uses createRoot for isolated scope with global listeners.
  • Throttles updates with requestAnimationFrame.
  • Disposes listeners on mouseup.

353-603: LGTM: Comprehensive resize handles with correct math and constraints.

The ResizeHandles component properly implements:

  • Eight handles (4 corners + 4 edges) with appropriate cursors.
  • Correct positioning logic for each handle type.
  • Minimum 150px size enforcement.
  • Position compensation when size limits are hit.
  • Event propagation control with stopPropagation.

605-646: LGTM: Occluders and area creation/dragging logic are well-implemented.

The implementation correctly:

  • Darkens non-selected areas with properly calculated occluder dimensions.
  • Handles bidirectional drag during creation (Math.abs for width/height, Math.min for position).
  • Validates minimum area size (>10px) before committing.
  • Clamps position explicitly during drag (lines 729-742) before using _setBounds.
  • Shows appropriate visual feedback and instruction text based on state.

Also applies to: 698-804

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

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/target-select-overlay.tsx (1)

1002-1006: Remove unused helper.

getDisplayId isn’t referenced; drop it to reduce surface area.

♻️ Duplicate comments (2)
apps/desktop/src/routes/target-select-overlay.tsx (2)

120-127: Scope the broadcast reset, filter by type, ignore self, and close channel (remove global window mousedown).

Global window mousedown broadcasts reset on every click; onMessage resets on any message. This wipes selection when clicking controls and can start with zero-size bounds. Add senderId, filter message.type, ignore self, and close on cleanup; only post when user starts a new selection on the overlay surface. This was previously flagged and remains unresolved.

Apply:

-  const { postMessage, onMessage } = makeBroadcastChannel<{ type: "reset" }>(
-    "target_select_overlay",
-  );
-  createEventListener(window, "mousedown", () =>
-    postMessage({ type: "reset" }),
-  );
-  onMessage(() => setBounds(EMPTY_BOUNDS));
+  const { postMessage, onMessage, close } = makeBroadcastChannel<
+    { type: "reset"; senderId?: string }
+  >("target_select_overlay");
+  const senderId =
+    (globalThis.crypto?.randomUUID?.() ??
+      `${Date.now()}-${Math.random()}`).toString();
+  onMessage((msg) => {
+    if (msg?.type === "reset" && msg?.senderId !== senderId) {
+      setBounds(structuredClone(EMPTY_BOUNDS));
+    }
+  });
+  onCleanup(() => close?.());

620-667: Emit typed reset with senderId only when starting creation on the overlay surface.

Pair this with the filtered receiver above so other overlays clear, not the sender.

-        // Start creating a new area
+        // Start creating a new area
         downEvent.preventDefault();
-        postMessage({ type: "reset" });
+        postMessage({ type: "reset", senderId });
🧹 Nitpick comments (2)
apps/desktop/src/routes/target-select-overlay.tsx (2)

258-267: Avoid mixed derived/mutable state for hasArea.

createWritableMemo is derived from bounds but is also manually set, which is confusing. Prefer a pure derived memo or a plain signal. Given UI already derives “too small” from bounds, keep hasArea derived.

- const [hasArea, setHasArea] = createWritableMemo(
-   () => bounds.size.width > 0 && bounds.size.height > 0,
- );
+ const hasArea = createMemo(
+   () => bounds.size.width > 0 && bounds.size.height > 0,
+ );

- setHasArea(false);
+ // rely on bounds reset to compute hasArea=false

- if (bounds.size.width > 10 && bounds.size.height > 10) {
-   setHasArea(true);
- } else {
+ if (!(bounds.size.width > 10 && bounds.size.height > 10)) {
    setBounds(EMPTY_BOUNDS);

Also applies to: 628-629, 654-656


577-618: Occluders also read window dimensions non-reactively.

Use the same reactive viewport signals so occluders adjust on resize.

-  width: `${window.innerWidth - (bounds.size.width + bounds.position.x)}px`,
+  width: `${viewportW() - (bounds.size.width + bounds.position.x)}px`,
...
-  height: `${window.innerHeight - (bounds.size.height + bounds.position.y)}px`,
+  height: `${viewportH() - (bounds.size.height + bounds.position.y)}px`,

Add alongside viewportH:

+ const [viewportW, setViewportW] = createSignal(window.innerWidth);
+ createEventListener(window, "resize", () => setViewportW(window.innerWidth));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9d0da8 and 36ab661.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/target-select-overlay.tsx (8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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 (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)

69-75: Param rename to isHoveredDisplay looks consistent.

Front-end now derives isHoveredDisplay from search params; aligns with the described backend change.

Comment on lines +270 to +289
const [controlsHeight, setControlsHeight] = createSignal<
number | undefined
>(undefined);
// Recompute placement when bounds change or window resizes
const placeControlsAbove = createMemo(() => {
const top = bounds.position.y;
const height = bounds.size.height;
// Measure controls height (fallback to 64px if not yet mounted)
const ctrlH = controlsHeight() ?? 64;
const margin = 16;

const wouldOverflow =
top + height + margin + ctrlH > window.innerHeight;
return wouldOverflow;
});
onMount(() => setControlsHeight(controlsEl?.offsetHeight));
createEventListener(window, "resize", () =>
setControlsHeight(controlsEl?.offsetHeight),
);

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 | 🟠 Major

placeControlsAbove isn’t reactive to viewport size changes.

You read window.innerHeight in a memo without a reactive source; if controlsHeight doesn’t change on resize, placement won’t recompute. Track viewport height explicitly.

- const [controlsHeight, setControlsHeight] = createSignal<number | undefined>(undefined);
+ const [controlsHeight, setControlsHeight] = createSignal<number | undefined>(undefined);
+ const [viewportH, setViewportH] = createSignal(window.innerHeight);

- const placeControlsAbove = createMemo(() => {
+ const placeControlsAbove = createMemo(() => {
    const top = bounds.position.y;
    const height = bounds.size.height;
    const ctrlH = controlsHeight() ?? 64;
    const margin = 16;
-   const wouldOverflow = top + height + margin + ctrlH > window.innerHeight;
+   const wouldOverflow = top + height + margin + ctrlH > viewportH();
    return wouldOverflow;
  });
- onMount(() => setControlsHeight(controlsEl?.offsetHeight));
- createEventListener(window, "resize", () => setControlsHeight(controlsEl?.offsetHeight));
+ onMount(() => {
+   setControlsHeight(controlsEl?.offsetHeight);
+   setViewportH(window.innerHeight);
+ });
+ createEventListener(window, "resize", () => {
+   setControlsHeight(controlsEl?.offsetHeight);
+   setViewportH(window.innerHeight);
+ });
🤖 Prompt for AI Agents
In apps/desktop/src/routes/target-select-overlay.tsx around lines 270 to 289,
placeControlsAbove reads window.innerHeight directly so the memo won't rerun on
viewport changes; add a reactive viewport height signal (e.g., const
[viewportHeight, setViewportHeight] = createSignal(window.innerHeight)), set it
on mount and update it inside the existing resize listener
(setViewportHeight(window.innerHeight)), and then use viewportHeight() in the
memo instead of window.innerHeight so placement recomputes when the viewport is
resized; keep the existing controlsHeight updates as-is.

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 (3)
apps/desktop/src/routes/target-select-overlay.tsx (3)

69-73: Verify param alignment with backend changes.

Past reviews indicate the backend parameter naming may have changed (from isPrimaryDisplay to isCursorDisplay or similar). Ensure the front-end param key matches what the Rust backend actually sends in windows.rs.


116-126: Critical: Broadcast channel issues remain unaddressed.

The global window mousedown listener (line 123-125) and unfiltered onMessage handler (line 126) create multiple issues already flagged in past reviews:

  1. Every window click (including UI button clicks) broadcasts a reset, clearing the area mid-interaction
  2. No message type filtering—any broadcast resets bounds
  3. No sender ID—windows can reset themselves
  4. No cleanup—channel remains open after unmount

See past review comments for the comprehensive fix involving message filtering, sender IDs, scoped reset calls, and cleanup.


270-288: placeControlsAbove won't recompute on viewport resize.

The memo reads window.innerHeight directly (line 282), which isn't a reactive signal. If the window resizes but controlsHeight() doesn't change, placement won't update.

See past review comment for the fix: add a viewportHeight signal, update it in the resize listener, and use it in the memo.

🧹 Nitpick comments (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)

46-49: EMPTY_BOUNDS as a function is an unusual pattern; prefer a constant with structuredClone at call sites.

Defining EMPTY_BOUNDS as a function that returns a new object each time is unconventional. Since you already use structuredClone(EMPTY_BOUNDS()) in line 126 and setBounds(EMPTY_BOUNDS()) elsewhere, you're creating nested object creation overhead.

Apply this diff to use a constant instead:

-const EMPTY_BOUNDS = () => ({
+const EMPTY_BOUNDS = {
 	position: { x: 0, y: 0 },
 	size: { width: 0, height: 0 },
-});
+};

Then update call sites to clone:

-	onMessage(() => setBounds(EMPTY_BOUNDS()));
+	onMessage(() => setBounds(structuredClone(EMPTY_BOUNDS)));

-								setBounds(EMPTY_BOUNDS());
+								setBounds(structuredClone(EMPTY_BOUNDS));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36ab661 and b996143.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/target-select-overlay.tsx (8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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 (5)
apps/desktop/src/routes/target-select-overlay.tsx (5)

290-323: Good use of createRoot with proper disposal.

The createOnMouseDown helper correctly creates an isolated reactive root and disposes it on mouseup (line 308), preventing memory leaks. The requestAnimationFrame throttling is also appropriate for mousemove handlers.


325-575: Resize handles now correctly use MIN_WIDTH/MIN_HEIGHT constants.

All resize handle calculations properly enforce the minimum dimensions using the constants (200×100), addressing the previous inconsistency with hardcoded 150 values. The implementation is now consistent with the isAreaTooSmall validation.


623-664: Area creation correctly broadcasts reset on interaction start.

The postMessage({ type: "reset" }) call on line 626 is properly scoped to when the user begins creating a new area, not on every window click. This is the correct placement per the architecture discussed in past reviews.

However, note that the broadcast channel itself still needs the filtering/cleanup fixes flagged in the critical issue comment.


789-794: isAreaTooSmall correctly validates using shared constants.

The validation function properly uses MIN_WIDTH and MIN_HEIGHT, and now that the resize handles also use these constants (verified in lines 325-575), the UX is fully consistent—users cannot create or resize to dimensions that would be flagged as "too small."


860-959: Good UX: clear feedback for undersized areas.

The conditional rendering with a descriptive fallback message (lines 863-875) provides helpful feedback, showing both the minimum required dimensions and the current selection size. This aligns well with the validation logic.

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 (2)
apps/desktop/src/routes/target-select-overlay.tsx (2)

120-126: 🚨 CRITICAL: Global mousedown still broadcasts reset on every click, including UI interactions.

Despite being marked as addressed in previous reviews, the global window mousedown listener (lines 123-125) still broadcasts a reset on every mouse click, including clicks on UI controls like "Start Recording". The onMessage handler (line 126) doesn't filter by message type and has no cleanup.

Impact:

  • Clicking "Start Recording" can reset bounds to zero before the command reads them
  • Any UI interaction clears the selection
  • The window resets its own selection during creation

Apply this diff to fix:

- const { postMessage, onMessage } = makeBroadcastChannel<{ type: "reset" }>(
+ const { postMessage, onMessage, close } = makeBroadcastChannel<{ type: "reset" }>(
    "target_select_overlay",
  );
- createEventListener(window, "mousedown", () =>
-   postMessage({ type: "reset" }),
- );
- onMessage(() => setBounds(EMPTY_BOUNDS()));
+ onMessage((msg) => {
+   if (msg?.type === "reset") setBounds(EMPTY_BOUNDS());
+ });
+ onCleanup(() => close?.());

The reset broadcast now only happens at line 626 when actually starting a new area creation, which is the correct behavior.


270-288: placeControlsAbove reads window.innerHeight directly—not reactive to viewport resize.

The memo at line 274 reads window.innerHeight (line 282) without a reactive source. If the viewport resizes but controlsHeight doesn't change, the placement won't recompute. This can cause controls to overflow or remain above when there's sufficient space below.

Apply this diff to track viewport height reactively:

  const [controlsHeight, setControlsHeight] = createSignal<
    number | undefined
  >(undefined);
+ const [viewportHeight, setViewportHeight] = createSignal(window.innerHeight);
+
  // Recompute placement when bounds change or window resizes
  const placeControlsAbove = createMemo(() => {
    const top = bounds.position.y;
    const height = bounds.size.height;
    // Measure controls height (fallback to 64px if not yet mounted)
    const ctrlH = controlsHeight() ?? 64;
    const margin = 16;

    const wouldOverflow =
-     top + height + margin + ctrlH > window.innerHeight;
+     top + height + margin + ctrlH > viewportHeight();
    return wouldOverflow;
  });
- onMount(() => setControlsHeight(controlsEl?.offsetHeight));
+ onMount(() => {
+   setControlsHeight(controlsEl?.offsetHeight);
+   setViewportHeight(window.innerHeight);
+ });
  createEventListener(window, "resize", () =>
-   setControlsHeight(controlsEl?.offsetHeight),
+   {
+     setControlsHeight(controlsEl?.offsetHeight);
+     setViewportHeight(window.innerHeight);
+   }
  );

Based on previous review comments.

🧹 Nitpick comments (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)

51-54: Consider making DEFAULT_BOUNDS a function for consistency and safety.

While structuredClone is used at line 117, making DEFAULT_BOUNDS a function like EMPTY_BOUNDS would prevent accidental mutations and maintain consistency across the codebase.

Apply this diff:

-const DEFAULT_BOUNDS = {
+const DEFAULT_BOUNDS = () => ({
   position: { x: 100, y: 100 },
   size: { width: 400, height: 300 },
-};
+});

Then update line 117:

-  structuredClone(isHoveredDisplay ? DEFAULT_BOUNDS : EMPTY_BOUNDS()),
+  isHoveredDisplay ? DEFAULT_BOUNDS() : EMPTY_BOUNDS(),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b996143 and 00a2116.

📒 Files selected for processing (1)
  • apps/desktop/src/routes/target-select-overlay.tsx (8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use a 2-space indent for TypeScript code.
Use Biome for formatting and linting TypeScript/JavaScript files by running pnpm format.

Use strict TypeScript and avoid any; leverage shared types

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use kebab-case for filenames for TypeScript/JavaScript modules (e.g., user-menu.tsx).
Use PascalCase for React/Solid components.

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/desktop/src/**/*.{ts,tsx}: Desktop icons are auto-imported (unplugin-icons); do not import icons manually
Desktop IPC: Call generated tauri_specta commands/events; listen to generated events and use typed interfaces
Use @tanstack/solid-query for server state in the desktop app

Files:

  • apps/desktop/src/routes/target-select-overlay.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/target-select-overlay.tsx (1)
apps/desktop/src/utils/tauri.ts (1)
  • DisplayId (386-386)
⏰ 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 (4)
apps/desktop/src/routes/target-select-overlay.tsx (4)

69-73: Verify param name matches backend implementation.

Past reviews flagged a mismatch between frontend param naming (isPrimaryDisplay) and backend behavior (cursor display vs OS primary). The current code uses isHoveredDisplay. Ensure this param name and semantics align with what the backend actually sends in windows.rs.

Based on previous review comments.


325-575: Excellent: Resize handles now consistently use MIN_WIDTH and MIN_HEIGHT.

All 8 resize handles (4 corners + 4 edges) correctly enforce the minimum dimensions using the MIN_WIDTH and MIN_HEIGHT constants. This resolves the previous inconsistency where hardcoded 150 values caused UX confusion.


623-664: Area creation flow is well-structured.

The click-and-drag creation UX is correctly implemented:

  • Line 626: Broadcasts reset only when starting a new selection (not on every click)
  • Lines 638-663: Proper lifecycle management with createRoot and cleanup on mouseup
  • Lines 654-659: Smart validation—only commits areas larger than 10×10 pixels

789-793: Good: Area size validation with clear user feedback.

The isAreaTooSmall helper correctly validates against MIN_WIDTH and MIN_HEIGHT, and the fallback UI (lines 864-874) provides clear, actionable feedback showing both the requirement and current dimensions.

Also applies to: 861-876

@Brendonovich Brendonovich merged commit 1581b9a into main Oct 22, 2025
15 checks passed
@Brendonovich Brendonovich deleted the improve-area-select2 branch October 22, 2025 11:41
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.

3 participants