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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ScreenshotSource[]>([]);
|
||||
@@ -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<string | null>(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
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Dev Mode Info State */}
|
||||
{isDevMode && (
|
||||
<div className="flex items-start gap-3 p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg mb-4">
|
||||
<Info className="h-5 w-5 text-amber-600 dark:text-amber-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-400">
|
||||
{t('tasks:screenshot.devMode.title')}
|
||||
</p>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-500">
|
||||
{t('tasks:screenshot.devMode.description')}
|
||||
</p>
|
||||
<p className="text-sm text-amber-600 dark:text-amber-500">
|
||||
{t('tasks:screenshot.devMode.hint', { shortcut: getPasteShortcut() })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
{error && !isDevMode && (
|
||||
<div className="flex items-center gap-3 p-4 bg-destructive/10 border border-destructive/30 rounded-lg mb-4">
|
||||
<AlertCircle className="h-5 w-5 text-destructive flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
@@ -135,14 +170,14 @@ export function ScreenshotCapture({ open, onOpenChange, onCapture }: ScreenshotC
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && sources.length === 0 && (
|
||||
{isLoading && sources.length === 0 && !isDevMode && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sources Grid */}
|
||||
{!isLoading && sources.length > 0 && (
|
||||
{!isLoading && !isDevMode && sources.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-1">
|
||||
{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 && (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<Monitor className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
@@ -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
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCapture}
|
||||
disabled={!selectedSource || isCapturing}
|
||||
disabled={!selectedSource || isCapturing || isDevMode}
|
||||
>
|
||||
{isCapturing ? (
|
||||
<>
|
||||
|
||||
@@ -310,6 +310,11 @@
|
||||
"fetchSources": "Failed to fetch screenshot sources",
|
||||
"capture": "Failed to capture screenshot",
|
||||
"captureFailed": "Failed to capture screenshot"
|
||||
},
|
||||
"devMode": {
|
||||
"title": "Screenshot capture unavailable",
|
||||
"description": "Screen capture is not available in development mode due to system permission restrictions.",
|
||||
"hint": "Use an external screenshot tool and paste directly into the task description with {{shortcut}}."
|
||||
}
|
||||
},
|
||||
"referenceImages": {
|
||||
|
||||
@@ -309,6 +309,11 @@
|
||||
"fetchSources": "Échec de la récupération des sources de capture d'écran",
|
||||
"capture": "Échec de la capture d'écran",
|
||||
"captureFailed": "Échec de la capture d'écran"
|
||||
},
|
||||
"devMode": {
|
||||
"title": "Capture d'écran non disponible",
|
||||
"description": "La capture d'écran n'est pas disponible en mode développement en raison des restrictions de permissions système.",
|
||||
"hint": "Utilisez un outil de capture d'écran externe et collez directement dans la description de la tâche avec {{shortcut}}."
|
||||
}
|
||||
},
|
||||
"referenceImages": {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
McpHealthCheckResult,
|
||||
McpTestConnectionResult
|
||||
} from './project';
|
||||
import type { ScreenshotSource } from './screenshot';
|
||||
import type {
|
||||
Task,
|
||||
TaskStatus,
|
||||
@@ -860,11 +861,7 @@ export interface ElectronAPI {
|
||||
testMcpConnection: (server: CustomMcpServer) => Promise<IPCResult<McpTestConnectionResult>>;
|
||||
|
||||
// Screenshot capture operations
|
||||
getSources: () => Promise<IPCResult<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnail: string;
|
||||
}>>>;
|
||||
getSources: () => Promise<IPCResult<ScreenshotSource[]> & { devMode?: boolean }>;
|
||||
capture: (options: { sourceId: string }) => Promise<IPCResult<string>>;
|
||||
|
||||
// Queue Routing API (rate limit recovery)
|
||||
|
||||
@@ -23,3 +23,4 @@ export interface ScreenshotCaptureOptions {
|
||||
/** The ID of the source to capture */
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user