Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion packages/desktop/src/infrastructure/ipc/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand All @@ -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,
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
delete: (projectId: number): Promise<IPCResponse> => ipcRenderer.invoke('projects:delete', projectId),
getWorktrees: (projectId: number, sessionId?: string | null): Promise<IPCResponse> =>
ipcRenderer.invoke('projects:get-worktrees', projectId, sessionId),
removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null): Promise<IPCResponse> =>
ipcRenderer.invoke('projects:remove-worktree', projectId, worktreePath, sessionId),
removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean): Promise<IPCResponse> =>
ipcRenderer.invoke('projects:remove-worktree', projectId, worktreePath, sessionId, autoDeleteBranch),
renameWorktree: (projectId: number, worktreePath: string, nextName: string, sessionId?: string | null): Promise<IPCResponse> =>
ipcRenderer.invoke('projects:rename-worktree', projectId, worktreePath, nextName, sessionId),
},
Expand Down
32 changes: 32 additions & 0 deletions packages/ui/src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,38 @@ export function SettingsDialog() {
</div>
</div>
</section>

{/* Worktree Settings */}
<section className="space-y-3">
<h3 className="text-sm font-semibold" style={{ color: 'var(--st-text)' }}>
Worktree Settings
</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm" style={{ color: 'var(--st-text-muted)' }}>
Auto-delete branch on worktree remove
</label>
<button
type="button"
onClick={() => updateSettings({
autoDeleteBranchOnWorktreeRemove: !settings.autoDeleteBranchOnWorktreeRemove
})}
className="flex-shrink-0 w-10 h-5 cursor-pointer rounded-full p-0.5"
role="switch"
aria-checked={settings.autoDeleteBranchOnWorktreeRemove}
style={{
backgroundColor: settings.autoDeleteBranchOnWorktreeRemove ? 'var(--st-accent)' : 'var(--st-border)',
transition: 'background-color 0.2s'
}}
>
<span
className="block h-4 w-4 bg-white rounded-full transition-transform"
style={{ transform: settings.autoDeleteBranchOnWorktreeRemove ? 'translateX(1.25rem)' : 'translateX(0)' }}
/>
</button>
</div>
</div>
</section>
</div>

{/* Footer */}
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Project[]>([]);
const [activeProjectId, setActiveProjectId] = useState<number | null>(null);
const [collapsedProjects, setCollapsedProjects] = useState<Set<number>>(() => new Set());
Expand Down Expand Up @@ -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 });
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/stores/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export interface AppSettings {
// Terminal
terminalFontSize: number;
terminalScrollback: number;

// Worktree
autoDeleteBranchOnWorktreeRemove: boolean;
}

const DEFAULT_SETTINGS: AppSettings = {
Expand All @@ -29,6 +32,7 @@ const DEFAULT_SETTINGS: AppSettings = {
},
terminalFontSize: 13,
terminalScrollback: 1000,
autoDeleteBranchOnWorktreeRemove: false,
};

interface SettingsStore {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export interface ElectronAPI {
deletions: number;
filesChanged: number;
}>>>;
removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null) => Promise<IPCResponse<unknown>>;
removeWorktree: (projectId: number, worktreePath: string, sessionId?: string | null, autoDeleteBranch?: boolean) => Promise<IPCResponse<unknown>>;
renameWorktree: (projectId: number, worktreePath: string, nextName: string, sessionId?: string | null) => Promise<IPCResponse<{ path: string } | unknown>>;
};

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading