From 678893dbefffc72e662e7b6d5312a0a802e19890 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Nov 2025 16:02:59 -0600 Subject: [PATCH 1/5] conditionally render tabs again --- .../ContentView/TabsContent/index.tsx | 103 +++--------------- 1 file changed, 18 insertions(+), 85 deletions(-) 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..824c4edc1ca 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,124 +1,57 @@ 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} -
- ); -} - export function TabsContent() { const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); const activeWorkspaceId = activeWorkspace?.id; 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.Single ? ( + <> + + {isDropZone && } + + ) : ( + <> + + {isDropZone && } + + )}
); } From 73c2f06d0f96ad570f3b09031c907b93c566f377 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Nov 2025 16:06:36 -0600 Subject: [PATCH 2/5] fix delete worktree --- .../lib/trpc/routers/workspaces/workspaces.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index ccc9812ac90..4fc5558f066 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -324,7 +324,29 @@ export const createWorkspacesRouter = () => { if (worktree && project) { try { - await removeWorktree(project.mainRepoPath, worktree.path); + // Check if worktree exists in git before attempting removal + const gitInstance = simpleGit(project.mainRepoPath); + const worktrees = await gitInstance.raw([ + "worktree", + "list", + "--porcelain", + ]); + + // Parse porcelain format to verify worktree exists + const lines = worktrees.split("\n"); + const worktreePrefix = `worktree ${worktree.path}`; + const worktreeExists = lines.some( + (line) => line.trim() === worktreePrefix, + ); + + // Only attempt removal if worktree exists in git + if (worktreeExists) { + 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 = From 49566def1bc069cea60bf45610cf4777c0d2141f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Nov 2025 16:12:30 -0600 Subject: [PATCH 3/5] fix format --- .../routers/projects/utils/colors/colors.ts | 4 ++- .../lib/trpc/routers/workspaces/utils/git.ts | 25 +++++++++++++++++++ .../lib/trpc/routers/workspaces/workspaces.ts | 1 + apps/desktop/src/main/lib/terminal-manager.ts | 3 ++- .../ContentView/TabsContent/index.tsx | 16 +++++------- 5 files changed, 37 insertions(+), 12 deletions(-) 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 4fc5558f066..e77b515c8f4 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -10,6 +10,7 @@ import { createWorktree, generateBranchName, removeWorktree, + worktreeExists, } from "./utils/git"; import { copySetupFiles, loadSetupConfig } from "./utils/setup"; 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 824c4edc1ca..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 @@ -6,6 +6,7 @@ import { EmptyTabView } from "./EmptyTabView"; import { GroupTabView } from "./GroupTabView"; import { SingleTabView } from "./SingleTabView"; import { useTabContentDrop } from "./useTabContentDrop"; +import { SetupTabView } from "./SetupTabView"; export function TabsContent() { const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); @@ -41,17 +42,12 @@ export function TabsContent() { return (
- {tabToRender.type === TabType.Single ? ( - <> - - {isDropZone && } - - ) : ( - <> - - {isDropZone && } - + {tabToRender.type === TabType.Setup && } + {tabToRender.type === TabType.Single && ( + )} + {tabToRender.type === TabType.Group && } + {isDropZone && }
); } From dadafa450b9a52e741f3e286e9f8aafa62bad429 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Nov 2025 16:14:28 -0600 Subject: [PATCH 4/5] fix format --- .../lib/trpc/routers/workspaces/workspaces.ts | 40 ++++--------------- .../src/shared/constants/project-colors.ts | 2 +- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index e77b515c8f4..740f94c9d3d 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -3,7 +3,6 @@ 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 { @@ -258,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, @@ -325,23 +313,12 @@ export const createWorkspacesRouter = () => { if (worktree && project) { try { - // Check if worktree exists in git before attempting removal - const gitInstance = simpleGit(project.mainRepoPath); - const worktrees = await gitInstance.raw([ - "worktree", - "list", - "--porcelain", - ]); - - // Parse porcelain format to verify worktree exists - 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, ); - // Only attempt removal if worktree exists in git - if (worktreeExists) { + if (exists) { await removeWorktree(project.mainRepoPath, worktree.path); } else { console.warn( @@ -349,7 +326,6 @@ export const createWorkspacesRouter = () => { ); } } 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/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" }, From 65eea9abe35698f9ea93978c3c714eeed3e4ddb3 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 26 Nov 2025 16:22:14 -0600 Subject: [PATCH 5/5] add show in finder --- apps/desktop/src/lib/trpc/routers/external/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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({