From 16eeb301a84f8e55a51c1f00adf86fc5ac5eceaf Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:15:22 +0100 Subject: [PATCH] Add dev mode screenshot capture warning (#1516) * auto-claude: subtask-1-1 - Add devMode optional flag to screenshot response type Add ScreenshotSourcesResponse interface with devMode flag to indicate when screenshot capture is unavailable due to running in development mode. Co-Authored-By: Claude Opus 4.5 * auto-claude: subtask-2-1 - Add app.isPackaged check to SCREENSHOT_GET_SOURCES - Import `app` from 'electron' for dev mode detection - Add `!app.isPackaged` check at start of SCREENSHOT_GET_SOURCES handler - Return `{ success: false, devMode: true }` when running in dev mode - This avoids triggering permission prompts in development builds on macOS - Add JSDoc comments explaining the dev mode behavior Co-Authored-By: Claude Opus 4.5 * auto-claude: subtask-3-1 - Add dev mode warning translations to English tasks * auto-claude: subtask-3-2 - Add dev mode warning translations to French tasks.json * auto-claude: subtask-4-1 - Add dev mode state and amber info message UI to ScreenshotCapture - Add isDevMode state and getPasteShortcut helper function to ScreenshotCapture component - Check result.devMode in fetchSources callback and set isDevMode state - Display amber info box with Info icon when in dev mode (not AlertCircle) - Include paste keyboard shortcut hint (Cmd+V / Ctrl+V based on platform) - Disable refresh and capture buttons when in dev mode - Exclude dev mode from showing loading, sources grid, and empty states - Update ScreenshotAPI and ElectronAPI types to include devMode property * fix: address code quality findings from PR review - Remove unused ScreenshotSourcesResponse interface - Move getPasteShortcut to module level for better performance - Use ScreenshotSource type instead of inline duplicate in ipc.ts - Remove redundant setIsLoading(false) before early return - Reset selectedSource at start of fetchSources for consistency Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .../main/ipc-handlers/screenshot-handlers.ts | 19 ++++++- .../src/preload/api/screenshot-api.ts | 2 + .../renderer/components/ScreenshotCapture.tsx | 53 +++++++++++++++---- .../src/shared/i18n/locales/en/tasks.json | 5 ++ .../src/shared/i18n/locales/fr/tasks.json | 5 ++ apps/frontend/src/shared/types/ipc.ts | 7 +-- apps/frontend/src/shared/types/screenshot.ts | 1 + 7 files changed, 77 insertions(+), 15 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/screenshot-handlers.ts b/apps/frontend/src/main/ipc-handlers/screenshot-handlers.ts index 5c7a4980..9451c539 100644 --- a/apps/frontend/src/main/ipc-handlers/screenshot-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/screenshot-handlers.ts @@ -3,8 +3,12 @@ * * Provides screenshot capture functionality using Electron's desktopCapturer API. * Users can capture screenshots of their entire screen or individual application windows. + * + * Note: Screenshot capture may not work in development mode (app.isPackaged === false) + * due to macOS screen recording permission requirements for unsigned builds. + * In dev mode, the handler returns a devMode flag so the UI can show a helpful message. */ -import { ipcMain } from 'electron'; +import { ipcMain, app } from 'electron'; import { desktopCapturer } from 'electron'; import { IPC_CHANNELS } from '../../shared/constants/ipc'; import type { ScreenshotSource, ScreenshotCaptureOptions } from '../../shared/types/screenshot'; @@ -15,8 +19,21 @@ import type { ScreenshotSource, ScreenshotCaptureOptions } from '../../shared/ty export function registerScreenshotHandlers(): void { /** * Get available screenshot sources (screens and windows) + * + * In development mode (app.isPackaged === false), returns devMode: true + * instead of attempting to get sources, as screen recording permissions + * typically aren't granted to unsigned development builds on macOS. */ ipcMain.handle(IPC_CHANNELS.SCREENSHOT_GET_SOURCES, async () => { + // Check if running in development mode + // Dev builds don't have screen recording permissions on macOS + if (!app.isPackaged) { + return { + success: false, + devMode: true + }; + } + try { const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], diff --git a/apps/frontend/src/preload/api/screenshot-api.ts b/apps/frontend/src/preload/api/screenshot-api.ts index 19e5c6a8..f62c1d20 100644 --- a/apps/frontend/src/preload/api/screenshot-api.ts +++ b/apps/frontend/src/preload/api/screenshot-api.ts @@ -16,6 +16,8 @@ export interface ScreenshotAPI { success: boolean; data?: ScreenshotSource[]; error?: string; + /** Indicates the app is running in development mode (screenshot capture unavailable) */ + devMode?: boolean; }>; capture: (options: ScreenshotCaptureOptions) => Promise<{ success: boolean; diff --git a/apps/frontend/src/renderer/components/ScreenshotCapture.tsx b/apps/frontend/src/renderer/components/ScreenshotCapture.tsx index a83c6889..cafdcbdd 100644 --- a/apps/frontend/src/renderer/components/ScreenshotCapture.tsx +++ b/apps/frontend/src/renderer/components/ScreenshotCapture.tsx @@ -13,7 +13,7 @@ */ import { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { Loader2, RefreshCw, Monitor, Frame, AlertCircle } from 'lucide-react'; +import { Loader2, RefreshCw, Monitor, Frame, AlertCircle, Info } from 'lucide-react'; import { Dialog, DialogContent, @@ -30,6 +30,14 @@ interface ScreenshotCaptureProps { onCapture: (imageData: string) => void; // base64 encoded PNG } +/** + * Get the appropriate paste keyboard shortcut based on platform + */ +const getPasteShortcut = (): string => { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + return isMac ? 'Cmd+V' : 'Ctrl+V'; +}; + export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotCaptureProps) { const { t } = useTranslation(['tasks', 'common']); const [sources, setSources] = useState([]); @@ -37,6 +45,7 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC const [isLoading, setIsLoading] = useState(false); const [isCapturing, setIsCapturing] = useState(false); const [error, setError] = useState(null); + const [isDevMode, setIsDevMode] = useState(false); /** * Fetch available screenshot sources @@ -44,11 +53,19 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC const fetchSources = useCallback(async () => { setIsLoading(true); setError(null); + setIsDevMode(false); + setSelectedSource(null); try { const result = await window.electronAPI.getSources(); + + // Check if running in dev mode (screenshot capture unavailable) + if (result.devMode) { + setIsDevMode(true); + return; + } + if (result.success && result.data) { setSources(result.data); - setSelectedSource(null); } else { setError(result.error || t('tasks:screenshot.errors.getSources')); } @@ -58,7 +75,7 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC } finally { setIsLoading(false); } - }, []); + }, [t]); // Fetch sources when dialog opens useEffect(() => { @@ -112,8 +129,26 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC
+ {/* Dev Mode Info State */} + {isDevMode && ( +
+ +
+

+ {t('tasks:screenshot.devMode.title')} +

+

+ {t('tasks:screenshot.devMode.description')} +

+

+ {t('tasks:screenshot.devMode.hint', { shortcut: getPasteShortcut() })} +

+
+
+ )} + {/* Error State */} - {error && ( + {error && !isDevMode && (
@@ -135,14 +170,14 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC )} {/* Loading State */} - {isLoading && sources.length === 0 && ( + {isLoading && sources.length === 0 && !isDevMode && (
)} {/* Sources Grid */} - {!isLoading && sources.length > 0 && ( + {!isLoading && !isDevMode && sources.length > 0 && (
{sources.map((source) => { const isSelected = selectedSource === source.id; @@ -230,7 +265,7 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC )} {/* Empty State */} - {!isLoading && sources.length === 0 && !error && ( + {!isLoading && !isDevMode && sources.length === 0 && !error && (

@@ -268,7 +303,7 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC variant="outline" size="icon" onClick={fetchSources} - disabled={isLoading || isCapturing} + disabled={isLoading || isCapturing || isDevMode} title={t('common:buttons.refresh')} > {isLoading ? ( @@ -279,7 +314,7 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC