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:
Andy
2026-01-26 13:15:22 +01:00
committed by GitHub
parent 1e453653b2
commit 16eeb301a8
7 changed files with 77 additions and 15 deletions
@@ -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": {
+2 -5
View File
@@ -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;
}