only picker overlays can finish a pick, and failed picks say why

- complete-capture-pick ignores any sender that isn't an open picker overlay
- when a pick fails for a reason other than cancel, like a window that
  closed, the hud shows the message in a toast
- moving or resizing an area reads a flag on the drag itself instead of
  react state, so a pointer move that lands before the next render still
  counts as part of the drag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KevinandClaude Opus 5.5 committed 2026-10-05 11:29:54 -07:00
1 parent f91d1a1288
commit feb5d0276a
4 files changed
+32 -7

No files matched your search

+8 -2
View File
@@ -1,5 +1,9 @@
import { ipcMain } from "electron";
import { closeCapturePickerWindows, createCapturePickerWindows } from "../../windows";
import {
closeCapturePickerWindows,
createCapturePickerWindows,
isCapturePickerWebContents,
} from "../../windows";
import { getNativeMacWindowsFrontToBack, stopWindowBoundsCapture } from "../cursor/bounds";
import { setSelectedSource } from "../state";
import { createAreaSource, createWindowSource, normalizeCapturePick } from "../sourceArea";
@@ -115,7 +119,9 @@ export function registerCapturePickerHandlers() {
return { success: true, source, record: pick.record };
});
ipcMain.handle("complete-capture-pick", async (_, input: unknown) => {
ipcMain.handle("complete-capture-pick", async (event, input: unknown) => {
// Only the picker overlays answer a pick.
if (!isCapturePickerWebContents(event.sender)) return;
if (!input) {
finishPick(null);
return;
+6 -1
View File
@@ -3,7 +3,7 @@ import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { app, BrowserWindow, ipcMain } from "electron";
import { app, BrowserWindow, ipcMain, type WebContents } from "electron";
import {
supportsHudCaptureProtection,
shouldProtectHudCapture,
@@ -1261,6 +1261,11 @@ export function closeCapturePickerWindows(): void {
}
}
/** Whether a message came from one of the open picker overlays. */
export function isCapturePickerWebContents(contents: WebContents): boolean {
return capturePickerWindows.some((win) => !win.isDestroyed() && win.webContents === contents);
}
/**
* Outlines the area being recorded. The outline sits just outside the recorded
* rectangle and belongs to Recordly, which recordings exclude, so it never shows
@@ -25,8 +25,15 @@ type Selection = Target | { kind: "area"; rect: AreaRect };
type Drag =
| { kind: "press"; startX: number; startY: number }
| { kind: "draw"; originX: number; originY: number }
| { kind: "move"; startX: number; startY: number; rect: AreaRect }
| { kind: "resize"; handle: AreaHandle; startX: number; startY: number; rect: AreaRect };
| { kind: "move"; startX: number; startY: number; rect: AreaRect; moved: boolean }
| {
kind: "resize";
handle: AreaHandle;
startX: number;
startY: number;
rect: AreaRect;
moved: boolean;
};
const TOOLBAR_HEIGHT = 44;
const TOOLBAR_GAP = 12;
@@ -190,6 +197,7 @@ export function CapturePicker() {
startX: event.clientX,
startY: event.clientY,
rect: current.rect,
moved: false,
};
} else if (current && rectContains(current.rect, event.clientX, event.clientY)) {
dragRef.current = {
@@ -197,6 +205,7 @@ export function CapturePicker() {
startX: event.clientX,
startY: event.clientY,
rect: current.rect,
moved: false,
};
} else {
dragRef.current = { kind: "press", startX: event.clientX, startY: event.clientY };
@@ -245,7 +254,8 @@ export function CapturePicker() {
const dx = event.clientX - active.startX;
const dy = event.clientY - active.startY;
// A click inside a picked window keeps it; only real movement turns it into an area.
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD / 2) return;
if (!active.moved && Math.hypot(dx, dy) < DRAG_THRESHOLD / 2) return;
active.moved = true;
setDragging(true);
setSelection({
kind: "area",
+5 -1
View File
@@ -15,6 +15,7 @@ import {
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useRef } from "react";
import { Separator } from "@/components/ui/separator";
import { toast } from "@/components/ui/toast";
import { useScopedT } from "../../contexts/I18nContext";
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
@@ -188,7 +189,10 @@ function LaunchWindowContent() {
const handlePickOnScreen = async () => {
const result = await window.electronAPI.pickCaptureTarget();
if (!result.success) return;
if (!result.success) {
if (!result.canceled && result.message) toast.error(result.message);
return;
}
if (result.record) {
toggleRecording();
} else if (result.source) {