mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user