fix: address code review feedback from PR #423

Addresses feedback from gemini-code-assist and coderabbitai reviewers:

## Duplicate Code (High Priority)
- Extract `getEffectiveDefaultEditor` logic into shared `useEffectiveDefaultEditor` hook
- Both account-section.tsx and worktree-actions-dropdown.tsx now use the shared hook

## Performance (Medium Priority)
- Refactor `detectAllEditors` to use `Promise.all` for parallel editor detection
- Replace sequential `await tryAddEditor()` calls with parallel `findEditor()` checks

## Code Quality (Medium Priority)
- Remove verbose IIFE pattern for editor icon rendering
- Pre-compute icon components before JSX return statement

## Bug Fixes
- Use `os.homedir()` instead of `~` fallback which doesn't expand in shell
- Normalize Select value to 'auto' when saved editor command not found in editors
- Add defensive check for empty editors array in useEffectiveDefaultEditor
- Improve mock openInEditor to correctly map all editor commands to display names

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Stefan de Vogelaere
2026-01-11 16:28:31 +01:00
parent 32656a9662
commit ac87594b5d
5 changed files with 102 additions and 71 deletions

View File

@@ -6,6 +6,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { homedir } from 'os';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -48,7 +49,7 @@ async function findMacApp(appName: string): Promise<string | null> {
// Check ~/Applications (used by JetBrains Toolbox and others) // Check ~/Applications (used by JetBrains Toolbox and others)
try { try {
const homeDir = process.env.HOME || '~'; const homeDir = homedir();
await execAsync(`test -d "${homeDir}/Applications/${appName}.app"`); await execAsync(`test -d "${homeDir}/Applications/${appName}.app"`);
return `${homeDir}/Applications/${appName}.app`; return `${homeDir}/Applications/${appName}.app`;
} catch { } catch {
@@ -57,18 +58,17 @@ async function findMacApp(appName: string): Promise<string | null> {
} }
/** /**
* Try to add an editor - checks CLI first, then macOS app bundle * Try to find an editor - checks CLI first, then macOS app bundle
* Returns EditorInfo if found, null otherwise
*/ */
async function tryAddEditor( async function findEditor(
editors: EditorInfo[],
name: string, name: string,
cliCommand: string, cliCommand: string,
macAppName: string macAppName: string
): Promise<void> { ): Promise<EditorInfo | null> {
// Try CLI command first // Try CLI command first
if (await commandExists(cliCommand)) { if (await commandExists(cliCommand)) {
editors.push({ name, command: cliCommand }); return { name, command: cliCommand };
return;
} }
// Try macOS app bundle (checks /Applications and ~/Applications) // Try macOS app bundle (checks /Applications and ~/Applications)
@@ -76,9 +76,11 @@ async function tryAddEditor(
const appPath = await findMacApp(macAppName); const appPath = await findMacApp(macAppName);
if (appPath) { if (appPath) {
// Use 'open -a' with full path for apps not in /Applications // Use 'open -a' with full path for apps not in /Applications
editors.push({ name, command: `open -a "${appPath}"` }); return { name, command: `open -a "${appPath}"` };
} }
} }
return null;
} }
async function detectAllEditors(): Promise<EditorInfo[]> { async function detectAllEditors(): Promise<EditorInfo[]> {
@@ -87,26 +89,29 @@ async function detectAllEditors(): Promise<EditorInfo[]> {
return cachedEditors; return cachedEditors;
} }
const editors: EditorInfo[] = [];
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
// Try editors (CLI command, then macOS app bundle) // Check all editors in parallel for better performance
await tryAddEditor(editors, 'Cursor', 'cursor', 'Cursor'); const editorChecks = [
await tryAddEditor(editors, 'VS Code', 'code', 'Visual Studio Code'); findEditor('Cursor', 'cursor', 'Cursor'),
await tryAddEditor(editors, 'Zed', 'zed', 'Zed'); findEditor('VS Code', 'code', 'Visual Studio Code'),
await tryAddEditor(editors, 'Sublime Text', 'subl', 'Sublime Text'); findEditor('Zed', 'zed', 'Zed'),
await tryAddEditor(editors, 'Windsurf', 'windsurf', 'Windsurf'); findEditor('Sublime Text', 'subl', 'Sublime Text'),
await tryAddEditor(editors, 'Trae', 'trae', 'Trae'); findEditor('Windsurf', 'windsurf', 'Windsurf'),
await tryAddEditor(editors, 'Rider', 'rider', 'Rider'); findEditor('Trae', 'trae', 'Trae'),
await tryAddEditor(editors, 'WebStorm', 'webstorm', 'WebStorm'); findEditor('Rider', 'rider', 'Rider'),
findEditor('WebStorm', 'webstorm', 'WebStorm'),
// Xcode (macOS only) - will return null on other platforms
isMac ? findEditor('Xcode', 'xed', 'Xcode') : Promise.resolve(null),
findEditor('Android Studio', 'studio', 'Android Studio'),
findEditor('Antigravity', 'agy', 'Antigravity'),
];
// Xcode (macOS only) // Wait for all checks to complete in parallel
if (isMac) { const results = await Promise.all(editorChecks);
await tryAddEditor(editors, 'Xcode', 'xed', 'Xcode');
}
await tryAddEditor(editors, 'Android Studio', 'studio', 'Android Studio'); // Filter out null results (editors not found)
await tryAddEditor(editors, 'Antigravity', 'agy', 'Antigravity'); const editors = results.filter((e): e is EditorInfo => e !== null);
// Always add file manager as fallback // Always add file manager as fallback
const platform = process.platform; const platform = process.platform;

View File

@@ -28,9 +28,8 @@ import {
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types'; import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
import { TooltipWrapper } from './tooltip-wrapper'; import { TooltipWrapper } from './tooltip-wrapper';
import { useAvailableEditors } from '../hooks/use-available-editors'; import { useAvailableEditors, useEffectiveDefaultEditor } from '../hooks/use-available-editors';
import { getEditorIcon } from '@/components/icons/editor-icons'; import { getEditorIcon } from '@/components/icons/editor-icons';
import { useAppStore } from '@/store/app-store';
interface WorktreeActionsDropdownProps { interface WorktreeActionsDropdownProps {
worktree: WorktreeInfo; worktree: WorktreeInfo;
@@ -84,30 +83,19 @@ export function WorktreeActionsDropdown({
onOpenDevServerUrl, onOpenDevServerUrl,
}: WorktreeActionsDropdownProps) { }: WorktreeActionsDropdownProps) {
// Get available editors for the "Open In" submenu // Get available editors for the "Open In" submenu
const { editors, hasMultipleEditors } = useAvailableEditors(); const { editors } = useAvailableEditors();
// Get the user's preferred default editor from settings // Use shared hook for effective default editor
const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand); const effectiveDefaultEditor = useEffectiveDefaultEditor(editors);
// Calculate effective default editor based on user setting or auto-detect (Cursor > VS Code > first)
const getEffectiveDefaultEditor = () => {
if (defaultEditorCommand) {
const found = editors.find((e) => e.command === defaultEditorCommand);
if (found) return found;
}
// Auto-detect: prefer Cursor, then VS Code, then first available
const cursor = editors.find((e) => e.command === 'cursor');
if (cursor) return cursor;
const vscode = editors.find((e) => e.command === 'code');
if (vscode) return vscode;
return editors[0];
};
const effectiveDefaultEditor = getEffectiveDefaultEditor();
// Get other editors (excluding the default) for the submenu // Get other editors (excluding the default) for the submenu
const otherEditors = editors.filter((e) => e.command !== effectiveDefaultEditor?.command); const otherEditors = editors.filter((e) => e.command !== effectiveDefaultEditor?.command);
// Get icon component for the effective editor (avoid IIFE in JSX)
const DefaultEditorIcon = effectiveDefaultEditor
? getEditorIcon(effectiveDefaultEditor.command)
: null;
// Check if there's a PR associated with this worktree from stored metadata // Check if there's a PR associated with this worktree from stored metadata
const hasPR = !!worktree.pr; const hasPR = !!worktree.pr;
@@ -240,10 +228,7 @@ export function WorktreeActionsDropdown({
onClick={() => onOpenInEditor(worktree, effectiveDefaultEditor.command)} onClick={() => onOpenInEditor(worktree, effectiveDefaultEditor.command)}
className="text-xs flex-1 pr-0 rounded-r-none" className="text-xs flex-1 pr-0 rounded-r-none"
> >
{(() => { {DefaultEditorIcon && <DefaultEditorIcon className="w-3.5 h-3.5 mr-2" />}
const EditorIcon = getEditorIcon(effectiveDefaultEditor.command);
return <EditorIcon className="w-3.5 h-3.5 mr-2" />;
})()}
Open in {effectiveDefaultEditor.name} Open in {effectiveDefaultEditor.name}
</DropdownMenuItem> </DropdownMenuItem>
{/* Chevron trigger for submenu with other editors and Copy Path */} {/* Chevron trigger for submenu with other editors and Copy Path */}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger'; import { createLogger } from '@automaker/utils/logger';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
const logger = createLogger('AvailableEditors'); const logger = createLogger('AvailableEditors');
@@ -44,3 +45,30 @@ export function useAvailableEditors() {
defaultEditor: editors[0] ?? null, defaultEditor: editors[0] ?? null,
}; };
} }
/**
* Hook to get the effective default editor based on user settings
* Falls back to: Cursor > VS Code > first available editor
*/
export function useEffectiveDefaultEditor(editors: EditorInfo[]): EditorInfo | null {
const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand);
return useMemo(() => {
if (editors.length === 0) return null;
// If user has a saved preference and it exists in available editors, use it
if (defaultEditorCommand) {
const found = editors.find((e) => e.command === defaultEditorCommand);
if (found) return found;
}
// Auto-detect: prefer Cursor, then VS Code, then first available
const cursor = editors.find((e) => e.command === 'cursor');
if (cursor) return cursor;
const vscode = editors.find((e) => e.command === 'code');
if (vscode) return vscode;
return editors[0];
}, [editors, defaultEditorCommand]);
}

View File

@@ -13,7 +13,10 @@ import { cn } from '@/lib/utils';
import { logout } from '@/lib/http-api-client'; import { logout } from '@/lib/http-api-client';
import { useAuthStore } from '@/store/auth-store'; import { useAuthStore } from '@/store/auth-store';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { useAvailableEditors } from '@/components/views/board-view/worktree-panel/hooks/use-available-editors'; import {
useAvailableEditors,
useEffectiveDefaultEditor,
} from '@/components/views/board-view/worktree-panel/hooks/use-available-editors';
import { getEditorIcon } from '@/components/icons/editor-icons'; import { getEditorIcon } from '@/components/icons/editor-icons';
export function AccountSection() { export function AccountSection() {
@@ -25,20 +28,16 @@ export function AccountSection() {
const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand); const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand);
const setDefaultEditorCommand = useAppStore((s) => s.setDefaultEditorCommand); const setDefaultEditorCommand = useAppStore((s) => s.setDefaultEditorCommand);
// Get effective default editor (respecting auto-detect order: Cursor > VS Code > first) // Use shared hook for effective default editor
const getEffectiveDefaultEditor = () => { const effectiveEditor = useEffectiveDefaultEditor(editors);
if (defaultEditorCommand) {
return editors.find((e) => e.command === defaultEditorCommand) ?? editors[0];
}
// Auto-detect: prefer Cursor, then VS Code, then first available
const cursor = editors.find((e) => e.command === 'cursor');
if (cursor) return cursor;
const vscode = editors.find((e) => e.command === 'code');
if (vscode) return vscode;
return editors[0];
};
const effectiveEditor = getEffectiveDefaultEditor(); // Normalize Select value: if saved editor isn't found, show 'auto'
const hasSavedEditor =
!!defaultEditorCommand && editors.some((e) => e.command === defaultEditorCommand);
const selectValue = hasSavedEditor ? defaultEditorCommand : 'auto';
// Get icon component for the effective editor
const EffectiveEditorIcon = effectiveEditor ? getEditorIcon(effectiveEditor.command) : null;
const handleLogout = async () => { const handleLogout = async () => {
setIsLoggingOut(true); setIsLoggingOut(true);
@@ -87,7 +86,7 @@ export function AccountSection() {
</div> </div>
</div> </div>
<Select <Select
value={defaultEditorCommand ?? 'auto'} value={selectValue}
onValueChange={(value) => setDefaultEditorCommand(value === 'auto' ? null : value)} onValueChange={(value) => setDefaultEditorCommand(value === 'auto' ? null : value)}
disabled={isLoadingEditors || editors.length === 0} disabled={isLoadingEditors || editors.length === 0}
> >
@@ -95,12 +94,9 @@ export function AccountSection() {
<SelectValue placeholder="Select editor"> <SelectValue placeholder="Select editor">
{effectiveEditor ? ( {effectiveEditor ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{(() => { {EffectiveEditorIcon && <EffectiveEditorIcon className="w-4 h-4" />}
const Icon = getEditorIcon(effectiveEditor.command);
return <Icon className="w-4 h-4" />;
})()}
{effectiveEditor.name} {effectiveEditor.name}
{!defaultEditorCommand && ( {selectValue === 'auto' && (
<span className="text-muted-foreground text-xs">(Auto)</span> <span className="text-muted-foreground text-xs">(Auto)</span>
)} )}
</span> </span>

View File

@@ -1626,7 +1626,24 @@ function createMockWorktreeAPI(): WorktreeAPI {
}, },
openInEditor: async (worktreePath: string, editorCommand?: string) => { openInEditor: async (worktreePath: string, editorCommand?: string) => {
const editorName = editorCommand === 'cursor' ? 'Cursor' : 'VS Code'; // Map editor commands to display names
const editorNameMap: Record<string, string> = {
cursor: 'Cursor',
code: 'VS Code',
zed: 'Zed',
subl: 'Sublime Text',
windsurf: 'Windsurf',
trae: 'Trae',
rider: 'Rider',
webstorm: 'WebStorm',
xed: 'Xcode',
studio: 'Android Studio',
agy: 'Antigravity',
open: 'Finder',
explorer: 'Explorer',
'xdg-open': 'File Manager',
};
const editorName = editorCommand ? (editorNameMap[editorCommand] ?? 'Editor') : 'VS Code';
console.log('[Mock] Opening in editor:', worktreePath, 'using:', editorName); console.log('[Mock] Opening in editor:', worktreePath, 'using:', editorName);
return { return {
success: true, success: true,