mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: add default IDE setting and multi-editor support with icons
Add comprehensive editor detection and selection system that allows users to configure their preferred IDE for opening branches and worktrees. ## Server-side Changes - Add `/api/worktree/available-editors` endpoint to detect installed editors - Support detection via CLI commands (cursor, code, zed, subl, etc.) - Support detection via macOS app bundles in /Applications and ~/Applications - Detect editors: Cursor, VS Code, Zed, Sublime Text, Windsurf, Trae, Rider, WebStorm, Xcode, Android Studio, Antigravity, and file managers ## UI Changes ### Editor Icons - Add new `editor-icons.tsx` with SVG icons for all supported editors - Icons: Cursor, VS Code, Zed, Sublime Text, Windsurf, Trae, Rider, WebStorm, Xcode, Android Studio, Antigravity, Finder - `getEditorIcon()` helper maps editor commands to appropriate icons ### Default IDE Setting - Add "Default IDE" selector in Settings > Account section - Options: Auto-detect (Cursor > VS Code > first available) or explicit choice - Setting persists via `defaultEditorCommand` in global settings ### Worktree Dropdown Improvements - Implement split-button UX for "Open In" action - Click main area: opens directly in default IDE (single click) - Click chevron: shows submenu with other editors + Copy Path - Each editor shows with its branded icon ## Type & Store Changes - Add `defaultEditorCommand: string | null` to GlobalSettings - Add to app-store with `setDefaultEditorCommand` action - Add to SETTINGS_FIELDS_TO_SYNC for persistence - Add `useAvailableEditors` hook for fetching detected editors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
// 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<EditorInfo[]> {
|
||||
// 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<EditorInfo> {
|
||||
// Return cached result if available
|
||||
@@ -27,54 +131,29 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
try {
|
||||
@@ -96,8 +175,9 @@ export function createGetDefaultEditorHandler() {
|
||||
export function createOpenInEditorHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
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}"`);
|
||||
|
||||
Reference in New Issue
Block a user