mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge pull request #423 from stefandevo/main
feat: add default IDE setting and multi-editor support with icons
This commit is contained in:
@@ -24,6 +24,8 @@ import { createSwitchBranchHandler } from './routes/switch-branch.js';
|
|||||||
import {
|
import {
|
||||||
createOpenInEditorHandler,
|
createOpenInEditorHandler,
|
||||||
createGetDefaultEditorHandler,
|
createGetDefaultEditorHandler,
|
||||||
|
createGetAvailableEditorsHandler,
|
||||||
|
createRefreshEditorsHandler,
|
||||||
} from './routes/open-in-editor.js';
|
} from './routes/open-in-editor.js';
|
||||||
import { createInitGitHandler } from './routes/init-git.js';
|
import { createInitGitHandler } from './routes/init-git.js';
|
||||||
import { createMigrateHandler } from './routes/migrate.js';
|
import { createMigrateHandler } from './routes/migrate.js';
|
||||||
@@ -77,6 +79,8 @@ export function createWorktreeRoutes(): Router {
|
|||||||
router.post('/switch-branch', requireValidWorktree, createSwitchBranchHandler());
|
router.post('/switch-branch', requireValidWorktree, createSwitchBranchHandler());
|
||||||
router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler());
|
router.post('/open-in-editor', validatePathParams('worktreePath'), createOpenInEditorHandler());
|
||||||
router.get('/default-editor', createGetDefaultEditorHandler());
|
router.get('/default-editor', createGetDefaultEditorHandler());
|
||||||
|
router.get('/available-editors', createGetAvailableEditorsHandler());
|
||||||
|
router.post('/refresh-editors', createRefreshEditorsHandler());
|
||||||
router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler());
|
router.post('/init-git', validatePathParams('projectPath'), createInitGitHandler());
|
||||||
router.post('/migrate', createMigrateHandler());
|
router.post('/migrate', createMigrateHandler());
|
||||||
router.post(
|
router.post(
|
||||||
|
|||||||
@@ -1,78 +1,40 @@
|
|||||||
/**
|
/**
|
||||||
* POST /open-in-editor endpoint - Open a worktree directory in the default code editor
|
* POST /open-in-editor endpoint - Open a worktree directory in the default code editor
|
||||||
* GET /default-editor endpoint - Get the name of the default code editor
|
* GET /default-editor endpoint - Get the name of the default code editor
|
||||||
|
* POST /refresh-editors endpoint - Clear editor cache and re-detect available editors
|
||||||
|
*
|
||||||
|
* This module uses @automaker/platform for cross-platform editor detection and launching.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from 'child_process';
|
import { isAbsolute } from 'path';
|
||||||
import { promisify } from 'util';
|
import {
|
||||||
|
clearEditorCache,
|
||||||
|
detectAllEditors,
|
||||||
|
detectDefaultEditor,
|
||||||
|
openInEditor,
|
||||||
|
openInFileManager,
|
||||||
|
} from '@automaker/platform';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from '../common.js';
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const logger = createLogger('open-in-editor');
|
||||||
|
|
||||||
// Editor detection with caching
|
export function createGetAvailableEditorsHandler() {
|
||||||
interface EditorInfo {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
name: string;
|
try {
|
||||||
command: string;
|
const editors = await detectAllEditors();
|
||||||
}
|
res.json({
|
||||||
|
success: true,
|
||||||
let cachedEditor: EditorInfo | null = null;
|
result: {
|
||||||
|
editors,
|
||||||
/**
|
},
|
||||||
* Detect which code editor is available on the system
|
});
|
||||||
*/
|
} catch (error) {
|
||||||
async function detectDefaultEditor(): Promise<EditorInfo> {
|
logError(error, 'Get available editors failed');
|
||||||
// Return cached result if available
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
if (cachedEditor) {
|
}
|
||||||
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' };
|
|
||||||
}
|
|
||||||
return cachedEditor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGetDefaultEditorHandler() {
|
export function createGetDefaultEditorHandler() {
|
||||||
@@ -93,11 +55,41 @@ export function createGetDefaultEditorHandler() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to refresh the editor cache and re-detect available editors
|
||||||
|
* Useful when the user has installed/uninstalled editors
|
||||||
|
*/
|
||||||
|
export function createRefreshEditorsHandler() {
|
||||||
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Clear the cache
|
||||||
|
clearEditorCache();
|
||||||
|
|
||||||
|
// Re-detect editors (this will repopulate the cache)
|
||||||
|
const editors = await detectAllEditors();
|
||||||
|
|
||||||
|
logger.info(`Editor cache refreshed, found ${editors.length} editors`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
result: {
|
||||||
|
editors,
|
||||||
|
message: `Found ${editors.length} available editors`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logError(error, 'Refresh editors failed');
|
||||||
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createOpenInEditorHandler() {
|
export function createOpenInEditorHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { worktreePath } = req.body as {
|
const { worktreePath, editorCommand } = req.body as {
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
|
editorCommand?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!worktreePath) {
|
if (!worktreePath) {
|
||||||
@@ -108,42 +100,44 @@ export function createOpenInEditorHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = await detectDefaultEditor();
|
// Security: Validate that worktreePath is an absolute path
|
||||||
|
if (!isAbsolute(worktreePath)) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'worktreePath must be an absolute path',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`${editor.command} "${worktreePath}"`);
|
// Use the platform utility to open in editor
|
||||||
|
const result = await openInEditor(worktreePath, editorCommand);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
message: `Opened ${worktreePath} in ${editor.name}`,
|
message: `Opened ${worktreePath} in ${result.editorName}`,
|
||||||
editorName: editor.name,
|
editorName: result.editorName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (editorError) {
|
} catch (editorError) {
|
||||||
// If the detected editor fails, try opening in default file manager as fallback
|
// If the specified editor fails, try opening in default file manager as fallback
|
||||||
const platform = process.platform;
|
logger.warn(
|
||||||
let openCommand: string;
|
`Failed to open in editor, falling back to file manager: ${getErrorMessage(editorError)}`
|
||||||
let fallbackName: string;
|
);
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
try {
|
||||||
openCommand = `open "${worktreePath}"`;
|
const result = await openInFileManager(worktreePath);
|
||||||
fallbackName = 'Finder';
|
res.json({
|
||||||
} else if (platform === 'win32') {
|
success: true,
|
||||||
openCommand = `explorer "${worktreePath}"`;
|
result: {
|
||||||
fallbackName = 'Explorer';
|
message: `Opened ${worktreePath} in ${result.editorName}`,
|
||||||
} else {
|
editorName: result.editorName,
|
||||||
openCommand = `xdg-open "${worktreePath}"`;
|
},
|
||||||
fallbackName = 'File Manager';
|
});
|
||||||
|
} catch (fallbackError) {
|
||||||
|
// Both editor and file manager failed
|
||||||
|
throw fallbackError;
|
||||||
}
|
}
|
||||||
|
|
||||||
await execAsync(openCommand);
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
result: {
|
|
||||||
message: `Opened ${worktreePath} in ${fallbackName}`,
|
|
||||||
editorName: fallbackName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, 'Open in editor failed');
|
logError(error, 'Open in editor failed');
|
||||||
|
|||||||
220
apps/ui/src/components/icons/editor-icons.tsx
Normal file
220
apps/ui/src/components/icons/editor-icons.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import type { ComponentType, ComponentProps } from 'react';
|
||||||
|
import { FolderOpen } from 'lucide-react';
|
||||||
|
|
||||||
|
type IconProps = ComponentProps<'svg'>;
|
||||||
|
type IconComponent = ComponentType<IconProps>;
|
||||||
|
|
||||||
|
const ANTIGRAVITY_COMMANDS = ['antigravity', 'agy'] as const;
|
||||||
|
const [PRIMARY_ANTIGRAVITY_COMMAND, LEGACY_ANTIGRAVITY_COMMAND] = ANTIGRAVITY_COMMANDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor editor logo icon - from LobeHub icons
|
||||||
|
*/
|
||||||
|
export function CursorIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VS Code editor logo icon
|
||||||
|
*/
|
||||||
|
export function VSCodeIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VS Code Insiders editor logo icon (same as VS Code)
|
||||||
|
*/
|
||||||
|
export function VSCodeInsidersIcon(props: IconProps) {
|
||||||
|
return <VSCodeIcon {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kiro editor logo icon (VS Code fork)
|
||||||
|
*/
|
||||||
|
export function KiroIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M6.594.016A7.006 7.006 0 0 0 .742 3.875a6.996 6.996 0 0 0-.726 2.793C.004 6.878 0 9.93.004 16.227c.004 8.699.008 9.265.031 9.476.113.93.324 1.652.707 2.422a6.918 6.918 0 0 0 3.172 3.148c.75.372 1.508.59 2.398.692.227.027.77.027 9.688.027 8.945 0 9.457 0 9.688-.027.917-.106 1.66-.32 2.437-.707a6.918 6.918 0 0 0 3.148-3.172c.372-.75.59-1.508.692-2.398.027-.227.027-.77.027-9.665 0-9.976.004-9.53-.07-10.03a6.993 6.993 0 0 0-3.024-4.798 6.427 6.427 0 0 0-.757-.445 7.06 7.06 0 0 0-2.774-.734c-.328-.02-18.437-.02-18.773 0Zm10.789 5.406a7.556 7.556 0 0 1 6.008 3.805c.148.257.406.796.52 1.085.394 1 .632 2.157.769 3.75.035.38.05 1.965.023 2.407-.125 2.168-.625 4.183-1.515 6.078a9.77 9.77 0 0 1-.801 1.437c-.93 1.305-2.32 2.332-3.48 2.57-.895.184-1.602-.1-2.048-.827a3.42 3.42 0 0 1-.25-.528c-.035-.097-.062-.129-.086-.09-.003.008-.09.075-.191.153-.95.722-2.02 1.175-3.059 1.293-.273.03-.859.023-1.085-.016-.715-.121-1.286-.441-1.649-.93a2.563 2.563 0 0 1-.328-.632c-.117-.36-.156-.813-.117-1.227.054-.55.226-1.184.484-1.766a.48.48 0 0 0 .043-.117 2.11 2.11 0 0 0-.137.055c-.363.16-.898.305-1.308.351-.844.098-1.426-.14-1.715-.699-.106-.203-.149-.39-.16-.676-.008-.261.008-.43.066-.656.059-.23.121-.367.403-.89.382-.72.492-.946.636-1.348.328-.899.48-1.723.688-3.754.148-1.469.254-2.14.433-2.766.028-.09.078-.277.114-.414.796-3.074 3.113-5.183 6.148-5.601.129-.016.309-.04.399-.047.238-.016.96-.02 1.195 0Zm0 0" />
|
||||||
|
<path d="M16.754 11.336a.815.815 0 0 0-.375.219c-.176.18-.293.441-.356.804-.039.235-.058.602-.039.868.028.406.082.64.204.894.128.262.304.426.546.496.106.031.383.031.5 0 .422-.113.703-.531.801-1.191a4.822 4.822 0 0 0-.012-.95c-.062-.378-.183-.675-.359-.863a.808.808 0 0 0-.648-.293.804.804 0 0 0-.262.016ZM20.375 11.328a1.01 1.01 0 0 0-.363.188c-.164.144-.293.402-.364.718-.05.23-.07.426-.07.743 0 .32.02.511.07.742.11.496.352.808.688.898.121.031.379.031.5 0 .402-.105.68-.5.781-1.11.035-.198.047-.648.024-.87-.063-.63-.293-1.059-.649-1.23a1.513 1.513 0 0 0-.219-.079 1.362 1.362 0 0 0-.398 0Zm0 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zed editor logo icon (from Simple Icons)
|
||||||
|
*/
|
||||||
|
export function ZedIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sublime Text editor logo icon
|
||||||
|
*/
|
||||||
|
export function SublimeTextIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M20.953.004a.397.397 0 0 0-.18.045L3.473 8.63a.397.397 0 0 0-.033.69l4.873 3.33-5.26 2.882a.397.397 0 0 0-.006.692l17.3 9.73a.397.397 0 0 0 .593-.344V15.094a.397.397 0 0 0-.203-.346l-4.917-2.763 5.233-2.725a.397.397 0 0 0 .207-.348V.397a.397.397 0 0 0-.307-.393z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* macOS Finder icon
|
||||||
|
*/
|
||||||
|
export function FinderIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M2.5 3A2.5 2.5 0 0 0 0 5.5v13A2.5 2.5 0 0 0 2.5 21h19a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 21.5 3h-19zM7 8.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm10 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm-9 6c0-.276.336-.5.75-.5h6.5c.414 0 .75.224.75.5v1c0 .828-1.343 2.5-4 2.5s-4-1.672-4-2.5v-1z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windsurf editor logo icon (by Codeium) - from LobeHub icons
|
||||||
|
*/
|
||||||
|
export function WindsurfIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M23.78 5.004h-.228a2.187 2.187 0 00-2.18 2.196v4.912c0 .98-.804 1.775-1.76 1.775a1.818 1.818 0 01-1.472-.773L13.168 5.95a2.197 2.197 0 00-1.81-.95c-1.134 0-2.154.972-2.154 2.173v4.94c0 .98-.797 1.775-1.76 1.775-.57 0-1.136-.289-1.472-.773L.408 5.098C.282 4.918 0 5.007 0 5.228v4.284c0 .216.066.426.188.604l5.475 7.889c.324.466.8.812 1.351.938 1.377.316 2.645-.754 2.645-2.117V11.89c0-.98.787-1.775 1.76-1.775h.002c.586 0 1.135.288 1.472.773l4.972 7.163a2.15 2.15 0 001.81.95c1.158 0 2.151-.973 2.151-2.173v-4.939c0-.98.787-1.775 1.76-1.775h.194c.122 0 .22-.1.22-.222V5.225a.221.221 0 00-.22-.222z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trae editor logo icon (by ByteDance) - from LobeHub icons
|
||||||
|
*/
|
||||||
|
export function TraeIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M24 20.541H3.428v-3.426H0V3.4h24V20.54zM3.428 17.115h17.144V6.827H3.428v10.288zm8.573-5.196l-2.425 2.424-2.424-2.424 2.424-2.424 2.425 2.424zm6.857-.001l-2.424 2.423-2.425-2.423 2.425-2.425 2.424 2.425z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JetBrains Rider logo icon
|
||||||
|
*/
|
||||||
|
export function RiderIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M0 0v24h24V0zm7.031 3.113A4.063 4.063 0 0 1 9.72 4.14a3.23 3.23 0 0 1 .84 2.28A3.16 3.16 0 0 1 8.4 9.54l2.46 3.6H8.28L6.12 9.9H4.38v3.24H2.16V3.12c1.61-.004 3.281.009 4.871-.007zm5.509.007h3.96c3.18 0 5.34 2.16 5.34 5.04 0 2.82-2.16 5.04-5.34 5.04h-3.96zm4.069 1.976c-.607.01-1.235.004-1.849.004v6.06h1.74a2.882 2.882 0 0 0 3.06-3 2.897 2.897 0 0 0-2.951-3.064zM4.319 5.1v2.88H6.6c1.08 0 1.68-.6 1.68-1.44 0-.96-.66-1.44-1.74-1.44zM2.16 19.5h9V21h-9Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JetBrains WebStorm logo icon
|
||||||
|
*/
|
||||||
|
export function WebStormIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M0 0v24h24V0H0zm17.889 2.889c1.444 0 2.667.444 3.667 1.278l-1.111 1.667c-.889-.611-1.722-1-2.556-1s-1.278.389-1.278.889v.056c0 .667.444.889 2.111 1.333 2 .556 3.111 1.278 3.111 3v.056c0 2-1.5 3.111-3.611 3.111-1.5-.056-3-.611-4.167-1.667l1.278-1.556c.889.722 1.833 1.222 2.944 1.222.889 0 1.389-.333 1.389-.944v-.056c0-.556-.333-.833-2-1.278-2-.5-3.222-1.056-3.222-3.056v-.056c0-1.833 1.444-3 3.444-3zm-16.111.222h2.278l1.5 5.778 1.722-5.778h1.667l1.667 5.778 1.5-5.778h2.333l-2.833 9.944H9.723L8.112 7.277l-1.667 5.778H4.612L1.779 3.111zm.5 16.389h9V21h-9v-1.5z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xcode logo icon
|
||||||
|
*/
|
||||||
|
export function XcodeIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M19.06 5.3327c.4517-.1936.7744-.2581 1.097-.1936.5163.1291.7744.5163.968.7098.1936.3872.9034.7744 1.2261.8389.2581.0645.7098-.6453 1.0325-1.2906.3227-.5808.5163-1.3552.4517-1.5488-.0645-.1936-.968-.5808-1.1616-.5808-.1291 0-.3872.1291-.8389.0645-.4517-.0645-.9034-.5808-1.1616-.968-.4517-.6453-1.097-1.0325-1.6778-1.3552-.6453-.3227-1.3552-.5163-2.065-.6453-1.0325-.2581-2.065-.4517-3.0975-.3227-.5808.0645-1.2906.1291-1.8069.3227-.0645 0-.1936.1936-.0645.1936s.5808.0645.5808.0645-.5807.1292-.5807.2583c0 .1291.0645.1291.1291.1291.0645 0 1.4842-.0645 2.065 0 .6453.1291 1.3552.4517 1.8069 1.2261.7744 1.4197.4517 2.7749.2581 3.2266-.968 2.1295-8.6472 15.2294-9.0344 16.1328-.3873.9034-.5163 1.4842.5807 2.065s1.6778.3227 2.0005-.0645c.3872-.5163 7.0339-17.1654 9.2925-18.2624zm-3.6138 8.7117h1.5488c1.0325 0 1.2261.5163 1.2261.7098.0645.5163-.1936 1.1616-1.2261 1.1616h-.968l.7744 1.2906c.4517.7744.2581 1.1616 0 1.4197-.3872.3872-1.2261.3872-1.6778-.4517l-.9034-1.5488c-.6453 1.4197-1.2906 2.9684-2.065 4.7753h4.0009c1.9359 0 3.5492-1.6133 3.5492-3.5492V6.5588c-.0645-.1291-.1936-.0645-.2581 0-.3872.4517-1.4842 2.0004-4.001 7.4856zm-9.8087 8.0019h-.3227c-2.3231 0-4.1945-1.8714-4.1945-4.1945V7.0105c0-2.3231 1.8714-4.1945 4.1945-4.1945h9.3571c-.1936-.1936-.968-.5163-1.7423-.4517-.3227 0-.968.1291-1.3552-.1291-.3872-.3227-.3227-.5163-.9034-.5163H4.9277c-2.6458 0-4.7753 2.1295-4.7753 4.7753v11.7447c0 2.6458 2.1295 4.7753 4.4527 4.7108.6452 0 .8388-.5162 1.0324-.9034zM20.4152 6.9459v10.9058c0 2.3231-1.8714 4.1945-4.1945 4.1945H11.897s-.3872 1.0325.8389 1.0325h3.8719c2.6458 0 4.7753-2.1295 4.7753-4.7753V8.8173c.0646-.9034-.7098-1.4842-.9679-1.8714zm-18.5851.0646v10.8413c0 1.9359 1.6133 3.5492 3.5492 3.5492h.5808c0-.0645.7744-1.4197 2.4522-4.2591.1936-.3872.4517-.7744.7098-1.2261H4.4114c-.5808 0-.9034-.3872-.968-.7098-.1291-.5163.1936-1.1616.9034-1.1616h2.3877l3.033-5.2916s-.7098-1.2906-.9034-1.6133c-.2582-.4517-.1291-.9034.129-1.1615.3872-.3872 1.0325-.5808 1.6778.4517l.2581.3872.2581-.3872c.5808-.8389.968-.7744 1.2906-.7098.5163.1291.8389.7098.3872 1.6133L8.864 14.0444h1.3552c.4517-.7744.9034-1.5488 1.3552-2.3877-.0645-.3227-.1291-.7098-.0645-1.0325.0645-.5163.3227-.968.6453-1.3552l.3872.6453c1.2261-2.1295 2.1295-3.9364 2.3877-4.6463.1291-.3872.3227-1.1616.1291-1.8069H5.3794c-2.0005.0001-3.5493 1.6134-3.5493 3.5494zM4.605 17.7872c0-.0645.7744-1.4197.7744-1.4197 1.2261-.3227 1.8069.4517 1.8714.5163 0 0-.8389 1.4842-1.097 1.7423s-.5808.3227-.9034.2581c-.5164-.129-.839-.6453-.6454-1.097z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android Studio logo icon
|
||||||
|
*/
|
||||||
|
export function AndroidStudioIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path d="M19.2693 10.3368c-.3321 0-.6026.2705-.6026.6031v9.8324h-1.7379l-3.3355-6.9396c.476-.5387.6797-1.286.5243-2.0009a2.2862 2.2862 0 0 0-1.2893-1.6248v-.8124c.0121-.2871-.1426-.5787-.4043-.7407-.1391-.0825-.2884-.1234-.4402-.1234a.8478.8478 0 0 0-.4318.1182c-.2701.1671-.4248.4587-.4123.7662l-.0003.721c-1.0149.3668-1.6619 1.4153-1.4867 2.5197a2.282 2.282 0 0 0 .5916 1.2103l-3.2096 6.9064H4.0928c-1.0949-.007-1.9797-.8948-1.9832-1.9896V5.016c-.0055 1.1024.8836 2.0006 1.9859 2.0062a2.024 2.024 0 0 0 .1326-.0037h14.7453s2.5343-.2189 2.8619 1.5392c-.2491.0287-.4449.2321-.4449.4889 0 .7115-.5791 1.2901-1.3028 1.2901h-.8183zM17.222 22.5366c.2347.4837.0329 1.066-.4507 1.3007-.1296.0629-.2666.0895-.4018.0927a.9738.9738 0 0 1-.3194-.0455c-.024-.0078-.046-.0209-.0694-.0305a.9701.9701 0 0 1-.2277-.1321c-.0247-.0192-.0495-.038-.0724-.0598-.0825-.0783-.1574-.1672-.21-.2757l-1.2554-2.6143-1.5585-3.2452a.7725.7725 0 0 0-.6995-.4443h-.0024a.792.792 0 0 0-.7083.4443l-1.5109 3.2452-1.2321 2.6464a.9722.9722 0 0 1-.7985.5795c-.0626.0053-.1238-.0024-.185-.0087-.0344-.0036-.069-.0053-.1025-.0124-.0489-.0103-.0954-.0278-.142-.0452-.0301-.0113-.0613-.0197-.0901-.0339-.0496-.0244-.0948-.0565-.1397-.0889-.0217-.0156-.0457-.0275-.0662-.045a.9862.9862 0 0 1-.1695-.1844.9788.9788 0 0 1-.0708-.9852l.8469-1.8223 3.2676-7.0314a1.7964 1.7964 0 0 1-.7072-1.1637c-.1555-.9799.5129-1.9003 1.4928-2.0559V9.3946a.3542.3542 0 0 1 .1674-.3155.3468.3468 0 0 1 .3541 0 .354.354 0 0 1 .1674.3155v1.159l.0129.0064a1.8028 1.8028 0 0 1 1.2878 1.378 1.7835 1.7835 0 0 1-.6439 1.7836l3.3889 7.0507.8481 1.7643zM12.9841 12.306c.0042-.6081-.4854-1.1044-1.0935-1.1085a1.1204 1.1204 0 0 0-.7856.3219 1.101 1.101 0 0 0-.323.7716c-.0042.6081.4854 1.1044 1.0935 1.1085h.0077c.6046 0 1.0967-.488 1.1009-1.0935zm-1.027 5.2768c-.1119.0005-.2121.0632-.2571.1553l-1.4127 3.0342h3.3733l-1.4564-3.0328a.274.274 0 0 0-.2471-.1567zm8.1432-6.7459l-.0129-.0001h-.8177a.103.103 0 0 0-.103.103v12.9103a.103.103 0 0 0 .0966.103h.8435c.9861-.0035 1.7836-.804 1.7836-1.79V9.0468c0 .9887-.8014 1.7901-1.7901 1.7901zM2.6098 5.0161v.019c.0039.816.6719 1.483 1.4874 1.4869a12.061 12.061 0 0 1 .1309-.0034h1.1286c.1972-1.315.7607-2.525 1.638-3.4859H4.0993c-.9266.0031-1.6971.6401-1.9191 1.4975.2417.0355.4296.235.4296.4859zm6.3381-2.8977L7.9112.3284a.219.219 0 0 1 0-.2189A.2384.2384 0 0 1 8.098 0a.219.219 0 0 1 .1867.1094l1.0496 1.8158a6.4907 6.4907 0 0 1 5.3186 0L15.696.1094a.2189.2189 0 0 1 .3734.2189l-1.0302 1.79c1.6671.9125 2.7974 2.5439 3.0975 4.4018l-12.286-.0014c.3004-1.8572 1.4305-3.488 3.0972-4.4003zm5.3774 2.6202a.515.515 0 0 0 .5271.5028.515.515 0 0 0 .5151-.5151.5213.5213 0 0 0-.8885-.367.5151.5151 0 0 0-.1537.3793zm-5.7178-.0067a.5151.5151 0 0 0 .5207.5095.5086.5086 0 0 0 .367-.1481.5215.5215 0 1 0-.734-.7341.515.515 0 0 0-.1537.3727z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Antigravity IDE logo icon - stylized "A" arch shape
|
||||||
|
*/
|
||||||
|
export function AntigravityIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M12 1C11 1 9.5 3 8 7c-1.5 4-3 8.5-4 11.5-.5 1.5-.3 2.8.5 3.3.8.5 2 .2 3-.8.8-.8 1.3-2 1.8-3.2.3-.8.8-1.3 1.5-1.3h2.4c.7 0 1.2.5 1.5 1.3.5 1.2 1 2.4 1.8 3.2 1 1 2.2 1.3 3 .8.8-.5 1-1.8.5-3.3-1-3-2.5-7.5-4-11.5C14.5 3 13 1 12 1zm0 5c.8 2 2 5.5 3 8.5H9c1-3 2.2-6.5 3-8.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate icon component for an editor command
|
||||||
|
*/
|
||||||
|
export function getEditorIcon(command: string): IconComponent {
|
||||||
|
// Handle direct CLI commands
|
||||||
|
const cliIcons: Record<string, IconComponent> = {
|
||||||
|
cursor: CursorIcon,
|
||||||
|
code: VSCodeIcon,
|
||||||
|
'code-insiders': VSCodeInsidersIcon,
|
||||||
|
kido: KiroIcon,
|
||||||
|
zed: ZedIcon,
|
||||||
|
subl: SublimeTextIcon,
|
||||||
|
windsurf: WindsurfIcon,
|
||||||
|
trae: TraeIcon,
|
||||||
|
rider: RiderIcon,
|
||||||
|
webstorm: WebStormIcon,
|
||||||
|
xed: XcodeIcon,
|
||||||
|
studio: AndroidStudioIcon,
|
||||||
|
[PRIMARY_ANTIGRAVITY_COMMAND]: AntigravityIcon,
|
||||||
|
[LEGACY_ANTIGRAVITY_COMMAND]: 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 - insiders')) return VSCodeInsidersIcon;
|
||||||
|
if (cmdLower.includes('visual studio code')) return VSCodeIcon;
|
||||||
|
if (cmdLower.includes('kiro')) return KiroIcon;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -6,13 +6,15 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Trash2,
|
Trash2,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
GitCommit,
|
GitCommit,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
ExternalLink,
|
|
||||||
Download,
|
Download,
|
||||||
Upload,
|
Upload,
|
||||||
Play,
|
Play,
|
||||||
@@ -21,15 +23,18 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
GitMerge,
|
GitMerge,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
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, useEffectiveDefaultEditor } from '../hooks/use-available-editors';
|
||||||
|
import { getEditorIcon } from '@/components/icons/editor-icons';
|
||||||
|
|
||||||
interface WorktreeActionsDropdownProps {
|
interface WorktreeActionsDropdownProps {
|
||||||
worktree: WorktreeInfo;
|
worktree: WorktreeInfo;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
defaultEditorName: string;
|
|
||||||
aheadCount: number;
|
aheadCount: number;
|
||||||
behindCount: number;
|
behindCount: number;
|
||||||
isPulling: boolean;
|
isPulling: boolean;
|
||||||
@@ -41,7 +46,7 @@ interface WorktreeActionsDropdownProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onPull: (worktree: WorktreeInfo) => void;
|
onPull: (worktree: WorktreeInfo) => void;
|
||||||
onPush: (worktree: WorktreeInfo) => void;
|
onPush: (worktree: WorktreeInfo) => void;
|
||||||
onOpenInEditor: (worktree: WorktreeInfo) => void;
|
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
|
||||||
onCommit: (worktree: WorktreeInfo) => void;
|
onCommit: (worktree: WorktreeInfo) => void;
|
||||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||||
@@ -55,7 +60,6 @@ interface WorktreeActionsDropdownProps {
|
|||||||
export function WorktreeActionsDropdown({
|
export function WorktreeActionsDropdown({
|
||||||
worktree,
|
worktree,
|
||||||
isSelected,
|
isSelected,
|
||||||
defaultEditorName,
|
|
||||||
aheadCount,
|
aheadCount,
|
||||||
behindCount,
|
behindCount,
|
||||||
isPulling,
|
isPulling,
|
||||||
@@ -77,6 +81,20 @@ export function WorktreeActionsDropdown({
|
|||||||
onStopDevServer,
|
onStopDevServer,
|
||||||
onOpenDevServerUrl,
|
onOpenDevServerUrl,
|
||||||
}: WorktreeActionsDropdownProps) {
|
}: WorktreeActionsDropdownProps) {
|
||||||
|
// Get available editors for the "Open In" submenu
|
||||||
|
const { editors } = useAvailableEditors();
|
||||||
|
|
||||||
|
// Use shared hook for effective default editor
|
||||||
|
const effectiveDefaultEditor = useEffectiveDefaultEditor(editors);
|
||||||
|
|
||||||
|
// Get other editors (excluding the default) for the submenu
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -200,10 +218,54 @@ export function WorktreeActionsDropdown({
|
|||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => onOpenInEditor(worktree)} className="text-xs">
|
{/* Open in editor - split button: click main area for default, chevron for other options */}
|
||||||
<ExternalLink className="w-3.5 h-3.5 mr-2" />
|
{effectiveDefaultEditor && (
|
||||||
Open in {defaultEditorName}
|
<DropdownMenuSub>
|
||||||
</DropdownMenuItem>
|
<div className="flex items-center">
|
||||||
|
{/* Main clickable area - opens in default editor */}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onOpenInEditor(worktree, effectiveDefaultEditor.command)}
|
||||||
|
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
{DefaultEditorIcon && <DefaultEditorIcon className="w-3.5 h-3.5 mr-2" />}
|
||||||
|
Open in {effectiveDefaultEditor.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{/* Chevron trigger for submenu with other editors and Copy Path */}
|
||||||
|
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
{/* Other editors */}
|
||||||
|
{otherEditors.map((editor) => {
|
||||||
|
const EditorIcon = getEditorIcon(editor.command);
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={editor.command}
|
||||||
|
onClick={() => onOpenInEditor(worktree, editor.command)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<EditorIcon className="w-3.5 h-3.5 mr-2" />
|
||||||
|
{editor.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{otherEditors.length > 0 && <DropdownMenuSeparator />}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(worktree.path);
|
||||||
|
toast.success('Path copied to clipboard');
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to copy path to clipboard');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Copy className="w-3.5 h-3.5 mr-2" />
|
||||||
|
Copy Path
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{worktree.hasChanges && (
|
{worktree.hasChanges && (
|
||||||
<TooltipWrapper
|
<TooltipWrapper
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ interface WorktreeTabProps {
|
|||||||
isActivating: boolean;
|
isActivating: boolean;
|
||||||
isDevServerRunning: boolean;
|
isDevServerRunning: boolean;
|
||||||
devServerInfo?: DevServerInfo;
|
devServerInfo?: DevServerInfo;
|
||||||
defaultEditorName: string;
|
|
||||||
branches: BranchInfo[];
|
branches: BranchInfo[];
|
||||||
filteredBranches: BranchInfo[];
|
filteredBranches: BranchInfo[];
|
||||||
branchFilter: string;
|
branchFilter: string;
|
||||||
@@ -37,7 +36,7 @@ interface WorktreeTabProps {
|
|||||||
onCreateBranch: (worktree: WorktreeInfo) => void;
|
onCreateBranch: (worktree: WorktreeInfo) => void;
|
||||||
onPull: (worktree: WorktreeInfo) => void;
|
onPull: (worktree: WorktreeInfo) => void;
|
||||||
onPush: (worktree: WorktreeInfo) => void;
|
onPush: (worktree: WorktreeInfo) => void;
|
||||||
onOpenInEditor: (worktree: WorktreeInfo) => void;
|
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
|
||||||
onCommit: (worktree: WorktreeInfo) => void;
|
onCommit: (worktree: WorktreeInfo) => void;
|
||||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||||
@@ -58,7 +57,6 @@ export function WorktreeTab({
|
|||||||
isActivating,
|
isActivating,
|
||||||
isDevServerRunning,
|
isDevServerRunning,
|
||||||
devServerInfo,
|
devServerInfo,
|
||||||
defaultEditorName,
|
|
||||||
branches,
|
branches,
|
||||||
filteredBranches,
|
filteredBranches,
|
||||||
branchFilter,
|
branchFilter,
|
||||||
@@ -315,7 +313,6 @@ export function WorktreeTab({
|
|||||||
<WorktreeActionsDropdown
|
<WorktreeActionsDropdown
|
||||||
worktree={worktree}
|
worktree={worktree}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
aheadCount={aheadCount}
|
aheadCount={aheadCount}
|
||||||
behindCount={behindCount}
|
behindCount={behindCount}
|
||||||
isPulling={isPulling}
|
isPulling={isPulling}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ export { useWorktrees } from './use-worktrees';
|
|||||||
export { useDevServers } from './use-dev-servers';
|
export { useDevServers } from './use-dev-servers';
|
||||||
export { useBranches } from './use-branches';
|
export { useBranches } from './use-branches';
|
||||||
export { useWorktreeActions } from './use-worktree-actions';
|
export { useWorktreeActions } from './use-worktree-actions';
|
||||||
export { useDefaultEditor } from './use-default-editor';
|
|
||||||
export { useRunningFeatures } from './use-running-features';
|
export { useRunningFeatures } from './use-running-features';
|
||||||
|
export { useAvailableEditors, useEffectiveDefaultEditor } from './use-available-editors';
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { EditorInfo } from '@automaker/types';
|
||||||
|
|
||||||
|
const logger = createLogger('AvailableEditors');
|
||||||
|
|
||||||
|
// Re-export EditorInfo for convenience
|
||||||
|
export type { EditorInfo };
|
||||||
|
|
||||||
|
export function useAvailableEditors() {
|
||||||
|
const [editors, setEditors] = useState<EditorInfo[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh editors by clearing the server cache and re-detecting
|
||||||
|
* Use this when the user has installed/uninstalled editors
|
||||||
|
*/
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api?.worktree?.refreshEditors) {
|
||||||
|
// Fallback to regular fetch if refresh not available
|
||||||
|
await fetchAvailableEditors();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await api.worktree.refreshEditors();
|
||||||
|
if (result.success && result.result?.editors) {
|
||||||
|
setEditors(result.result.editors);
|
||||||
|
logger.info(`Editor cache refreshed, found ${result.result.editors.length} editors`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to refresh editors:', error);
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
}, [fetchAvailableEditors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAvailableEditors();
|
||||||
|
}, [fetchAvailableEditors]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
editors,
|
||||||
|
isLoading,
|
||||||
|
isRefreshing,
|
||||||
|
refresh,
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]);
|
||||||
|
}
|
||||||
@@ -125,14 +125,14 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
|
|||||||
[isPushing, fetchBranches, fetchWorktrees]
|
[isPushing, fetchBranches, fetchWorktrees]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo) => {
|
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo, editorCommand?: string) => {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.worktree?.openInEditor) {
|
if (!api?.worktree?.openInEditor) {
|
||||||
logger.warn('Open in editor API not available');
|
logger.warn('Open in editor API not available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await api.worktree.openInEditor(worktree.path);
|
const result = await api.worktree.openInEditor(worktree.path, editorCommand);
|
||||||
if (result.success && result.result) {
|
if (result.success && result.result) {
|
||||||
toast.success(result.result.message);
|
toast.success(result.result.message);
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
useDevServers,
|
useDevServers,
|
||||||
useBranches,
|
useBranches,
|
||||||
useWorktreeActions,
|
useWorktreeActions,
|
||||||
useDefaultEditor,
|
|
||||||
useRunningFeatures,
|
useRunningFeatures,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { WorktreeTab } from './components';
|
import { WorktreeTab } from './components';
|
||||||
@@ -75,8 +74,6 @@ export function WorktreePanel({
|
|||||||
fetchBranches,
|
fetchBranches,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { defaultEditorName } = useDefaultEditor();
|
|
||||||
|
|
||||||
const { hasRunningFeatures } = useRunningFeatures({
|
const { hasRunningFeatures } = useRunningFeatures({
|
||||||
runningFeatureIds,
|
runningFeatureIds,
|
||||||
features,
|
features,
|
||||||
@@ -137,7 +134,6 @@ export function WorktreePanel({
|
|||||||
isActivating={isActivating}
|
isActivating={isActivating}
|
||||||
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
||||||
devServerInfo={getDevServerInfo(mainWorktree)}
|
devServerInfo={getDevServerInfo(mainWorktree)}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
branches={branches}
|
branches={branches}
|
||||||
filteredBranches={filteredBranches}
|
filteredBranches={filteredBranches}
|
||||||
branchFilter={branchFilter}
|
branchFilter={branchFilter}
|
||||||
@@ -192,7 +188,6 @@ export function WorktreePanel({
|
|||||||
isActivating={isActivating}
|
isActivating={isActivating}
|
||||||
isDevServerRunning={isDevServerRunning(worktree)}
|
isDevServerRunning={isDevServerRunning(worktree)}
|
||||||
devServerInfo={getDevServerInfo(worktree)}
|
devServerInfo={getDevServerInfo(worktree)}
|
||||||
defaultEditorName={defaultEditorName}
|
|
||||||
branches={branches}
|
branches={branches}
|
||||||
filteredBranches={filteredBranches}
|
filteredBranches={filteredBranches}
|
||||||
branchFilter={branchFilter}
|
branchFilter={branchFilter}
|
||||||
|
|||||||
@@ -1,15 +1,51 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { LogOut, User } from 'lucide-react';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { LogOut, User, Code2, RefreshCw } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
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 {
|
||||||
|
useAvailableEditors,
|
||||||
|
useEffectiveDefaultEditor,
|
||||||
|
} from '@/components/views/board-view/worktree-panel/hooks/use-available-editors';
|
||||||
|
import { getEditorIcon } from '@/components/icons/editor-icons';
|
||||||
|
|
||||||
export function AccountSection() {
|
export function AccountSection() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
|
|
||||||
|
// Editor settings
|
||||||
|
const { editors, isLoading: isLoadingEditors, isRefreshing, refresh } = useAvailableEditors();
|
||||||
|
const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand);
|
||||||
|
const setDefaultEditorCommand = useAppStore((s) => s.setDefaultEditorCommand);
|
||||||
|
|
||||||
|
// Use shared hook for effective default editor
|
||||||
|
const effectiveEditor = useEffectiveDefaultEditor(editors);
|
||||||
|
|
||||||
|
// 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 handleRefreshEditors = async () => {
|
||||||
|
await refresh();
|
||||||
|
toast.success('Editor list refreshed');
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
try {
|
try {
|
||||||
@@ -43,6 +79,81 @@ export function AccountSection() {
|
|||||||
<p className="text-sm text-muted-foreground/80 ml-12">Manage your session and account.</p>
|
<p className="text-sm text-muted-foreground/80 ml-12">Manage your session and account.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
|
{/* Default IDE */}
|
||||||
|
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
|
||||||
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
|
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-muted/50 to-muted/30 border border-border/30 flex items-center justify-center shrink-0">
|
||||||
|
<Code2 className="w-5 h-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-foreground">Default IDE</p>
|
||||||
|
<p className="text-xs text-muted-foreground/70 mt-0.5">
|
||||||
|
Default IDE to use when opening branches or worktrees
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select
|
||||||
|
value={selectValue}
|
||||||
|
onValueChange={(value) => setDefaultEditorCommand(value === 'auto' ? null : value)}
|
||||||
|
disabled={isLoadingEditors || isRefreshing || editors.length === 0}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px] shrink-0">
|
||||||
|
<SelectValue placeholder="Select editor">
|
||||||
|
{effectiveEditor ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{EffectiveEditorIcon && <EffectiveEditorIcon className="w-4 h-4" />}
|
||||||
|
{effectiveEditor.name}
|
||||||
|
{selectValue === 'auto' && (
|
||||||
|
<span className="text-muted-foreground text-xs">(Auto)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Select editor'
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Code2 className="w-4 h-4" />
|
||||||
|
Auto-detect
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
{editors.map((editor) => {
|
||||||
|
const Icon = getEditorIcon(editor.command);
|
||||||
|
return (
|
||||||
|
<SelectItem key={editor.command} value={editor.command}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
|
{editor.name}
|
||||||
|
</span>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefreshEditors}
|
||||||
|
disabled={isRefreshing || isLoadingEditors}
|
||||||
|
className="shrink-0 h-9 w-9"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn('w-4 h-4', isRefreshing && 'animate-spin')} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Refresh available editors</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Logout */}
|
{/* Logout */}
|
||||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
|
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
|
||||||
<div className="flex items-center gap-3.5 min-w-0">
|
<div className="flex items-center gap-3.5 min-w-0">
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
|||||||
'autoLoadClaudeMd',
|
'autoLoadClaudeMd',
|
||||||
'keyboardShortcuts',
|
'keyboardShortcuts',
|
||||||
'mcpServers',
|
'mcpServers',
|
||||||
|
'defaultEditorCommand',
|
||||||
'promptCustomization',
|
'promptCustomization',
|
||||||
'projects',
|
'projects',
|
||||||
'trashedProjects',
|
'trashedProjects',
|
||||||
@@ -451,6 +452,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
|||||||
>),
|
>),
|
||||||
},
|
},
|
||||||
mcpServers: serverSettings.mcpServers,
|
mcpServers: serverSettings.mcpServers,
|
||||||
|
defaultEditorCommand: serverSettings.defaultEditorCommand ?? null,
|
||||||
promptCustomization: serverSettings.promptCustomization ?? {},
|
promptCustomization: serverSettings.promptCustomization ?? {},
|
||||||
projects: serverSettings.projects,
|
projects: serverSettings.projects,
|
||||||
trashedProjects: serverSettings.trashedProjects,
|
trashedProjects: serverSettings.trashedProjects,
|
||||||
|
|||||||
@@ -1645,13 +1645,34 @@ function createMockWorktreeAPI(): WorktreeAPI {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
openInEditor: async (worktreePath: string) => {
|
openInEditor: async (worktreePath: string, editorCommand?: string) => {
|
||||||
console.log('[Mock] Opening in editor:', worktreePath);
|
const ANTIGRAVITY_EDITOR_COMMAND = 'antigravity';
|
||||||
|
const ANTIGRAVITY_LEGACY_COMMAND = 'agy';
|
||||||
|
// 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',
|
||||||
|
[ANTIGRAVITY_EDITOR_COMMAND]: 'Antigravity',
|
||||||
|
[ANTIGRAVITY_LEGACY_COMMAND]: '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);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
message: `Opened ${worktreePath} in VS Code`,
|
message: `Opened ${worktreePath} in ${editorName}`,
|
||||||
editorName: 'VS Code',
|
editorName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -1667,6 +1688,32 @@ 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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
refreshEditors: async () => {
|
||||||
|
console.log('[Mock] Refreshing available editors');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result: {
|
||||||
|
editors: [
|
||||||
|
{ name: 'VS Code', command: 'code' },
|
||||||
|
{ name: 'Finder', command: 'open' },
|
||||||
|
],
|
||||||
|
message: 'Found 2 available editors',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
initGit: async (projectPath: string) => {
|
initGit: async (projectPath: string) => {
|
||||||
console.log('[Mock] Initializing git:', projectPath);
|
console.log('[Mock] Initializing git:', projectPath);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1635,9 +1635,11 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
this.post('/api/worktree/list-branches', { worktreePath }),
|
this.post('/api/worktree/list-branches', { worktreePath }),
|
||||||
switchBranch: (worktreePath: string, branchName: string) =>
|
switchBranch: (worktreePath: string, branchName: string) =>
|
||||||
this.post('/api/worktree/switch-branch', { worktreePath, branchName }),
|
this.post('/api/worktree/switch-branch', { worktreePath, branchName }),
|
||||||
openInEditor: (worktreePath: string) =>
|
openInEditor: (worktreePath: string, editorCommand?: string) =>
|
||||||
this.post('/api/worktree/open-in-editor', { worktreePath }),
|
this.post('/api/worktree/open-in-editor', { worktreePath, editorCommand }),
|
||||||
getDefaultEditor: () => this.get('/api/worktree/default-editor'),
|
getDefaultEditor: () => this.get('/api/worktree/default-editor'),
|
||||||
|
getAvailableEditors: () => this.get('/api/worktree/available-editors'),
|
||||||
|
refreshEditors: () => this.post('/api/worktree/refresh-editors', {}),
|
||||||
initGit: (projectPath: string) => this.post('/api/worktree/init-git', { projectPath }),
|
initGit: (projectPath: string) => this.post('/api/worktree/init-git', { projectPath }),
|
||||||
startDevServer: (projectPath: string, worktreePath: string) =>
|
startDevServer: (projectPath: string, worktreePath: string) =>
|
||||||
this.post('/api/worktree/start-dev', { projectPath, worktreePath }),
|
this.post('/api/worktree/start-dev', { projectPath, worktreePath }),
|
||||||
|
|||||||
@@ -580,6 +580,9 @@ export interface AppState {
|
|||||||
// MCP Servers
|
// MCP Servers
|
||||||
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
||||||
|
|
||||||
|
// Editor Configuration
|
||||||
|
defaultEditorCommand: string | null; // Default editor for "Open In" action
|
||||||
|
|
||||||
// Skills Configuration
|
// Skills Configuration
|
||||||
enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories)
|
enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories)
|
||||||
skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from
|
skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from
|
||||||
@@ -960,6 +963,9 @@ export interface AppActions {
|
|||||||
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
||||||
setSkipSandboxWarning: (skip: boolean) => Promise<void>;
|
setSkipSandboxWarning: (skip: boolean) => Promise<void>;
|
||||||
|
|
||||||
|
// Editor Configuration actions
|
||||||
|
setDefaultEditorCommand: (command: string | null) => void;
|
||||||
|
|
||||||
// Prompt Customization actions
|
// Prompt Customization actions
|
||||||
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
||||||
|
|
||||||
@@ -1160,6 +1166,7 @@ const initialState: AppState = {
|
|||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
|
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
|
||||||
mcpServers: [], // No MCP servers configured by default
|
mcpServers: [], // No MCP servers configured by default
|
||||||
|
defaultEditorCommand: null, // Auto-detect: Cursor > VS Code > first available
|
||||||
enableSkills: true, // Skills enabled by default
|
enableSkills: true, // Skills enabled by default
|
||||||
skillsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
skillsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
||||||
enableSubagents: true, // Subagents enabled by default
|
enableSubagents: true, // Subagents enabled by default
|
||||||
@@ -1949,6 +1956,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
set({ skipSandboxWarning: previous });
|
set({ skipSandboxWarning: previous });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Editor Configuration actions
|
||||||
|
setDefaultEditorCommand: (command) => set({ defaultEditorCommand: command }),
|
||||||
// Prompt Customization actions
|
// Prompt Customization actions
|
||||||
setPromptCustomization: async (customization) => {
|
setPromptCustomization: async (customization) => {
|
||||||
set({ promptCustomization: customization });
|
set({ promptCustomization: customization });
|
||||||
|
|||||||
29
apps/ui/src/types/electron.d.ts
vendored
29
apps/ui/src/types/electron.d.ts
vendored
@@ -884,7 +884,10 @@ export interface WorktreeAPI {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
// Open a worktree directory in the editor
|
// Open a worktree directory in the editor
|
||||||
openInEditor: (worktreePath: string) => Promise<{
|
openInEditor: (
|
||||||
|
worktreePath: string,
|
||||||
|
editorCommand?: string
|
||||||
|
) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
result?: {
|
result?: {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -903,6 +906,30 @@ export interface WorktreeAPI {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
// Get all available code editors
|
||||||
|
getAvailableEditors: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
result?: {
|
||||||
|
editors: Array<{
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Refresh editor cache and re-detect available editors
|
||||||
|
refreshEditors: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
result?: {
|
||||||
|
editors: Array<{
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
}>;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
// Initialize git repository in a project
|
// Initialize git repository in a project
|
||||||
initGit: (projectPath: string) => Promise<{
|
initGit: (projectPath: string) => Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
343
libs/platform/src/editor.ts
Normal file
343
libs/platform/src/editor.ts
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
/**
|
||||||
|
* Cross-platform editor detection and launching utilities
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Detecting available code editors on the system
|
||||||
|
* - Cross-platform editor launching (handles Windows .cmd files)
|
||||||
|
* - Caching of detected editors for performance
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execFile, spawn, type ChildProcess } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { access } from 'fs/promises';
|
||||||
|
import type { EditorInfo } from '@automaker/types';
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
// Platform detection
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
|
// Cache with TTL for editor detection
|
||||||
|
let cachedEditors: EditorInfo[] | null = null;
|
||||||
|
let cacheTimestamp: number = 0;
|
||||||
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the editor cache is still valid
|
||||||
|
*/
|
||||||
|
function isCacheValid(): boolean {
|
||||||
|
return cachedEditors !== null && Date.now() - cacheTimestamp < CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the editor detection cache
|
||||||
|
* Useful when editors may have been installed/uninstalled
|
||||||
|
*/
|
||||||
|
export function clearEditorCache(): void {
|
||||||
|
cachedEditors = null;
|
||||||
|
cacheTimestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a CLI command exists in PATH
|
||||||
|
* Uses platform-specific command lookup (where on Windows, which on Unix)
|
||||||
|
*/
|
||||||
|
export async function commandExists(cmd: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const whichCmd = isWindows ? 'where' : 'which';
|
||||||
|
await execFileAsync(whichCmd, [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 (!isMac) return null;
|
||||||
|
|
||||||
|
// Check /Applications first
|
||||||
|
const systemAppPath = join('/Applications', `${appName}.app`);
|
||||||
|
try {
|
||||||
|
await access(systemAppPath);
|
||||||
|
return systemAppPath;
|
||||||
|
} catch {
|
||||||
|
// Not in /Applications
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ~/Applications (used by JetBrains Toolbox and others)
|
||||||
|
const userAppPath = join(homedir(), 'Applications', `${appName}.app`);
|
||||||
|
try {
|
||||||
|
await access(userAppPath);
|
||||||
|
return userAppPath;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Editor definition with CLI command and macOS app bundle name
|
||||||
|
*/
|
||||||
|
interface EditorDefinition {
|
||||||
|
name: string;
|
||||||
|
cliCommand: string;
|
||||||
|
cliAliases?: readonly string[];
|
||||||
|
macAppName: string;
|
||||||
|
/** If true, only available on macOS */
|
||||||
|
macOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANTIGRAVITY_CLI_COMMANDS = ['antigravity', 'agy'] as const;
|
||||||
|
const [PRIMARY_ANTIGRAVITY_COMMAND, ...LEGACY_ANTIGRAVITY_COMMANDS] = ANTIGRAVITY_CLI_COMMANDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of supported editors in priority order
|
||||||
|
*/
|
||||||
|
const SUPPORTED_EDITORS: EditorDefinition[] = [
|
||||||
|
{ name: 'Cursor', cliCommand: 'cursor', macAppName: 'Cursor' },
|
||||||
|
{ name: 'VS Code', cliCommand: 'code', macAppName: 'Visual Studio Code' },
|
||||||
|
{
|
||||||
|
name: 'VS Code Insiders',
|
||||||
|
cliCommand: 'code-insiders',
|
||||||
|
macAppName: 'Visual Studio Code - Insiders',
|
||||||
|
},
|
||||||
|
{ name: 'Kiro', cliCommand: 'kiro', macAppName: 'Kiro' },
|
||||||
|
{ name: 'Zed', cliCommand: 'zed', macAppName: 'Zed' },
|
||||||
|
{ name: 'Sublime Text', cliCommand: 'subl', macAppName: 'Sublime Text' },
|
||||||
|
{ name: 'Windsurf', cliCommand: 'windsurf', macAppName: 'Windsurf' },
|
||||||
|
{ name: 'Trae', cliCommand: 'trae', macAppName: 'Trae' },
|
||||||
|
{ name: 'Rider', cliCommand: 'rider', macAppName: 'Rider' },
|
||||||
|
{ name: 'WebStorm', cliCommand: 'webstorm', macAppName: 'WebStorm' },
|
||||||
|
{ name: 'Xcode', cliCommand: 'xed', macAppName: 'Xcode', macOnly: true },
|
||||||
|
{ name: 'Android Studio', cliCommand: 'studio', macAppName: 'Android Studio' },
|
||||||
|
{
|
||||||
|
name: 'Antigravity',
|
||||||
|
cliCommand: PRIMARY_ANTIGRAVITY_COMMAND,
|
||||||
|
cliAliases: LEGACY_ANTIGRAVITY_COMMANDS,
|
||||||
|
macAppName: 'Antigravity',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Xcode is fully installed (not just Command Line Tools)
|
||||||
|
* xed command requires full Xcode.app, not just CLT
|
||||||
|
*/
|
||||||
|
async function isXcodeFullyInstalled(): Promise<boolean> {
|
||||||
|
if (!isMac) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if xcode-select points to full Xcode, not just CommandLineTools
|
||||||
|
const { stdout } = await execFileAsync('xcode-select', ['-p']);
|
||||||
|
const devPath = stdout.trim();
|
||||||
|
|
||||||
|
// Full Xcode path: /Applications/Xcode.app/Contents/Developer
|
||||||
|
// Command Line Tools: /Library/Developer/CommandLineTools
|
||||||
|
const isPointingToXcode = devPath.includes('Xcode.app');
|
||||||
|
|
||||||
|
if (!isPointingToXcode && devPath.includes('CommandLineTools')) {
|
||||||
|
// Check if xed command exists (indicates CLT are installed)
|
||||||
|
const xedExists = await commandExists('xed');
|
||||||
|
|
||||||
|
// Check if Xcode.app actually exists
|
||||||
|
const xcodeAppPath = await findMacApp('Xcode');
|
||||||
|
|
||||||
|
if (xedExists && xcodeAppPath) {
|
||||||
|
console.warn(
|
||||||
|
'Xcode is installed but xcode-select is pointing to Command Line Tools. ' +
|
||||||
|
'To use Xcode as an editor, run: sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPointingToXcode;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to find an editor - checks CLI first, then macOS app bundle
|
||||||
|
* Returns EditorInfo if found, null otherwise
|
||||||
|
*/
|
||||||
|
async function findEditor(definition: EditorDefinition): Promise<EditorInfo | null> {
|
||||||
|
// Skip macOS-only editors on other platforms
|
||||||
|
if (definition.macOnly && !isMac) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for Xcode: verify full installation, not just xed command
|
||||||
|
if (definition.name === 'Xcode') {
|
||||||
|
if (!(await isXcodeFullyInstalled())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try CLI command first (works on all platforms)
|
||||||
|
const cliCandidates = [definition.cliCommand, ...(definition.cliAliases ?? [])];
|
||||||
|
for (const cliCommand of cliCandidates) {
|
||||||
|
if (await commandExists(cliCommand)) {
|
||||||
|
return { name: definition.name, command: cliCommand };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try macOS app bundle (checks /Applications and ~/Applications)
|
||||||
|
if (isMac) {
|
||||||
|
const appPath = await findMacApp(definition.macAppName);
|
||||||
|
if (appPath) {
|
||||||
|
// Use 'open -a' with full path for apps not in /Applications
|
||||||
|
return { name: definition.name, command: `open -a "${appPath}"` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the platform-specific file manager
|
||||||
|
*/
|
||||||
|
function getFileManagerInfo(): EditorInfo {
|
||||||
|
if (isMac) {
|
||||||
|
return { name: 'Finder', command: 'open' };
|
||||||
|
} else if (isWindows) {
|
||||||
|
return { name: 'Explorer', command: 'explorer' };
|
||||||
|
} else {
|
||||||
|
return { name: 'File Manager', command: 'xdg-open' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect all available code editors on the system
|
||||||
|
* Results are cached for 5 minutes for performance
|
||||||
|
*/
|
||||||
|
export async function detectAllEditors(): Promise<EditorInfo[]> {
|
||||||
|
// Return cached result if still valid
|
||||||
|
if (isCacheValid() && cachedEditors) {
|
||||||
|
return cachedEditors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all editors in parallel for better performance
|
||||||
|
const editorChecks = SUPPORTED_EDITORS.map((def) => findEditor(def));
|
||||||
|
const results = await Promise.all(editorChecks);
|
||||||
|
|
||||||
|
// Filter out null results (editors not found)
|
||||||
|
const editors = results.filter((e): e is EditorInfo => e !== null);
|
||||||
|
|
||||||
|
// Always add file manager as fallback
|
||||||
|
editors.push(getFileManagerInfo());
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
cachedEditors = editors;
|
||||||
|
cacheTimestamp = Date.now();
|
||||||
|
|
||||||
|
return editors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the default (first available) code editor on the system
|
||||||
|
* Returns the highest priority editor that is installed
|
||||||
|
*/
|
||||||
|
export async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||||
|
const editors = await detectAllEditors();
|
||||||
|
// Return first editor (highest priority) - always exists due to file manager fallback
|
||||||
|
return editors[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a specific editor by command
|
||||||
|
* Returns the editor info if available, null otherwise
|
||||||
|
*/
|
||||||
|
export async function findEditorByCommand(command: string): Promise<EditorInfo | null> {
|
||||||
|
const editors = await detectAllEditors();
|
||||||
|
return editors.find((e) => e.command === command) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a path in the specified editor
|
||||||
|
*
|
||||||
|
* Handles cross-platform differences:
|
||||||
|
* - On Windows, uses spawn with shell:true to handle .cmd batch scripts
|
||||||
|
* - On macOS, handles 'open -a' style commands for app bundles
|
||||||
|
* - On Linux, uses direct execution
|
||||||
|
*
|
||||||
|
* @param targetPath - The file or directory path to open
|
||||||
|
* @param editorCommand - The editor command to use (optional, uses default if not specified)
|
||||||
|
* @returns Promise that resolves with editor info when launched, rejects on error
|
||||||
|
*/
|
||||||
|
export async function openInEditor(
|
||||||
|
targetPath: string,
|
||||||
|
editorCommand?: string
|
||||||
|
): Promise<{ editorName: string }> {
|
||||||
|
// Determine which editor to use
|
||||||
|
let editor: EditorInfo;
|
||||||
|
|
||||||
|
if (editorCommand) {
|
||||||
|
const found = await findEditorByCommand(editorCommand);
|
||||||
|
if (found) {
|
||||||
|
editor = found;
|
||||||
|
} else {
|
||||||
|
// Fall back to default if specified editor not found
|
||||||
|
editor = await detectDefaultEditor();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editor = await detectDefaultEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the editor
|
||||||
|
await executeEditorCommand(editor.command, targetPath);
|
||||||
|
|
||||||
|
return { editorName: editor.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an editor command with a path argument
|
||||||
|
* Handles platform-specific differences in command execution
|
||||||
|
*/
|
||||||
|
async function executeEditorCommand(command: string, targetPath: string): Promise<void> {
|
||||||
|
// Handle 'open -a "AppPath"' style commands (macOS app bundles)
|
||||||
|
if (command.startsWith('open -a ')) {
|
||||||
|
const appPath = command.replace('open -a ', '').replace(/"/g, '');
|
||||||
|
await execFileAsync('open', ['-a', appPath, targetPath]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, editor CLI commands are typically .cmd batch scripts
|
||||||
|
// spawn with shell:true is required to execute them properly
|
||||||
|
if (isWindows) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child: ChildProcess = spawn(command, [targetPath], {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unref to allow the parent process to exit independently
|
||||||
|
child.unref();
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resolve after a small delay to catch immediate spawn errors
|
||||||
|
// Editors run in background, so we don't wait for them to exit
|
||||||
|
setTimeout(() => resolve(), 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix/macOS: use execFile for direct execution
|
||||||
|
await execFileAsync(command, [targetPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a path in the platform's default file manager
|
||||||
|
* Always available as a fallback option
|
||||||
|
*/
|
||||||
|
export async function openInFileManager(targetPath: string): Promise<{ editorName: string }> {
|
||||||
|
const fileManager = getFileManagerInfo();
|
||||||
|
await execFileAsync(fileManager.command, [targetPath]);
|
||||||
|
return { editorName: fileManager.name };
|
||||||
|
}
|
||||||
@@ -157,3 +157,14 @@ export {
|
|||||||
|
|
||||||
// Port configuration
|
// Port configuration
|
||||||
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './config/ports.js';
|
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './config/ports.js';
|
||||||
|
|
||||||
|
// Editor detection and launching (cross-platform)
|
||||||
|
export {
|
||||||
|
commandExists,
|
||||||
|
clearEditorCache,
|
||||||
|
detectAllEditors,
|
||||||
|
detectDefaultEditor,
|
||||||
|
findEditorByCommand,
|
||||||
|
openInEditor,
|
||||||
|
openInFileManager,
|
||||||
|
} from './editor.js';
|
||||||
|
|||||||
13
libs/types/src/editor.ts
Normal file
13
libs/types/src/editor.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Editor types for the "Open In" functionality
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about an available code editor
|
||||||
|
*/
|
||||||
|
export interface EditorInfo {
|
||||||
|
/** Display name of the editor (e.g., "VS Code", "Cursor") */
|
||||||
|
name: string;
|
||||||
|
/** CLI command or open command to launch the editor */
|
||||||
|
command: string;
|
||||||
|
}
|
||||||
@@ -214,6 +214,9 @@ export type {
|
|||||||
// Port configuration
|
// Port configuration
|
||||||
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js';
|
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js';
|
||||||
|
|
||||||
|
// Editor types
|
||||||
|
export type { EditorInfo } from './editor.js';
|
||||||
|
|
||||||
// Ideation types
|
// Ideation types
|
||||||
export type {
|
export type {
|
||||||
IdeaCategory,
|
IdeaCategory,
|
||||||
|
|||||||
@@ -460,6 +460,10 @@ export interface GlobalSettings {
|
|||||||
/** List of configured MCP servers for agent use */
|
/** List of configured MCP servers for agent use */
|
||||||
mcpServers: MCPServerConfig[];
|
mcpServers: MCPServerConfig[];
|
||||||
|
|
||||||
|
// Editor Configuration
|
||||||
|
/** Default editor command for "Open In" action (null = auto-detect: Cursor > VS Code > first available) */
|
||||||
|
defaultEditorCommand: string | null;
|
||||||
|
|
||||||
// Prompt Customization
|
// Prompt Customization
|
||||||
/** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */
|
/** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */
|
||||||
promptCustomization?: PromptCustomization;
|
promptCustomization?: PromptCustomization;
|
||||||
@@ -712,6 +716,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS,
|
codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS,
|
||||||
codexThreadId: undefined,
|
codexThreadId: undefined,
|
||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
|
defaultEditorCommand: null,
|
||||||
enableSkills: true,
|
enableSkills: true,
|
||||||
skillsSources: ['user', 'project'],
|
skillsSources: ['user', 'project'],
|
||||||
enableSubagents: true,
|
enableSubagents: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user