-
-
Notifications
You must be signed in to change notification settings - Fork 819
feat: Add IDE opening functionality to Worktrees UI #369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -299,4 +299,88 @@ export function registerSettingsHandlers( | |||||||||||||||||||||||||||||||
| await shell.openExternal(url); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ipcMain.handle( | ||||||||||||||||||||||||||||||||
| IPC_CHANNELS.SHELL_OPEN_PATH, | ||||||||||||||||||||||||||||||||
| async (_, folderPath: string): Promise<void> => { | ||||||||||||||||||||||||||||||||
| await shell.openPath(folderPath); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ipcMain.handle( | ||||||||||||||||||||||||||||||||
| IPC_CHANNELS.SHELL_OPEN_IN_IDE, | ||||||||||||||||||||||||||||||||
| async (_, folderPath: string, ide?: 'cursor' | 'vscode' | 'finder'): Promise<IPCResult> => { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| // Validate path exists | ||||||||||||||||||||||||||||||||
| if (!existsSync(folderPath)) { | ||||||||||||||||||||||||||||||||
| return { success: false, error: `Path does not exist: ${folderPath}` }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Determine which IDE to use | ||||||||||||||||||||||||||||||||
| const ideToUse = ide || 'cursor'; // Default to Cursor | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (ideToUse === 'finder') { | ||||||||||||||||||||||||||||||||
| // Open in Finder (macOS) or Explorer (Windows) | ||||||||||||||||||||||||||||||||
| await shell.openPath(folderPath); | ||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Try to open in the specified IDE | ||||||||||||||||||||||||||||||||
| const commands: Record<string, string[]> = { | ||||||||||||||||||||||||||||||||
| cursor: ['cursor', '.'], | ||||||||||||||||||||||||||||||||
| vscode: ['code', '.'] | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const [cmd, ...args] = commands[ideToUse] || commands.cursor; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| // Use execSync to run the command - this opens the IDE | ||||||||||||||||||||||||||||||||
| execSync(`${cmd} ${args.join(' ')}`, { | ||||||||||||||||||||||||||||||||
| cwd: folderPath, | ||||||||||||||||||||||||||||||||
| stdio: 'ignore', | ||||||||||||||||||||||||||||||||
| timeout: 5000 | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+336
to
+343
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Command injection vulnerability. Using string interpolation with 🔎 Proposed fixUse an array-based approach or properly escape arguments: try {
- // Use execSync to run the command - this opens the IDE
- execSync(`${cmd} ${args.join(' ')}`, {
+ // Use spawn for safer command execution
+ const { spawnSync } = require('child_process');
+ const result = spawnSync(cmd, [...args, folderPath], {
- cwd: folderPath,
stdio: 'ignore',
timeout: 5000
});
+
+ if (result.error) {
+ throw result.error;
+ }
+
return { success: true };Alternatively, if you must use + const { execSync } = require('child_process');
+ // Escape the folder path to prevent command injection
+ const escapedPath = folderPath.replace(/"/g, '\\"');
+
- execSync(`${cmd} ${args.join(' ')}`, {
- cwd: folderPath,
+ execSync(`${cmd} "${escapedPath}"`, {
stdio: 'ignore',
timeout: 5000
});But
|
||||||||||||||||||||||||||||||||
| } catch (execError) { | ||||||||||||||||||||||||||||||||
| // If the command fails, try alternative approaches | ||||||||||||||||||||||||||||||||
| console.warn(`[SHELL_OPEN_IN_IDE] ${cmd} command failed:`, execError); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // On macOS, try using 'open' command with the app | ||||||||||||||||||||||||||||||||
| if (process.platform === 'darwin') { | ||||||||||||||||||||||||||||||||
| const appNames: Record<string, string[]> = { | ||||||||||||||||||||||||||||||||
| cursor: ['Cursor', 'Cursor.app'], | ||||||||||||||||||||||||||||||||
| vscode: ['Visual Studio Code', 'Visual Studio Code.app', 'VSCode', 'Code'] | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const apps = appNames[ideToUse] || appNames.cursor; | ||||||||||||||||||||||||||||||||
| for (const appName of apps) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| execSync(`open -a "${appName}" "${folderPath}"`, { | ||||||||||||||||||||||||||||||||
| stdio: 'ignore', | ||||||||||||||||||||||||||||||||
| timeout: 5000 | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||
| // Try next app name | ||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+356
to
+367
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Command injection vulnerability in macOS fallback. The same command injection vulnerability exists in the macOS fallback path. The 🔎 Proposed fix const apps = appNames[ideToUse] || appNames.cursor;
for (const appName of apps) {
try {
- execSync(`open -a "${appName}" "${folderPath}"`, {
+ const { spawnSync } = require('child_process');
+ const result = spawnSync('open', ['-a', appName, folderPath], {
stdio: 'ignore',
timeout: 5000
});
+
+ if (result.error) {
+ continue;
+ }
+
return { success: true };
} catch {
// Try next app name
continue;
}
}
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Fallback: open in Finder/Explorer | ||||||||||||||||||||||||||||||||
| console.warn(`[SHELL_OPEN_IN_IDE] Falling back to system file browser`); | ||||||||||||||||||||||||||||||||
| await shell.openPath(folderPath); | ||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||
| error: `${ideToUse} not found. Opened in file browser instead.` | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+370
to
+377
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Clarify the success/error return pattern. Returning 🔎 Proposed refactor // Fallback: open in Finder/Explorer
console.warn(`[SHELL_OPEN_IN_IDE] Falling back to system file browser`);
await shell.openPath(folderPath);
return {
success: true,
- error: `${ideToUse} not found. Opened in file browser instead.`
+ data: {
+ warning: `${ideToUse} not found. Opened in file browser instead.`
+ }
};Or if you want to treat it as a partial failure: // Fallback: open in Finder/Explorer
console.warn(`[SHELL_OPEN_IN_IDE] Falling back to system file browser`);
await shell.openPath(folderPath);
return {
- success: true,
+ success: false,
error: `${ideToUse} not found. Opened in file browser instead.`
};📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||
| success: false, | ||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : 'Failed to open in IDE' | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,28 @@ | ||
| import { IPC_CHANNELS } from '../../../shared/constants'; | ||
| import { invokeIpc } from './ipc-utils'; | ||
|
|
||
| /** | ||
| * IDE types supported for opening folders | ||
| */ | ||
| export type IDEType = 'cursor' | 'vscode' | 'finder'; | ||
|
|
||
| /** | ||
| * Shell Operations API | ||
| */ | ||
| export interface ShellAPI { | ||
| openExternal: (url: string) => Promise<void>; | ||
| openPath: (path: string) => Promise<void>; | ||
| openInIde: (path: string, ide?: IDEType) => Promise<{ success: boolean; error?: string }>; | ||
| } | ||
|
|
||
| /** | ||
| * Creates the Shell Operations API implementation | ||
| */ | ||
| export const createShellAPI = (): ShellAPI => ({ | ||
| openExternal: (url: string): Promise<void> => | ||
| invokeIpc(IPC_CHANNELS.SHELL_OPEN_EXTERNAL, url) | ||
| invokeIpc(IPC_CHANNELS.SHELL_OPEN_EXTERNAL, url), | ||
| openPath: (path: string): Promise<void> => | ||
| invokeIpc(IPC_CHANNELS.SHELL_OPEN_PATH, path), | ||
| openInIde: (path: string, ide?: IDEType): Promise<{ success: boolean; error?: string }> => | ||
| invokeIpc(IPC_CHANNELS.SHELL_OPEN_IN_IDE, path, ide) | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,9 @@ import { | |
| Minus, | ||
| ChevronRight, | ||
| Check, | ||
| X | ||
| X, | ||
| Code2, | ||
| ExternalLink | ||
| } from 'lucide-react'; | ||
| import { Button } from './ui/button'; | ||
| import { Badge } from './ui/badge'; | ||
|
|
@@ -305,13 +307,41 @@ export function Worktrees({ projectId }: WorktreesProps) { | |
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => { | ||
| // Copy worktree path to clipboard | ||
| navigator.clipboard.writeText(worktree.path); | ||
| onClick={async () => { | ||
| // Open in Cursor (default IDE) | ||
| const result = await window.electronAPI.openInIde(worktree.path, 'cursor'); | ||
| if (!result.success) { | ||
| setError(result.error || 'Failed to open in Cursor'); | ||
| } | ||
| }} | ||
| > | ||
| <Code2 className="h-3.5 w-3.5 mr-1.5" /> | ||
| Open in Cursor | ||
| </Button> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={async () => { | ||
| // Open in VS Code | ||
| const result = await window.electronAPI.openInIde(worktree.path, 'vscode'); | ||
| if (!result.success) { | ||
| setError(result.error || 'Failed to open in VS Code'); | ||
| } | ||
| }} | ||
| > | ||
| <ExternalLink className="h-3.5 w-3.5 mr-1.5" /> | ||
| VS Code | ||
| </Button> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={async () => { | ||
| // Open in Finder | ||
| await window.electronAPI.openInIde(worktree.path, 'finder'); | ||
| }} | ||
| > | ||
| <FolderOpen className="h-3.5 w-3.5 mr-1.5" /> | ||
| Copy Path | ||
| Finder | ||
| </Button> | ||
|
Comment on lines
+310
to
345
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace hardcoded strings with translation keys. All user-facing button text must use translation keys as per coding guidelines. Additionally, the Finder button lacks error handling that the other IDE buttons have. 🔎 Proposed fixesFirst, ensure you have translation keys defined in {
"worktrees": {
"actions": {
"openInCursor": "Open in Cursor",
"openInVSCode": "VS Code",
"openInFinder": "Finder",
"failedToOpen": "Failed to open in {{ide}}"
}
}
}Then update the component: +import { useTranslation } from 'react-i18next';
+
export function Worktrees({ projectId }: WorktreesProps) {
+ const { t } = useTranslation();
const projects = useProjectStore((state) => state.projects);
// ... rest of component
// In the JSX:
<Button
variant="outline"
size="sm"
onClick={async () => {
- // Open in Cursor (default IDE)
const result = await window.electronAPI.openInIde(worktree.path, 'cursor');
if (!result.success) {
- setError(result.error || 'Failed to open in Cursor');
+ setError(result.error || t('worktrees:actions.failedToOpen', { ide: 'Cursor' }));
}
}}
>
<Code2 className="h-3.5 w-3.5 mr-1.5" />
- Open in Cursor
+ {t('worktrees:actions.openInCursor')}
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
- // Open in VS Code
const result = await window.electronAPI.openInIde(worktree.path, 'vscode');
if (!result.success) {
- setError(result.error || 'Failed to open in VS Code');
+ setError(result.error || t('worktrees:actions.failedToOpen', { ide: 'VS Code' }));
}
}}
>
<ExternalLink className="h-3.5 w-3.5 mr-1.5" />
- VS Code
+ {t('worktrees:actions.openInVSCode')}
</Button>
<Button
variant="outline"
size="sm"
onClick={async () => {
- // Open in Finder
- await window.electronAPI.openInIde(worktree.path, 'finder');
+ const result = await window.electronAPI.openInIde(worktree.path, 'finder');
+ if (!result.success) {
+ setError(result.error || t('worktrees:actions.failedToOpen', { ide: 'Finder' }));
+ }
}}
>
<FolderOpen className="h-3.5 w-3.5 mr-1.5" />
- Finder
+ {t('worktrees:actions.openInFinder')}
</Button>As per coding guidelines: "Always use translation keys with useTranslation() for all user-facing text in React/TypeScript frontend components - use format namespace:section.key (e.g., navigation:items.githubPRs). Never use hardcoded strings in JSX/TSX files for user-facing text."
|
||
| <Button | ||
| variant="outline" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling and path validation.
The
SHELL_OPEN_PATHhandler lacks error handling and path validation, making it inconsistent with other IPC handlers and potentially unsafe.🔎 Proposed fix
ipcMain.handle( IPC_CHANNELS.SHELL_OPEN_PATH, - async (_, folderPath: string): Promise<void> => { - await shell.openPath(folderPath); + async (_, folderPath: string): Promise<IPCResult> => { + try { + // Validate path exists + if (!existsSync(folderPath)) { + return { success: false, error: `Path does not exist: ${folderPath}` }; + } + + const result = await shell.openPath(folderPath); + + // shell.openPath returns a string with an error message if it fails + if (result) { + return { success: false, error: result }; + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to open path' + }; + } } );🤖 Prompt for AI Agents