From d103d0aa45c9ee4333f80b83f3d810f37274bb95 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Tue, 16 Dec 2025 12:35:36 -0500 Subject: [PATCH] default editor fixes, fix bug with worktree panel not showing --- .../components/worktree-selector.tsx | 25 ++- apps/app/src/lib/electron.ts | 14 +- apps/app/src/lib/http-api-client.ts | 2 + apps/app/src/types/electron.d.ts | 11 ++ apps/server/src/routes/worktree/index.ts | 3 +- .../routes/worktree/routes/open-in-editor.ts | 146 ++++++++++++++---- 6 files changed, 160 insertions(+), 41 deletions(-) diff --git a/apps/app/src/components/views/board-view/components/worktree-selector.tsx b/apps/app/src/components/views/board-view/components/worktree-selector.tsx index fed25640..d0b0765e 100644 --- a/apps/app/src/components/views/board-view/components/worktree-selector.tsx +++ b/apps/app/src/components/views/board-view/components/worktree-selector.tsx @@ -101,6 +101,7 @@ export function WorktreeSelector({ const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [branchFilter, setBranchFilter] = useState(""); const [runningDevServers, setRunningDevServers] = useState>(new Map()); + const [defaultEditorName, setDefaultEditorName] = useState("Editor"); const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath)); const setCurrentWorktree = useAppStore((s) => s.setCurrentWorktree); const setWorktreesInStore = useAppStore((s) => s.setWorktrees); @@ -145,6 +146,21 @@ export function WorktreeSelector({ } }, []); + const fetchDefaultEditor = useCallback(async () => { + try { + const api = getElectronAPI(); + if (!api?.worktree?.getDefaultEditor) { + return; + } + const result = await api.worktree.getDefaultEditor(); + if (result.success && result.result?.editorName) { + setDefaultEditorName(result.result.editorName); + } + } catch (error) { + console.error("Failed to fetch default editor:", error); + } + }, []); + const fetchBranches = useCallback(async (worktreePath: string) => { setIsLoadingBranches(true); try { @@ -169,7 +185,8 @@ export function WorktreeSelector({ useEffect(() => { fetchWorktrees(); fetchDevServers(); - }, [fetchWorktrees, fetchDevServers]); + fetchDefaultEditor(); + }, [fetchWorktrees, fetchDevServers, fetchDefaultEditor]); // Refresh when refreshTrigger changes (but skip the initial render) useEffect(() => { @@ -442,10 +459,6 @@ export function WorktreeSelector({ ? worktrees.find((w) => w.path === currentWorktree) : worktrees.find((w) => w.isMain); - if (worktrees.length === 0 && !isLoading) { - // No git repo or loading - return null; - } // Render a worktree tab with branch selector (for main) and actions dropdown const renderWorktreeTab = (worktree: WorktreeInfo) => { @@ -707,7 +720,7 @@ export function WorktreeSelector({ className="text-xs" > - Open in Editor + Open in {defaultEditorName} {/* Commit changes */} diff --git a/apps/app/src/lib/electron.ts b/apps/app/src/lib/electron.ts index 38fa4336..4f6f6e24 100644 --- a/apps/app/src/lib/electron.ts +++ b/apps/app/src/lib/electron.ts @@ -1222,7 +1222,19 @@ function createMockWorktreeAPI(): WorktreeAPI { return { success: true, result: { - message: `Opened ${worktreePath} in editor`, + message: `Opened ${worktreePath} in VS Code`, + editorName: "VS Code", + }, + }; + }, + + getDefaultEditor: async () => { + console.log("[Mock] Getting default editor"); + return { + success: true, + result: { + editorName: "VS Code", + editorCommand: "code", }, }; }, diff --git a/apps/app/src/lib/http-api-client.ts b/apps/app/src/lib/http-api-client.ts index ae9233d5..1149ef3b 100644 --- a/apps/app/src/lib/http-api-client.ts +++ b/apps/app/src/lib/http-api-client.ts @@ -612,6 +612,8 @@ export class HttpApiClient implements ElectronAPI { this.post("/api/worktree/switch-branch", { worktreePath, branchName }), openInEditor: (worktreePath: string) => this.post("/api/worktree/open-in-editor", { worktreePath }), + getDefaultEditor: () => + this.get("/api/worktree/default-editor"), initGit: (projectPath: string) => this.post("/api/worktree/init-git", { projectPath }), activate: (projectPath: string, worktreePath: string | null) => diff --git a/apps/app/src/types/electron.d.ts b/apps/app/src/types/electron.d.ts index 63a1b623..b90f1c9c 100644 --- a/apps/app/src/types/electron.d.ts +++ b/apps/app/src/types/electron.d.ts @@ -796,6 +796,17 @@ export interface WorktreeAPI { success: boolean; result?: { message: string; + editorName?: string; + }; + error?: string; + }>; + + // Get the default code editor name + getDefaultEditor: () => Promise<{ + success: boolean; + result?: { + editorName: string; + editorCommand: string; }; error?: string; }>; diff --git a/apps/server/src/routes/worktree/index.ts b/apps/server/src/routes/worktree/index.ts index 6c4fb870..4c9761cb 100644 --- a/apps/server/src/routes/worktree/index.ts +++ b/apps/server/src/routes/worktree/index.ts @@ -19,7 +19,7 @@ import { createPullHandler } from "./routes/pull.js"; import { createCheckoutBranchHandler } from "./routes/checkout-branch.js"; import { createListBranchesHandler } from "./routes/list-branches.js"; import { createSwitchBranchHandler } from "./routes/switch-branch.js"; -import { createOpenInEditorHandler } from "./routes/open-in-editor.js"; +import { createOpenInEditorHandler, createGetDefaultEditorHandler } from "./routes/open-in-editor.js"; import { createInitGitHandler } from "./routes/init-git.js"; import { createActivateHandler } from "./routes/activate.js"; import { createMigrateHandler } from "./routes/migrate.js"; @@ -47,6 +47,7 @@ export function createWorktreeRoutes(): Router { router.post("/list-branches", createListBranchesHandler()); router.post("/switch-branch", createSwitchBranchHandler()); router.post("/open-in-editor", createOpenInEditorHandler()); + router.get("/default-editor", createGetDefaultEditorHandler()); router.post("/init-git", createInitGitHandler()); router.post("/activate", createActivateHandler()); router.post("/migrate", createMigrateHandler()); 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 868227ee..04f9815f 100644 --- a/apps/server/src/routes/worktree/routes/open-in-editor.ts +++ b/apps/server/src/routes/worktree/routes/open-in-editor.ts @@ -1,5 +1,6 @@ /** - * POST /open-in-editor endpoint - Open a worktree directory in VS Code + * 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 */ import type { Request, Response } from "express"; @@ -9,6 +10,89 @@ import { getErrorMessage, logError } from "../common.js"; const execAsync = promisify(exec); +// Editor detection with caching +interface EditorInfo { + name: string; + command: string; +} + +let cachedEditor: EditorInfo | null = null; + +/** + * Detect which code editor is available on the system + */ +async function detectDefaultEditor(): Promise { + // Return cached result if available + 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() { + return async (_req: Request, res: Response): Promise => { + try { + const editor = await detectDefaultEditor(); + res.json({ + success: true, + result: { + editorName: editor.name, + editorCommand: editor.command, + }, + }); + } catch (error) { + logError(error, "Get default editor failed"); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} + export function createOpenInEditorHandler() { return async (req: Request, res: Response): Promise => { try { @@ -24,46 +108,42 @@ export function createOpenInEditorHandler() { return; } - // Try to open in VS Code + const editor = await detectDefaultEditor(); + try { - await execAsync(`code "${worktreePath}"`); + await execAsync(`${editor.command} "${worktreePath}"`); res.json({ success: true, result: { - message: `Opened ${worktreePath} in VS Code`, + message: `Opened ${worktreePath} in ${editor.name}`, + editorName: editor.name, }, }); - } catch { - // If 'code' command fails, try 'cursor' (for Cursor editor) - try { - await execAsync(`cursor "${worktreePath}"`); - res.json({ - success: true, - result: { - message: `Opened ${worktreePath} in Cursor`, - }, - }); - } catch { - // If both fail, try opening in default file manager - const platform = process.platform; - let openCommand: string; + } catch (editorError) { + // If the detected editor fails, try opening in default file manager as fallback + const platform = process.platform; + let openCommand: string; + let fallbackName: string; - if (platform === "darwin") { - openCommand = `open "${worktreePath}"`; - } else if (platform === "win32") { - openCommand = `explorer "${worktreePath}"`; - } else { - openCommand = `xdg-open "${worktreePath}"`; - } - - await execAsync(openCommand); - res.json({ - success: true, - result: { - message: `Opened ${worktreePath} in file manager`, - }, - }); + if (platform === "darwin") { + openCommand = `open "${worktreePath}"`; + fallbackName = "Finder"; + } else if (platform === "win32") { + openCommand = `explorer "${worktreePath}"`; + fallbackName = "Explorer"; + } else { + openCommand = `xdg-open "${worktreePath}"`; + fallbackName = "File Manager"; } + + await execAsync(openCommand); + res.json({ + success: true, + result: { + message: `Opened ${worktreePath} in ${fallbackName}`, + editorName: fallbackName, + }, + }); } } catch (error) { logError(error, "Open in editor failed");