From 491e5bb99eb7b6cb54d2f70ad889405fd470fcc3 Mon Sep 17 00:00:00 2001 From: everpcpc Date: Mon, 19 Jan 2026 21:54:58 +0800 Subject: [PATCH] Add auto-delete branch on worktree remove --- .../desktop/src/infrastructure/ipc/project.ts | 37 ++++++++++++++++++- packages/desktop/src/preload.ts | 4 +- packages/ui/src/components/SettingsDialog.tsx | 32 ++++++++++++++++ packages/ui/src/components/Sidebar.tsx | 6 +-- packages/ui/src/stores/settingsStore.ts | 4 ++ packages/ui/src/types/electron.d.ts | 2 +- packages/ui/src/utils/api.ts | 4 +- 7 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/infrastructure/ipc/project.ts b/packages/desktop/src/infrastructure/ipc/project.ts index ac275f6..a5776b4 100644 --- a/packages/desktop/src/infrastructure/ipc/project.ts +++ b/packages/desktop/src/infrastructure/ipc/project.ts @@ -90,7 +90,7 @@ export function registerProjectHandlers(ipcMain: IpcMain, services: AppServices) } }); - ipcMain.handle('projects:remove-worktree', async (_event, projectId: number, worktreePath: string, sessionId?: string | null) => { + ipcMain.handle('projects:remove-worktree', async (_event, projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean) => { try { const project = databaseService.getProject(projectId); if (!project) return { success: false, error: 'Project not found' }; @@ -102,6 +102,24 @@ export function registerProjectHandlers(ipcMain: IpcMain, services: AppServices) return { success: false, error: 'Cannot remove main repository worktree' }; } + // Get the branch name before removing the worktree + let branchName: string | null = null; + if (autoDeleteBranch) { + try { + const { stdout: branchOut } = await gitExecutor.run({ + sessionId, + cwd: normalizedWorktreePath, + argv: ['git', 'branch', '--show-current'], + kind: 'git.command', + op: 'read', + meta: { source: 'workspace', operation: 'worktree-branch-probe', worktreePath: normalizedWorktreePath }, + }); + branchName = branchOut.trim(); + } catch { + // ignore - worktree might already be deleted or inaccessible + } + } + await gitExecutor.run({ sessionId, cwd: project.path, @@ -112,6 +130,23 @@ export function registerProjectHandlers(ipcMain: IpcMain, services: AppServices) treatAsSuccessIfOutputIncludes: ['is not a working tree', 'does not exist', 'No such file or directory'], }); + // Delete the branch if requested and we got a branch name + if (autoDeleteBranch && branchName) { + try { + await gitExecutor.run({ + sessionId, + cwd: project.path, + argv: ['git', 'branch', '-D', branchName], + kind: 'git.command', + op: 'write', + meta: { source: 'workspace', operation: 'branch-delete', branch: branchName }, + treatAsSuccessIfOutputIncludes: ['not found'], + }); + } catch { + // ignore - branch might not exist or already deleted + } + } + // Remove any sessions that were attached to this worktree. const sessions = databaseService.getAllSessionsIncludingArchived().filter( (s) => s.project_id === projectId && s.worktree_path?.replace(/\/+$/, '') === normalizedWorktreePath diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index 88fb2df..890d56a 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -37,8 +37,8 @@ contextBridge.exposeInMainWorld('electronAPI', { delete: (projectId: number): Promise => ipcRenderer.invoke('projects:delete', projectId), getWorktrees: (projectId: number, sessionId?: string | null): Promise => ipcRenderer.invoke('projects:get-worktrees', projectId, sessionId), - removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null): Promise => - ipcRenderer.invoke('projects:remove-worktree', projectId, worktreePath, sessionId), + removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean): Promise => + ipcRenderer.invoke('projects:remove-worktree', projectId, worktreePath, sessionId, autoDeleteBranch), renameWorktree: (projectId: number, worktreePath: string, nextName: string, sessionId?: string | null): Promise => ipcRenderer.invoke('projects:rename-worktree', projectId, worktreePath, nextName, sessionId), }, diff --git a/packages/ui/src/components/SettingsDialog.tsx b/packages/ui/src/components/SettingsDialog.tsx index 5220b42..5056947 100644 --- a/packages/ui/src/components/SettingsDialog.tsx +++ b/packages/ui/src/components/SettingsDialog.tsx @@ -266,6 +266,38 @@ export function SettingsDialog() { + + {/* Worktree Settings */} +
+

+ Worktree Settings +

+
+
+ + +
+
+
{/* Footer */} diff --git a/packages/ui/src/components/Sidebar.tsx b/packages/ui/src/components/Sidebar.tsx index 1edb029..a87a17b 100644 --- a/packages/ui/src/components/Sidebar.tsx +++ b/packages/ui/src/components/Sidebar.tsx @@ -46,7 +46,7 @@ const applyBaseCommitSuffix = (name: string, baseCommit?: string): string => { export function Sidebar() { const { showError } = useErrorStore(); const { sessions, activeSessionId, setActiveSession } = useSessionStore(); - const { openSettings } = useSettingsStore(); + const { openSettings, settings } = useSettingsStore(); const [projects, setProjects] = useState([]); const [activeProjectId, setActiveProjectId] = useState(null); const [collapsedProjects, setCollapsedProjects] = useState>(() => new Set()); @@ -291,7 +291,7 @@ export function Sidebar() { ...prev, [project.id]: (prev[project.id] || []).filter((w) => w.path !== worktree.path), })); - const res = await API.projects.removeWorktree(project.id, worktree.path, activeSessionId); + const res = await API.projects.removeWorktree(project.id, worktree.path, activeSessionId, settings.autoDeleteBranchOnWorktreeRemove); if (!res.success) { showError({ title: 'Failed to Delete Workspace', error: res.error || 'Could not delete worktree' }); void loadWorktrees(project, { silent: true }); @@ -301,7 +301,7 @@ export function Sidebar() { } catch (error) { showError({ title: 'Failed to Delete Workspace', error: error instanceof Error ? error.message : 'Unknown error' }); } - }, [showError, loadWorktrees, activeSessionId]); + }, [showError, loadWorktrees, activeSessionId, settings.autoDeleteBranchOnWorktreeRemove]); const handleDeleteProject = useCallback(async (project: Project) => { const res = await API.projects.delete(project.id); diff --git a/packages/ui/src/stores/settingsStore.ts b/packages/ui/src/stores/settingsStore.ts index a0e5676..662db5c 100644 --- a/packages/ui/src/stores/settingsStore.ts +++ b/packages/ui/src/stores/settingsStore.ts @@ -17,6 +17,9 @@ export interface AppSettings { // Terminal terminalFontSize: number; terminalScrollback: number; + + // Worktree + autoDeleteBranchOnWorktreeRemove: boolean; } const DEFAULT_SETTINGS: AppSettings = { @@ -29,6 +32,7 @@ const DEFAULT_SETTINGS: AppSettings = { }, terminalFontSize: 13, terminalScrollback: 1000, + autoDeleteBranchOnWorktreeRemove: false, }; interface SettingsStore { diff --git a/packages/ui/src/types/electron.d.ts b/packages/ui/src/types/electron.d.ts index 6b2dc32..2f086f4 100644 --- a/packages/ui/src/types/electron.d.ts +++ b/packages/ui/src/types/electron.d.ts @@ -92,7 +92,7 @@ export interface ElectronAPI { deletions: number; filesChanged: number; }>>>; - removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null) => Promise>; + removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean) => Promise>; renameWorktree: (projectId: number, worktreePath: string, nextName: string, sessionId?: string | null) => Promise>; }; diff --git a/packages/ui/src/utils/api.ts b/packages/ui/src/utils/api.ts index ed99cb2..2ac588d 100644 --- a/packages/ui/src/utils/api.ts +++ b/packages/ui/src/utils/api.ts @@ -246,9 +246,9 @@ export class API { return window.electronAPI.projects.getWorktrees(projectId, sessionId); }, - async removeWorktree(projectId: number, worktreePath: string, sessionId?: string | null) { + async removeWorktree(projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean) { requireElectron(); - return window.electronAPI.projects.removeWorktree(projectId, worktreePath, sessionId); + return window.electronAPI.projects.removeWorktree(projectId, worktreePath, sessionId, autoDeleteBranch); }, async renameWorktree(projectId: number, worktreePath: string, nextName: string, sessionId?: string | null) {