diff --git a/apps/server/src/routes/worktree/index.ts b/apps/server/src/routes/worktree/index.ts index 7fef5c6e..6d64ad3a 100644 --- a/apps/server/src/routes/worktree/index.ts +++ b/apps/server/src/routes/worktree/index.ts @@ -24,6 +24,7 @@ import { createSwitchBranchHandler } from './routes/switch-branch.js'; import { createOpenInEditorHandler, createGetDefaultEditorHandler, + createGetAvailableEditorsHandler, } from './routes/open-in-editor.js'; import { createInitGitHandler } from './routes/init-git.js'; import { createMigrateHandler } from './routes/migrate.js'; @@ -77,6 +78,7 @@ export function createWorktreeRoutes(): Router { router.post('/switch-branch', requireValidWorktree, createSwitchBranchHandler()); router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler()); router.get('/default-editor', createGetDefaultEditorHandler()); + router.get('/available-editors', createGetAvailableEditorsHandler()); router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler()); router.post('/migrate', createMigrateHandler()); router.post( diff --git a/apps/server/src/routes/worktree/routes/open-in-editor.ts b/apps/server/src/routes/worktree/routes/open-in-editor.ts index 40e71b00..492ac85a 100644 --- a/apps/server/src/routes/worktree/routes/open-in-editor.ts +++ b/apps/server/src/routes/worktree/routes/open-in-editor.ts @@ -17,9 +17,113 @@ interface EditorInfo { } let cachedEditor: EditorInfo | null = null; +let cachedEditors: EditorInfo[] | null = null; /** - * Detect which code editor is available on the system + * Check if a CLI command exists in PATH + */ +async function commandExists(cmd: string): Promise { + try { + await execAsync(process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`); + return true; + } catch { + return false; + } +} + +/** + * Check if a macOS app bundle exists and return the path if found + * Checks both /Applications and ~/Applications + */ +async function findMacApp(appName: string): Promise { + if (process.platform !== 'darwin') return null; + + // Check /Applications first + try { + await execAsync(`test -d "/Applications/${appName}.app"`); + return `/Applications/${appName}.app`; + } catch { + // Not in /Applications + } + + // Check ~/Applications (used by JetBrains Toolbox and others) + try { + const homeDir = process.env.HOME || '~'; + await execAsync(`test -d "${homeDir}/Applications/${appName}.app"`); + return `${homeDir}/Applications/${appName}.app`; + } catch { + return null; + } +} + +/** + * Try to add an editor - checks CLI first, then macOS app bundle + */ +async function tryAddEditor( + editors: EditorInfo[], + name: string, + cliCommand: string, + macAppName: string +): Promise { + // Try CLI command first + if (await commandExists(cliCommand)) { + editors.push({ name, command: cliCommand }); + return; + } + + // Try macOS app bundle (checks /Applications and ~/Applications) + if (process.platform === 'darwin') { + const appPath = await findMacApp(macAppName); + if (appPath) { + // Use 'open -a' with full path for apps not in /Applications + editors.push({ name, command: `open -a "${appPath}"` }); + } + } +} + +async function detectAllEditors(): Promise { + // Return cached result if available + if (cachedEditors) { + return cachedEditors; + } + + const editors: EditorInfo[] = []; + const isMac = process.platform === 'darwin'; + + // Try editors (CLI command, then macOS app bundle) + await tryAddEditor(editors, 'Cursor', 'cursor', 'Cursor'); + await tryAddEditor(editors, 'VS Code', 'code', 'Visual Studio Code'); + await tryAddEditor(editors, 'Zed', 'zed', 'Zed'); + await tryAddEditor(editors, 'Sublime Text', 'subl', 'Sublime Text'); + await tryAddEditor(editors, 'Windsurf', 'windsurf', 'Windsurf'); + await tryAddEditor(editors, 'Trae', 'trae', 'Trae'); + await tryAddEditor(editors, 'Rider', 'rider', 'Rider'); + await tryAddEditor(editors, 'WebStorm', 'webstorm', 'WebStorm'); + + // Xcode (macOS only) + if (isMac) { + await tryAddEditor(editors, 'Xcode', 'xed', 'Xcode'); + } + + await tryAddEditor(editors, 'Android Studio', 'studio', 'Android Studio'); + await tryAddEditor(editors, 'Antigravity', 'agy', 'Antigravity'); + + // Always add file manager as fallback + const platform = process.platform; + if (platform === 'darwin') { + editors.push({ name: 'Finder', command: 'open' }); + } else if (platform === 'win32') { + editors.push({ name: 'Explorer', command: 'explorer' }); + } else { + editors.push({ name: 'File Manager', command: 'xdg-open' }); + } + + cachedEditors = editors; + return editors; +} + +/** + * Detect the default (first available) code editor on the system */ async function detectDefaultEditor(): Promise { // Return cached result if available @@ -27,54 +131,29 @@ async function detectDefaultEditor(): Promise { return cachedEditor; } - // Try Cursor first (if user has Cursor, they probably prefer it) - try { - await execAsync('which cursor || where cursor'); - cachedEditor = { name: 'Cursor', command: 'cursor' }; - return cachedEditor; - } catch { - // Cursor not found - } - - // Try VS Code - try { - await execAsync('which code || where code'); - cachedEditor = { name: 'VS Code', command: 'code' }; - return cachedEditor; - } catch { - // VS Code not found - } - - // Try Zed - try { - await execAsync('which zed || where zed'); - cachedEditor = { name: 'Zed', command: 'zed' }; - return cachedEditor; - } catch { - // Zed not found - } - - // Try Sublime Text - try { - await execAsync('which subl || where subl'); - cachedEditor = { name: 'Sublime Text', command: 'subl' }; - return cachedEditor; - } catch { - // Sublime not found - } - - // Fallback to file manager - const platform = process.platform; - if (platform === 'darwin') { - cachedEditor = { name: 'Finder', command: 'open' }; - } else if (platform === 'win32') { - cachedEditor = { name: 'Explorer', command: 'explorer' }; - } else { - cachedEditor = { name: 'File Manager', command: 'xdg-open' }; - } + // Get all editors and return the first one (highest priority) + const editors = await detectAllEditors(); + cachedEditor = editors[0]; return cachedEditor; } +export function createGetAvailableEditorsHandler() { + return async (_req: Request, res: Response): Promise => { + try { + const editors = await detectAllEditors(); + res.json({ + success: true, + result: { + editors, + }, + }); + } catch (error) { + logError(error, 'Get available editors failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} + export function createGetDefaultEditorHandler() { return async (_req: Request, res: Response): Promise => { try { @@ -96,8 +175,9 @@ export function createGetDefaultEditorHandler() { export function createOpenInEditorHandler() { return async (req: Request, res: Response): Promise => { try { - const { worktreePath } = req.body as { + const { worktreePath, editorCommand } = req.body as { worktreePath: string; + editorCommand?: string; }; if (!worktreePath) { @@ -108,7 +188,16 @@ export function createOpenInEditorHandler() { return; } - const editor = await detectDefaultEditor(); + // Use specified editor command or detect default + let editor: EditorInfo; + if (editorCommand) { + // Find the editor info from the available editors list + const allEditors = await detectAllEditors(); + const specifiedEditor = allEditors.find((e) => e.command === editorCommand); + editor = specifiedEditor ?? (await detectDefaultEditor()); + } else { + editor = await detectDefaultEditor(); + } try { await execAsync(`${editor.command} "${worktreePath}"`); diff --git a/apps/ui/src/components/icons/editor-icons.tsx b/apps/ui/src/components/icons/editor-icons.tsx new file mode 100644 index 00000000..5e3cd092 --- /dev/null +++ b/apps/ui/src/components/icons/editor-icons.tsx @@ -0,0 +1,193 @@ +import type { ComponentType, ComponentProps } from 'react'; +import { FolderOpen } from 'lucide-react'; + +type IconProps = ComponentProps<'svg'>; +type IconComponent = ComponentType; + +/** + * Cursor editor logo icon - from LobeHub icons + */ +export function CursorIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * VS Code editor logo icon + */ +export function VSCodeIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Zed editor logo icon (from Simple Icons) + */ +export function ZedIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Sublime Text editor logo icon + */ +export function SublimeTextIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * macOS Finder icon + */ +export function FinderIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Windsurf editor logo icon (by Codeium) - from LobeHub icons + */ +export function WindsurfIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Trae editor logo icon (by ByteDance) - from LobeHub icons + */ +export function TraeIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * JetBrains Rider logo icon + */ +export function RiderIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * JetBrains WebStorm logo icon + */ +export function WebStormIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Xcode logo icon + */ +export function XcodeIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Android Studio logo icon + */ +export function AndroidStudioIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Google Antigravity IDE logo icon - stylized "A" arch shape + */ +export function AntigravityIcon(props: IconProps) { + return ( + + + + ); +} + +/** + * Get the appropriate icon component for an editor command + */ +export function getEditorIcon(command: string): IconComponent { + // Handle direct CLI commands + const cliIcons: Record = { + cursor: CursorIcon, + code: VSCodeIcon, + zed: ZedIcon, + subl: SublimeTextIcon, + windsurf: WindsurfIcon, + trae: TraeIcon, + rider: RiderIcon, + webstorm: WebStormIcon, + xed: XcodeIcon, + studio: AndroidStudioIcon, + agy: AntigravityIcon, + open: FinderIcon, + explorer: FolderOpen, + 'xdg-open': FolderOpen, + }; + + // Check direct match first + if (cliIcons[command]) { + return cliIcons[command]; + } + + // Handle 'open' commands (macOS) - both 'open -a AppName' and 'open "/path/to/App.app"' + if (command.startsWith('open')) { + const cmdLower = command.toLowerCase(); + if (cmdLower.includes('cursor')) return CursorIcon; + if (cmdLower.includes('visual studio code')) return VSCodeIcon; + if (cmdLower.includes('zed')) return ZedIcon; + if (cmdLower.includes('sublime')) return SublimeTextIcon; + if (cmdLower.includes('windsurf')) return WindsurfIcon; + if (cmdLower.includes('trae')) return TraeIcon; + if (cmdLower.includes('rider')) return RiderIcon; + if (cmdLower.includes('webstorm')) return WebStormIcon; + if (cmdLower.includes('xcode')) return XcodeIcon; + if (cmdLower.includes('android studio')) return AndroidStudioIcon; + if (cmdLower.includes('antigravity')) return AntigravityIcon; + // If just 'open' without app name, it's Finder + if (command === 'open') return FinderIcon; + } + + return FolderOpen; +} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx index c6542256..af7acd67 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-actions-dropdown.tsx @@ -6,13 +6,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuLabel, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, } from '@/components/ui/dropdown-menu'; import { Trash2, MoreHorizontal, GitCommit, GitPullRequest, - ExternalLink, Download, Upload, Play, @@ -21,10 +23,14 @@ import { MessageSquare, GitMerge, AlertCircle, + Copy, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types'; import { TooltipWrapper } from './tooltip-wrapper'; +import { useAvailableEditors } from '../hooks/use-available-editors'; +import { getEditorIcon } from '@/components/icons/editor-icons'; +import { useAppStore } from '@/store/app-store'; interface WorktreeActionsDropdownProps { worktree: WorktreeInfo; @@ -41,7 +47,7 @@ interface WorktreeActionsDropdownProps { onOpenChange: (open: boolean) => void; onPull: (worktree: WorktreeInfo) => void; onPush: (worktree: WorktreeInfo) => void; - onOpenInEditor: (worktree: WorktreeInfo) => void; + onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void; onCommit: (worktree: WorktreeInfo) => void; onCreatePR: (worktree: WorktreeInfo) => void; onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void; @@ -77,6 +83,31 @@ export function WorktreeActionsDropdown({ onStopDevServer, onOpenDevServerUrl, }: WorktreeActionsDropdownProps) { + // Get available editors for the "Open In" submenu + const { editors, hasMultipleEditors } = useAvailableEditors(); + + // Get the user's preferred default editor from settings + const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand); + + // 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 + const otherEditors = editors.filter((e) => e.command !== effectiveDefaultEditor?.command); + // Check if there's a PR associated with this worktree from stored metadata const hasPR = !!worktree.pr; @@ -200,10 +231,50 @@ export function WorktreeActionsDropdown({ )} - onOpenInEditor(worktree)} className="text-xs"> - - Open in {defaultEditorName} - + {/* Open in editor - split button: click main area for default, chevron for other options */} + {effectiveDefaultEditor && ( + +
+ {/* Main clickable area - opens in default editor */} + onOpenInEditor(worktree, effectiveDefaultEditor.command)} + className="text-xs flex-1 pr-0 rounded-r-none" + > + {(() => { + const EditorIcon = getEditorIcon(effectiveDefaultEditor.command); + return ; + })()} + Open in {effectiveDefaultEditor.name} + + {/* Chevron trigger for submenu with other editors and Copy Path */} + +
+ + {/* Other editors */} + {otherEditors.map((editor) => { + const EditorIcon = getEditorIcon(editor.command); + return ( + onOpenInEditor(worktree, editor.command)} + className="text-xs" + > + + {editor.name} + + ); + })} + {otherEditors.length > 0 && } + navigator.clipboard.writeText(worktree.path)} + className="text-xs" + > + + Copy Path + + +
+ )} {worktree.hasChanges && ( void; onPull: (worktree: WorktreeInfo) => void; onPush: (worktree: WorktreeInfo) => void; - onOpenInEditor: (worktree: WorktreeInfo) => void; + onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void; onCommit: (worktree: WorktreeInfo) => void; onCreatePR: (worktree: WorktreeInfo) => void; onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-available-editors.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-available-editors.ts new file mode 100644 index 00000000..9cf97576 --- /dev/null +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-available-editors.ts @@ -0,0 +1,46 @@ +import { useState, useEffect, useCallback } from 'react'; +import { createLogger } from '@automaker/utils/logger'; +import { getElectronAPI } from '@/lib/electron'; + +const logger = createLogger('AvailableEditors'); + +export interface EditorInfo { + name: string; + command: string; +} + +export function useAvailableEditors() { + const [editors, setEditors] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const fetchAvailableEditors = useCallback(async () => { + try { + const api = getElectronAPI(); + if (!api?.worktree?.getAvailableEditors) { + setIsLoading(false); + return; + } + const result = await api.worktree.getAvailableEditors(); + if (result.success && result.result?.editors) { + setEditors(result.result.editors); + } + } catch (error) { + logger.error('Failed to fetch available editors:', error); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchAvailableEditors(); + }, [fetchAvailableEditors]); + + return { + editors, + isLoading, + // Convenience property: has multiple editors (for deciding whether to show submenu) + hasMultipleEditors: editors.length > 1, + // The first editor is the "default" one + defaultEditor: editors[0] ?? null, + }; +} diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts index ac53c12b..f1f245dc 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts @@ -125,14 +125,14 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre [isPushing, fetchBranches, fetchWorktrees] ); - const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo) => { + const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo, editorCommand?: string) => { try { const api = getElectronAPI(); if (!api?.worktree?.openInEditor) { logger.warn('Open in editor API not available'); return; } - const result = await api.worktree.openInEditor(worktree.path); + const result = await api.worktree.openInEditor(worktree.path, editorCommand); if (result.success && result.result) { toast.success(result.result.message); } else if (result.error) { diff --git a/apps/ui/src/components/views/settings-view/account/account-section.tsx b/apps/ui/src/components/views/settings-view/account/account-section.tsx index 8c44706b..ae594166 100644 --- a/apps/ui/src/components/views/settings-view/account/account-section.tsx +++ b/apps/ui/src/components/views/settings-view/account/account-section.tsx @@ -1,15 +1,45 @@ import { useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; import { Button } from '@/components/ui/button'; -import { LogOut, User } from 'lucide-react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { LogOut, User, Code2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { logout } from '@/lib/http-api-client'; import { useAuthStore } from '@/store/auth-store'; +import { useAppStore } from '@/store/app-store'; +import { useAvailableEditors } from '@/components/views/board-view/worktree-panel/hooks/use-available-editors'; +import { getEditorIcon } from '@/components/icons/editor-icons'; export function AccountSection() { const navigate = useNavigate(); const [isLoggingOut, setIsLoggingOut] = useState(false); + // Editor settings + const { editors, isLoading: isLoadingEditors } = useAvailableEditors(); + const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand); + const setDefaultEditorCommand = useAppStore((s) => s.setDefaultEditorCommand); + + // Get effective default editor (respecting auto-detect order: Cursor > VS Code > first) + const getEffectiveDefaultEditor = () => { + 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(); + const handleLogout = async () => { setIsLoggingOut(true); try { @@ -43,6 +73,64 @@ export function AccountSection() {

Manage your session and account.

+ {/* Default IDE */} +
+
+
+ +
+
+

Default IDE

+

+ Default IDE to use when opening branches or worktrees +

+
+
+ +
+ {/* Logout */}
diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index e7c4c406..107ea0e5 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -51,6 +51,7 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'keyboardShortcuts', 'aiProfiles', 'mcpServers', + 'defaultEditorCommand', 'promptCustomization', 'projects', 'trashedProjects', @@ -407,6 +408,7 @@ export async function refreshSettingsFromServer(): Promise { }, aiProfiles: serverSettings.aiProfiles, mcpServers: serverSettings.mcpServers, + defaultEditorCommand: serverSettings.defaultEditorCommand ?? null, promptCustomization: serverSettings.promptCustomization ?? {}, projects: serverSettings.projects, trashedProjects: serverSettings.trashedProjects, diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 84e0d945..7b0b3789 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -1625,13 +1625,14 @@ function createMockWorktreeAPI(): WorktreeAPI { }; }, - openInEditor: async (worktreePath: string) => { - console.log('[Mock] Opening in editor:', worktreePath); + openInEditor: async (worktreePath: string, editorCommand?: string) => { + const editorName = editorCommand === 'cursor' ? 'Cursor' : 'VS Code'; + console.log('[Mock] Opening in editor:', worktreePath, 'using:', editorName); return { success: true, result: { - message: `Opened ${worktreePath} in VS Code`, - editorName: 'VS Code', + message: `Opened ${worktreePath} in ${editorName}`, + editorName, }, }; }, @@ -1647,6 +1648,19 @@ function createMockWorktreeAPI(): WorktreeAPI { }; }, + getAvailableEditors: async () => { + console.log('[Mock] Getting available editors'); + return { + success: true, + result: { + editors: [ + { name: 'VS Code', command: 'code' }, + { name: 'Finder', command: 'open' }, + ], + }, + }; + }, + initGit: async (projectPath: string) => { console.log('[Mock] Initializing git:', projectPath); return { diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 691dcfec..df95124e 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1598,9 +1598,10 @@ export class HttpApiClient implements ElectronAPI { this.post('/api/worktree/list-branches', { worktreePath }), switchBranch: (worktreePath: string, branchName: string) => this.post('/api/worktree/switch-branch', { worktreePath, branchName }), - openInEditor: (worktreePath: string) => - this.post('/api/worktree/open-in-editor', { worktreePath }), + openInEditor: (worktreePath: string, editorCommand?: string) => + this.post('/api/worktree/open-in-editor', { worktreePath, editorCommand }), getDefaultEditor: () => this.get('/api/worktree/default-editor'), + getAvailableEditors: () => this.get('/api/worktree/available-editors'), initGit: (projectPath: string) => this.post('/api/worktree/init-git', { projectPath }), startDevServer: (projectPath: string, worktreePath: string) => this.post('/api/worktree/start-dev', { projectPath, worktreePath }), diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 78d6e65c..89a1d95f 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -585,6 +585,9 @@ export interface AppState { // MCP Servers mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use + // Editor Configuration + defaultEditorCommand: string | null; // Default editor for "Open In" action + // Skills Configuration enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories) skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from @@ -958,6 +961,9 @@ export interface AppActions { setAutoLoadClaudeMd: (enabled: boolean) => Promise; setSkipSandboxWarning: (skip: boolean) => Promise; + // Editor Configuration actions + setDefaultEditorCommand: (command: string | null) => void; + // Prompt Customization actions setPromptCustomization: (customization: PromptCustomization) => Promise; @@ -1196,6 +1202,7 @@ const initialState: AppState = { autoLoadClaudeMd: false, // Default to disabled (user must opt-in) skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog) mcpServers: [], // No MCP servers configured by default + defaultEditorCommand: null, // Auto-detect: Cursor > VS Code > first available enableSkills: true, // Skills enabled by default skillsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default enableSubagents: true, // Subagents enabled by default @@ -1959,6 +1966,9 @@ export const useAppStore = create()((set, get) => ({ set({ skipSandboxWarning: previous }); } }, + + // Editor Configuration actions + setDefaultEditorCommand: (command) => set({ defaultEditorCommand: command }), // Prompt Customization actions setPromptCustomization: async (customization) => { set({ promptCustomization: customization }); diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts index fc64f375..8bf3efe6 100644 --- a/apps/ui/src/types/electron.d.ts +++ b/apps/ui/src/types/electron.d.ts @@ -872,7 +872,10 @@ export interface WorktreeAPI { }>; // Open a worktree directory in the editor - openInEditor: (worktreePath: string) => Promise<{ + openInEditor: ( + worktreePath: string, + editorCommand?: string + ) => Promise<{ success: boolean; result?: { message: string; @@ -891,6 +894,18 @@ export interface WorktreeAPI { error?: string; }>; + // Get all available code editors + getAvailableEditors: () => Promise<{ + success: boolean; + result?: { + editors: Array<{ + name: string; + command: string; + }>; + }; + error?: string; + }>; + // Initialize git repository in a project initGit: (projectPath: string) => Promise<{ success: boolean; diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 6f13c8a3..e5420df3 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -565,6 +565,10 @@ export interface GlobalSettings { /** List of configured MCP servers for agent use */ mcpServers: MCPServerConfig[]; + // Editor Configuration + /** Default editor command for "Open In" action (null = auto-detect: Cursor > VS Code > first available) */ + defaultEditorCommand: string | null; + // Prompt Customization /** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */ promptCustomization?: PromptCustomization; @@ -819,6 +823,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS, codexThreadId: undefined, mcpServers: [], + defaultEditorCommand: null, enableSkills: true, skillsSources: ['user', 'project'], enableSubagents: true,