diff --git a/apps/desktop/src/lib/trpc/routers/external/index.ts b/apps/desktop/src/lib/trpc/routers/external/index.ts index 29c879f7d2c..58c58b3c68a 100644 --- a/apps/desktop/src/lib/trpc/routers/external/index.ts +++ b/apps/desktop/src/lib/trpc/routers/external/index.ts @@ -39,6 +39,12 @@ export const createExternalRouter = () => { await shell.openExternal(input); }), + openInFinder: publicProcedure + .input(z.string()) + .mutation(async ({ input }) => { + shell.showItemInFolder(input); + }), + openFileInEditor: publicProcedure .input( z.object({ diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts index c43b9bb9271..656864ad077 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/colors/colors.ts @@ -1,5 +1,7 @@ import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors"; export function assignRandomColor(): string { - return PROJECT_COLOR_VALUES[Math.floor(Math.random() * PROJECT_COLOR_VALUES.length)]; + return PROJECT_COLOR_VALUES[ + Math.floor(Math.random() * PROJECT_COLOR_VALUES.length) + ]; } diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts index 4a713bfa11a..b036f5fe583 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -89,3 +89,28 @@ export async function getGitRoot(path: string): Promise { throw new Error(`Not a git repository: ${path}`); } } + +/** + * Checks if a worktree exists in git's worktree list + * @param mainRepoPath - Path to the main repository + * @param worktreePath - Path to the worktree to check + * @returns true if the worktree exists in git, false otherwise + */ +export async function worktreeExists( + mainRepoPath: string, + worktreePath: string, +): Promise { + try { + const git = simpleGit(mainRepoPath); + const worktrees = await git.raw(["worktree", "list", "--porcelain"]); + + // Parse porcelain format to verify worktree exists + // Format: "worktree /path/to/worktree" followed by HEAD, branch, etc. + const lines = worktrees.split("\n"); + const worktreePrefix = `worktree ${worktreePath}`; + return lines.some((line) => line.trim() === worktreePrefix); + } catch (error) { + console.error(`Failed to check worktree existence: ${error}`); + throw error; + } +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index ccc9812ac90..740f94c9d3d 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -3,13 +3,13 @@ import { join } from "node:path"; import { db } from "main/lib/db"; import { nanoid } from "nanoid"; import { SUPERSET_DIR_NAME, WORKTREES_DIR_NAME } from "shared/constants"; -import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../.."; import { createWorktree, generateBranchName, removeWorktree, + worktreeExists, } from "./utils/git"; import { copySetupFiles, loadSetupConfig } from "./utils/setup"; @@ -257,23 +257,12 @@ export const createWorkspacesRouter = () => { if (worktree && project) { try { - const gitInstance = simpleGit(project.mainRepoPath); - const worktrees = await gitInstance.raw([ - "worktree", - "list", - "--porcelain", - ]); - - // Parse porcelain format to verify worktree exists in git before deletion - // (porcelain format: "worktree /path/to/worktree" followed by HEAD, branch, etc.) - const lines = worktrees.split("\n"); - const worktreePrefix = `worktree ${worktree.path}`; - const worktreeExists = lines.some( - (line) => line.trim() === worktreePrefix, + const exists = await worktreeExists( + project.mainRepoPath, + worktree.path, ); - if (!worktreeExists) { - // Worktree doesn't exist in git, but we can still delete the workspace + if (!exists) { return { canDelete: true, reason: null, @@ -324,9 +313,19 @@ export const createWorkspacesRouter = () => { if (worktree && project) { try { - await removeWorktree(project.mainRepoPath, worktree.path); + const exists = await worktreeExists( + project.mainRepoPath, + worktree.path, + ); + + if (exists) { + await removeWorktree(project.mainRepoPath, worktree.path); + } else { + console.warn( + `Worktree ${worktree.path} not found in git, skipping removal`, + ); + } } catch (error) { - // If worktree removal fails, return error and don't proceed with DB cleanup const errorMessage = error instanceof Error ? error.message : String(error); console.error("Failed to remove worktree:", errorMessage); diff --git a/apps/desktop/src/main/lib/terminal-manager.ts b/apps/desktop/src/main/lib/terminal-manager.ts index 21830868e08..6796167c26c 100644 --- a/apps/desktop/src/main/lib/terminal-manager.ts +++ b/apps/desktop/src/main/lib/terminal-manager.ts @@ -89,7 +89,8 @@ export class TerminalManager extends EventEmitter { // Spawn as login shell (-l for zsh/bash) to source profile files // This ensures pyenv, nvm, etc. are initialized before .zshrc runs - const shellArgs = shell.includes("zsh") || shell.includes("bash") ? ["-l"] : []; + const shellArgs = + shell.includes("zsh") || shell.includes("bash") ? ["-l"] : []; const ptyProcess = pty.spawn(shell, shellArgs, { name: "xterm-256color", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx index f4c4ebaa112..2c84622f03f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/index.tsx @@ -1,50 +1,12 @@ import { useMemo } from "react"; import { trpc } from "renderer/lib/trpc"; -import type { Tab } from "renderer/stores"; import { TabType, useActiveTabIds, useTabs } from "renderer/stores"; import { DropOverlay } from "./DropOverlay"; import { EmptyTabView } from "./EmptyTabView"; import { GroupTabView } from "./GroupTabView"; -import { SetupTabView } from "./SetupTabView"; import { SingleTabView } from "./SingleTabView"; import { useTabContentDrop } from "./useTabContentDrop"; - -interface RenderTabContentProps { - tab: Tab; - activeTabId: string | null; - isDropZone: boolean; -} - -function renderTabContent({ - tab, - activeTabId, - isDropZone, -}: RenderTabContentProps) { - const isActive = tab.id === activeTabId; - const content = (() => { - switch (tab.type) { - case TabType.Setup: - return ; - case TabType.Single: - return ; - case TabType.Group: - return ; - default: - return null; - } - })(); - - const style: React.CSSProperties = { - visibility: isActive ? "visible" : "hidden", - pointerEvents: isActive ? "auto" : "none", - }; - - return ( -
- {content} -
- ); -} +import { SetupTabView } from "./SetupTabView"; export function TabsContent() { const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); @@ -52,73 +14,40 @@ export function TabsContent() { const allTabs = useTabs(); const activeTabIds = useActiveTabIds(); - const { tabToRender, allTabs: renderedTabs } = useMemo(() => { - if (!activeWorkspaceId) return { tabToRender: null, allTabs: [] }; + const tabToRender = useMemo(() => { + if (!activeWorkspaceId) return null; const activeTabId = activeTabIds[activeWorkspaceId]; - - // Get all top-level tabs (tabs without parent) - const topLevelTabs = allTabs.filter((tab) => !tab.parentId); - - if (!activeTabId) { - return { tabToRender: null, allTabs: topLevelTabs }; - } + if (!activeTabId) return null; const activeTab = allTabs.find((tab) => tab.id === activeTabId); - if (!activeTab) { - return { tabToRender: null, allTabs: topLevelTabs }; - } + if (!activeTab) return null; - let displayTab = activeTab; if (activeTab.parentId) { const parentGroup = allTabs.find((tab) => tab.id === activeTab.parentId); - displayTab = parentGroup || activeTab; + return parentGroup || null; } - return { tabToRender: displayTab, allTabs: topLevelTabs }; + return activeTab; }, [activeWorkspaceId, activeTabIds, allTabs]); const { isDropZone, attachDrop } = useTabContentDrop(tabToRender); - const activeTabId = tabToRender?.id ?? null; - if (!tabToRender) { return ( -
+
- {renderedTabs.map((tab) => { - return ( -
- {renderTabContent({ - tab, - activeTabId: null, - isDropZone: false, - })} -
- ); - })}
); } - const dropOverlayMessage = - tabToRender.type === TabType.Single - ? "Drop to create split view" - : "Drop to add to split view"; - return (
- {renderedTabs.map((tab) => { - return ( -
- {renderTabContent({ - tab, - activeTabId, - isDropZone, - })} -
- ); - })} - {isDropZone && } + {tabToRender.type === TabType.Setup && } + {tabToRender.type === TabType.Single && ( + + )} + {tabToRender.type === TabType.Group && } + {isDropZone && }
); } diff --git a/apps/desktop/src/shared/constants/project-colors.ts b/apps/desktop/src/shared/constants/project-colors.ts index 33bee73b437..dc3776be19b 100644 --- a/apps/desktop/src/shared/constants/project-colors.ts +++ b/apps/desktop/src/shared/constants/project-colors.ts @@ -1,4 +1,4 @@ -export const PROJECT_COLORS = [ +export const PROJECT_COLORS: { name: string; value: string }[] = [ { name: "Blue", value: "#3b82f6" }, { name: "Green", value: "#22c55e" }, { name: "Yellow", value: "#eab308" },