2025-12-07 19:32:53 +02:00
|
|
|
import React from 'react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-01-19 02:50:40 +02:00
|
|
|
import { toast } from '@/components/ui';
|
2026-01-07 12:08:53 +02:00
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from '@/components/ui/dialog';
|
2026-01-17 22:30:30 +02:00
|
|
|
import { RiCheckboxBlankLine, RiCheckboxLine, RiDeleteBinLine, RiGitBranchLine } from '@remixicon/react';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
|
|
|
|
import { DirectoryExplorerDialog } from './DirectoryExplorerDialog';
|
|
|
|
|
import { cn, formatPathForDisplay } from '@/lib/utils';
|
2026-01-03 13:39:59 +02:00
|
|
|
import type { Session } from '@opencode-ai/sdk/v2';
|
2025-12-07 19:32:53 +02:00
|
|
|
import type { WorktreeMetadata } from '@/types/worktree';
|
2026-02-06 12:31:03 +02:00
|
|
|
import { getWorktreeStatus } from '@/lib/worktrees/worktreeStatus';
|
2026-01-27 19:59:48 +02:00
|
|
|
import { removeProjectWorktree } from '@/lib/worktrees/worktreeManager';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { useSessionStore } from '@/stores/useSessionStore';
|
|
|
|
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
2026-01-06 21:31:04 +02:00
|
|
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
2026-02-24 03:28:30 +02:00
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
2026-01-06 21:31:04 +02:00
|
|
|
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
|
2026-02-11 20:07:44 +02:00
|
|
|
import { isDesktopLocalOriginActive, isTauriShell } from '@/lib/desktop';
|
2025-12-07 19:32:53 +02:00
|
|
|
import { useDeviceInfo } from '@/lib/device';
|
|
|
|
|
import { sessionEvents } from '@/lib/sessionEvents';
|
|
|
|
|
|
|
|
|
|
const renderToastDescription = (text?: string) =>
|
|
|
|
|
text ? <span className="text-foreground/80 dark:text-foreground/70">{text}</span> : undefined;
|
|
|
|
|
|
|
|
|
|
const normalizeProjectDirectory = (path: string | null | undefined): string => {
|
|
|
|
|
if (!path) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
const replaced = path.replace(/\\/g, '/');
|
|
|
|
|
if (replaced === '/') {
|
|
|
|
|
return '/';
|
|
|
|
|
}
|
|
|
|
|
return replaced.replace(/\/+$/, '');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DeleteDialogState = {
|
|
|
|
|
sessions: Session[];
|
|
|
|
|
dateLabel?: string;
|
|
|
|
|
mode: 'session' | 'worktree';
|
|
|
|
|
worktree?: WorktreeMetadata | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const SessionDialogs: React.FC = () => {
|
|
|
|
|
const [isDirectoryDialogOpen, setIsDirectoryDialogOpen] = React.useState(false);
|
|
|
|
|
const [hasShownInitialDirectoryPrompt, setHasShownInitialDirectoryPrompt] = React.useState(false);
|
|
|
|
|
const [deleteDialog, setDeleteDialog] = React.useState<DeleteDialogState | null>(null);
|
|
|
|
|
const [deleteDialogSummaries, setDeleteDialogSummaries] = React.useState<Array<{ session: Session; metadata: WorktreeMetadata }>>([]);
|
|
|
|
|
const [deleteDialogShouldRemoveRemote, setDeleteDialogShouldRemoveRemote] = React.useState(false);
|
2026-02-13 19:18:22 +02:00
|
|
|
const [deleteDialogShouldDeleteLocalBranch, setDeleteDialogShouldDeleteLocalBranch] = React.useState(false);
|
2025-12-07 19:32:53 +02:00
|
|
|
const [isProcessingDelete, setIsProcessingDelete] = React.useState(false);
|
2026-02-06 12:31:03 +02:00
|
|
|
const [hasCompletedDirtyCheck, setHasCompletedDirtyCheck] = React.useState(false);
|
|
|
|
|
const [dirtyWorktreePaths, setDirtyWorktreePaths] = React.useState<Set<string>>(new Set());
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
deleteSession,
|
|
|
|
|
deleteSessions,
|
|
|
|
|
loadSessions,
|
|
|
|
|
getWorktreeMetadata,
|
|
|
|
|
} = useSessionStore();
|
2026-02-24 03:28:30 +02:00
|
|
|
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
|
|
|
|
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
2026-01-07 22:06:28 +02:00
|
|
|
const { currentDirectory, homeDirectory, isHomeReady } = useDirectoryStore();
|
2026-01-06 21:31:04 +02:00
|
|
|
const { projects, addProject, activeProjectId } = useProjectsStore();
|
|
|
|
|
const { requestAccess, startAccessing } = useFileSystemAccess();
|
2025-12-07 19:32:53 +02:00
|
|
|
const { isMobile, isTablet, hasTouchInput } = useDeviceInfo();
|
|
|
|
|
const useMobileOverlay = isMobile || isTablet || hasTouchInput;
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const projectDirectory = React.useMemo(() => {
|
2026-01-07 22:06:28 +02:00
|
|
|
const targetProject = activeProjectId
|
|
|
|
|
? projects.find((project) => project.id === activeProjectId) ?? null
|
2026-01-06 21:31:04 +02:00
|
|
|
: null;
|
|
|
|
|
const targetPath = targetProject?.path ?? currentDirectory;
|
|
|
|
|
return normalizeProjectDirectory(targetPath);
|
2026-01-07 22:06:28 +02:00
|
|
|
}, [activeProjectId, currentDirectory, projects]);
|
|
|
|
|
|
2026-01-30 15:40:01 +02:00
|
|
|
const getProjectRefForWorktree = React.useCallback((worktree: WorktreeMetadata) => {
|
|
|
|
|
const normalized = normalizeProjectDirectory(worktree.projectDirectory);
|
|
|
|
|
const fallbackPath = normalized || projectDirectory;
|
|
|
|
|
const match = projects.find((project) => normalizeProjectDirectory(project.path) === fallbackPath) ?? null;
|
|
|
|
|
return { id: match?.id ?? `path:${fallbackPath}`, path: fallbackPath };
|
|
|
|
|
}, [projectDirectory, projects]);
|
|
|
|
|
|
2026-02-06 12:31:03 +02:00
|
|
|
const hasDirtyWorktrees = hasCompletedDirtyCheck && dirtyWorktreePaths.size > 0;
|
2025-12-07 19:32:53 +02:00
|
|
|
const canRemoveRemoteBranches = React.useMemo(
|
|
|
|
|
() => {
|
|
|
|
|
const targetWorktree = deleteDialog?.worktree;
|
|
|
|
|
if (targetWorktree && typeof targetWorktree.branch === 'string' && targetWorktree.branch.trim().length > 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
deleteDialogSummaries.length > 0 &&
|
|
|
|
|
deleteDialogSummaries.every(({ metadata }) => typeof metadata.branch === 'string' && metadata.branch.trim().length > 0)
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
[deleteDialog?.worktree, deleteDialogSummaries],
|
|
|
|
|
);
|
|
|
|
|
const isWorktreeDelete = deleteDialog?.mode === 'worktree';
|
|
|
|
|
const shouldArchiveWorktree = isWorktreeDelete;
|
|
|
|
|
const removeRemoteOptionDisabled =
|
|
|
|
|
isProcessingDelete || !isWorktreeDelete || !canRemoveRemoteBranches;
|
2026-02-13 19:18:22 +02:00
|
|
|
const deleteLocalOptionDisabled = isProcessingDelete || !isWorktreeDelete;
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
loadSessions();
|
|
|
|
|
}, [loadSessions, currentDirectory]);
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const projectsKey = React.useMemo(
|
|
|
|
|
() => projects.map((project) => `${project.id}:${project.path}`).join('|'),
|
|
|
|
|
[projects],
|
|
|
|
|
);
|
|
|
|
|
const lastProjectsKeyRef = React.useRef(projectsKey);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
React.useEffect(() => {
|
2026-01-06 21:31:04 +02:00
|
|
|
if (projectsKey === lastProjectsKeyRef.current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastProjectsKeyRef.current = projectsKey;
|
|
|
|
|
loadSessions();
|
|
|
|
|
}, [loadSessions, projectsKey]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (hasShownInitialDirectoryPrompt || !isHomeReady || projects.length > 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setHasShownInitialDirectoryPrompt(true);
|
|
|
|
|
|
2026-02-11 20:07:44 +02:00
|
|
|
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
2026-01-06 21:31:04 +02:00
|
|
|
requestAccess('')
|
|
|
|
|
.then(async (result) => {
|
|
|
|
|
if (!result.success || !result.path) {
|
|
|
|
|
if (result.error && result.error !== 'Directory selection cancelled') {
|
|
|
|
|
toast.error('Failed to select directory', {
|
|
|
|
|
description: result.error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accessResult = await startAccessing(result.path);
|
|
|
|
|
if (!accessResult.success) {
|
|
|
|
|
toast.error('Failed to open directory', {
|
|
|
|
|
description: accessResult.error || 'Desktop could not grant file access.',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const added = addProject(result.path, { id: result.projectId });
|
|
|
|
|
if (!added) {
|
|
|
|
|
toast.error('Failed to add project', {
|
|
|
|
|
description: 'Please select a valid directory path.',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error('Desktop: Error selecting directory:', error);
|
|
|
|
|
toast.error('Failed to select directory');
|
|
|
|
|
});
|
|
|
|
|
return;
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
|
|
|
|
|
setIsDirectoryDialogOpen(true);
|
|
|
|
|
}, [
|
|
|
|
|
addProject,
|
|
|
|
|
hasShownInitialDirectoryPrompt,
|
|
|
|
|
isHomeReady,
|
|
|
|
|
projects.length,
|
|
|
|
|
requestAccess,
|
|
|
|
|
startAccessing,
|
|
|
|
|
]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const openDeleteDialog = React.useCallback((payload: { sessions: Session[]; dateLabel?: string; mode?: 'session' | 'worktree'; worktree?: WorktreeMetadata | null }) => {
|
|
|
|
|
setDeleteDialog({
|
|
|
|
|
sessions: payload.sessions,
|
|
|
|
|
dateLabel: payload.dateLabel,
|
|
|
|
|
mode: payload.mode ?? 'session',
|
|
|
|
|
worktree: payload.worktree ?? null,
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const closeDeleteDialog = React.useCallback(() => {
|
|
|
|
|
setDeleteDialog(null);
|
|
|
|
|
setDeleteDialogSummaries([]);
|
|
|
|
|
setDeleteDialogShouldRemoveRemote(false);
|
2026-02-13 19:18:22 +02:00
|
|
|
setDeleteDialogShouldDeleteLocalBranch(false);
|
2025-12-07 19:32:53 +02:00
|
|
|
setIsProcessingDelete(false);
|
2026-02-06 12:31:03 +02:00
|
|
|
setHasCompletedDirtyCheck(false);
|
|
|
|
|
setDirtyWorktreePaths(new Set());
|
2025-12-07 19:32:53 +02:00
|
|
|
}, []);
|
|
|
|
|
|
2026-02-24 03:28:30 +02:00
|
|
|
const deleteSessionsWithoutDialog = React.useCallback(async (payload: { sessions: Session[]; dateLabel?: string }) => {
|
|
|
|
|
if (payload.sessions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload.sessions.length === 1) {
|
|
|
|
|
const target = payload.sessions[0];
|
|
|
|
|
const success = await deleteSession(target.id);
|
|
|
|
|
if (success) {
|
|
|
|
|
toast.success('Session deleted');
|
|
|
|
|
} else {
|
|
|
|
|
toast.error('Failed to delete session');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ids = payload.sessions.map((session) => session.id);
|
|
|
|
|
const { deletedIds, failedIds } = await deleteSessions(ids);
|
|
|
|
|
|
|
|
|
|
if (deletedIds.length > 0) {
|
|
|
|
|
const successDescription = failedIds.length > 0
|
|
|
|
|
? `${failedIds.length} session${failedIds.length === 1 ? '' : 's'} could not be deleted.`
|
|
|
|
|
: payload.dateLabel
|
|
|
|
|
? `Removed all sessions from ${payload.dateLabel}.`
|
|
|
|
|
: undefined;
|
|
|
|
|
toast.success(`Deleted ${deletedIds.length} session${deletedIds.length === 1 ? '' : 's'}`, {
|
|
|
|
|
description: renderToastDescription(successDescription),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (failedIds.length > 0) {
|
|
|
|
|
toast.error(`Failed to delete ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`, {
|
|
|
|
|
description: renderToastDescription('Please try again in a moment.'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [deleteSession, deleteSessions]);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
return sessionEvents.onDeleteRequest((payload) => {
|
2026-02-24 03:28:30 +02:00
|
|
|
if (!showDeletionDialog && (payload.mode ?? 'session') === 'session') {
|
|
|
|
|
void deleteSessionsWithoutDialog(payload);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
openDeleteDialog(payload);
|
|
|
|
|
});
|
2026-02-24 03:28:30 +02:00
|
|
|
}, [openDeleteDialog, showDeletionDialog, deleteSessionsWithoutDialog]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
return sessionEvents.onDirectoryRequest(() => {
|
|
|
|
|
setIsDirectoryDialogOpen(true);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!deleteDialog) {
|
|
|
|
|
setDeleteDialogSummaries([]);
|
|
|
|
|
setDeleteDialogShouldRemoveRemote(false);
|
2026-02-13 19:18:22 +02:00
|
|
|
setDeleteDialogShouldDeleteLocalBranch(false);
|
2026-02-06 12:31:03 +02:00
|
|
|
setHasCompletedDirtyCheck(false);
|
|
|
|
|
setDirtyWorktreePaths(new Set());
|
2025-12-07 19:32:53 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summaries = deleteDialog.sessions
|
|
|
|
|
.map((session) => {
|
|
|
|
|
const metadata = getWorktreeMetadata(session.id);
|
|
|
|
|
return metadata ? { session, metadata } : null;
|
|
|
|
|
})
|
|
|
|
|
.filter((entry): entry is { session: Session; metadata: WorktreeMetadata } => Boolean(entry));
|
|
|
|
|
|
|
|
|
|
setDeleteDialogSummaries(summaries);
|
|
|
|
|
setDeleteDialogShouldRemoveRemote(false);
|
2026-02-06 12:31:03 +02:00
|
|
|
setHasCompletedDirtyCheck(false);
|
|
|
|
|
setDirtyWorktreePaths(new Set());
|
|
|
|
|
|
|
|
|
|
const metadataByPath = new Map<string, WorktreeMetadata>();
|
|
|
|
|
if (deleteDialog.worktree?.path) {
|
|
|
|
|
metadataByPath.set(normalizeProjectDirectory(deleteDialog.worktree.path), deleteDialog.worktree);
|
|
|
|
|
}
|
|
|
|
|
summaries.forEach(({ metadata }) => {
|
|
|
|
|
if (metadata.path) {
|
|
|
|
|
metadataByPath.set(normalizeProjectDirectory(metadata.path), metadata);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-02-06 12:31:03 +02:00
|
|
|
if (metadataByPath.size === 0) {
|
|
|
|
|
setHasCompletedDirtyCheck(true);
|
2025-12-07 19:32:53 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cancelled = false;
|
|
|
|
|
|
|
|
|
|
(async () => {
|
2026-02-06 12:31:03 +02:00
|
|
|
const statusByPath = new Map<string, WorktreeMetadata['status']>();
|
|
|
|
|
const nextDirtyPaths = new Set<string>();
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
Array.from(metadataByPath.entries()).map(async ([pathKey, metadata]) => {
|
2025-12-07 19:32:53 +02:00
|
|
|
try {
|
2026-02-06 12:31:03 +02:00
|
|
|
const status = await getWorktreeStatus(metadata.path);
|
|
|
|
|
statusByPath.set(pathKey, status);
|
|
|
|
|
if (status?.isDirty) {
|
|
|
|
|
nextDirtyPaths.add(pathKey);
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
} catch {
|
2026-02-06 12:31:03 +02:00
|
|
|
if (metadata.status) {
|
|
|
|
|
statusByPath.set(pathKey, metadata.status);
|
|
|
|
|
if (metadata.status.isDirty) {
|
|
|
|
|
nextDirtyPaths.add(pathKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
).catch((error) => {
|
|
|
|
|
console.warn('Failed to inspect worktree status before deletion:', error);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-06 12:31:03 +02:00
|
|
|
if (cancelled) {
|
2025-12-07 19:32:53 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 12:31:03 +02:00
|
|
|
setDirtyWorktreePaths(nextDirtyPaths);
|
|
|
|
|
setHasCompletedDirtyCheck(true);
|
|
|
|
|
|
|
|
|
|
setDeleteDialog((prev) => {
|
|
|
|
|
if (!prev?.worktree?.path) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
const pathKey = normalizeProjectDirectory(prev.worktree.path);
|
|
|
|
|
const nextStatus = statusByPath.get(pathKey);
|
|
|
|
|
if (!nextStatus) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
const prevStatus = prev.worktree.status;
|
|
|
|
|
if (
|
|
|
|
|
prevStatus?.isDirty === nextStatus.isDirty &&
|
|
|
|
|
prevStatus?.ahead === nextStatus.ahead &&
|
|
|
|
|
prevStatus?.behind === nextStatus.behind &&
|
|
|
|
|
prevStatus?.upstream === nextStatus.upstream
|
|
|
|
|
) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
worktree: {
|
|
|
|
|
...prev.worktree,
|
|
|
|
|
status: nextStatus,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
setDeleteDialogSummaries((prev) =>
|
2026-02-06 12:31:03 +02:00
|
|
|
prev.map((entry) => {
|
|
|
|
|
const pathKey = normalizeProjectDirectory(entry.metadata.path);
|
|
|
|
|
const nextStatus = statusByPath.get(pathKey);
|
|
|
|
|
if (!nextStatus) {
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
session: entry.session,
|
|
|
|
|
metadata: { ...entry.metadata, status: nextStatus },
|
|
|
|
|
};
|
|
|
|
|
})
|
2025-12-07 19:32:53 +02:00
|
|
|
);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true;
|
|
|
|
|
};
|
|
|
|
|
}, [deleteDialog, getWorktreeMetadata]);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (!canRemoveRemoteBranches) {
|
|
|
|
|
setDeleteDialogShouldRemoveRemote(false);
|
|
|
|
|
}
|
|
|
|
|
}, [canRemoveRemoteBranches]);
|
|
|
|
|
|
2026-02-13 19:18:22 +02:00
|
|
|
const removeSelectedWorktree = React.useCallback(async (
|
|
|
|
|
worktree: WorktreeMetadata,
|
|
|
|
|
deleteLocalBranch: boolean
|
|
|
|
|
): Promise<boolean> => {
|
|
|
|
|
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
|
|
|
|
try {
|
|
|
|
|
await removeProjectWorktree(
|
|
|
|
|
getProjectRefForWorktree(worktree),
|
|
|
|
|
worktree,
|
|
|
|
|
{ deleteRemoteBranch: shouldRemoveRemote, deleteLocalBranch }
|
|
|
|
|
);
|
|
|
|
|
return true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast.error('Failed to remove worktree', {
|
|
|
|
|
description: renderToastDescription(error instanceof Error ? error.message : 'Please try again.'),
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}, [canRemoveRemoteBranches, deleteDialogShouldRemoveRemote, getProjectRefForWorktree]);
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
const handleConfirmDelete = React.useCallback(async () => {
|
|
|
|
|
if (!deleteDialog) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setIsProcessingDelete(true);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-03 01:20:56 +02:00
|
|
|
const shouldArchive = shouldArchiveWorktree;
|
|
|
|
|
const removeRemoteBranch = shouldArchive && deleteDialogShouldRemoveRemote;
|
2026-02-13 19:18:22 +02:00
|
|
|
const deleteLocalBranch = shouldArchive && deleteDialogShouldDeleteLocalBranch;
|
2026-01-03 01:20:56 +02:00
|
|
|
|
|
|
|
|
if (deleteDialog.sessions.length === 0 && isWorktreeDelete && deleteDialog.worktree) {
|
2026-02-13 19:18:22 +02:00
|
|
|
const removed = await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
|
|
|
|
if (!removed) {
|
2026-01-30 15:40:01 +02:00
|
|
|
closeDeleteDialog();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-13 19:18:22 +02:00
|
|
|
const shouldRemoveRemote = deleteDialogShouldRemoveRemote && canRemoveRemoteBranches;
|
2026-01-03 01:20:56 +02:00
|
|
|
const archiveNote = shouldRemoveRemote ? 'Worktree and remote branch removed.' : 'Worktree removed.';
|
|
|
|
|
toast.success('Worktree removed', {
|
|
|
|
|
description: renderToastDescription(archiveNote),
|
|
|
|
|
});
|
|
|
|
|
closeDeleteDialog();
|
2026-01-03 02:37:55 +02:00
|
|
|
loadSessions();
|
2026-01-03 01:20:56 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
if (deleteDialog.sessions.length === 1) {
|
|
|
|
|
const target = deleteDialog.sessions[0];
|
|
|
|
|
const success = await deleteSession(target.id, {
|
2026-01-30 15:40:01 +02:00
|
|
|
// In "worktree" mode, remove the selected worktree explicitly below.
|
|
|
|
|
// Don't try to derive worktree removal from per-session metadata (may be missing).
|
|
|
|
|
archiveWorktree: isWorktreeDelete ? false : shouldArchive,
|
2025-12-07 19:32:53 +02:00
|
|
|
deleteRemoteBranch: removeRemoteBranch,
|
2026-02-13 19:18:22 +02:00
|
|
|
deleteLocalBranch,
|
2025-12-07 19:32:53 +02:00
|
|
|
});
|
|
|
|
|
if (!success) {
|
|
|
|
|
toast.error('Failed to delete session');
|
|
|
|
|
setIsProcessingDelete(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-30 15:40:01 +02:00
|
|
|
const archiveNote = !isWorktreeDelete && shouldArchive
|
2025-12-07 19:32:53 +02:00
|
|
|
? removeRemoteBranch
|
|
|
|
|
? 'Worktree and remote branch removed.'
|
|
|
|
|
: 'Attached worktree archived.'
|
|
|
|
|
: undefined;
|
|
|
|
|
toast.success('Session deleted', {
|
|
|
|
|
description: renderToastDescription(archiveNote),
|
2026-01-18 18:28:49 +06:00
|
|
|
action: {
|
|
|
|
|
label: 'OK',
|
|
|
|
|
onClick: () => { },
|
|
|
|
|
},
|
2025-12-07 19:32:53 +02:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
const ids = deleteDialog.sessions.map((session) => session.id);
|
|
|
|
|
const { deletedIds, failedIds } = await deleteSessions(ids, {
|
2026-01-30 15:40:01 +02:00
|
|
|
archiveWorktree: isWorktreeDelete ? false : shouldArchive,
|
2025-12-07 19:32:53 +02:00
|
|
|
deleteRemoteBranch: removeRemoteBranch,
|
2026-02-13 19:18:22 +02:00
|
|
|
deleteLocalBranch,
|
2025-12-07 19:32:53 +02:00
|
|
|
});
|
|
|
|
|
|
2026-01-30 15:40:01 +02:00
|
|
|
if (isWorktreeDelete && deleteDialog.worktree && failedIds.length === 0) {
|
|
|
|
|
// Remove selected worktree even if per-session metadata is missing.
|
|
|
|
|
// Use same projectRef logic as the no-sessions path.
|
2026-02-13 19:18:22 +02:00
|
|
|
const removed = await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
|
|
|
|
if (removed) {
|
2026-01-30 15:40:01 +02:00
|
|
|
await loadSessions();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
if (deletedIds.length > 0) {
|
2026-01-30 15:40:01 +02:00
|
|
|
const archiveNote = !isWorktreeDelete && shouldArchive
|
2025-12-07 19:32:53 +02:00
|
|
|
? removeRemoteBranch
|
|
|
|
|
? 'Archived worktrees and removed remote branches.'
|
|
|
|
|
: 'Attached worktrees archived.'
|
|
|
|
|
: undefined;
|
|
|
|
|
const successDescription =
|
|
|
|
|
failedIds.length > 0
|
|
|
|
|
? `${failedIds.length} session${failedIds.length === 1 ? '' : 's'} could not be deleted.`
|
|
|
|
|
: deleteDialog.dateLabel
|
|
|
|
|
? `Removed all sessions from ${deleteDialog.dateLabel}.`
|
|
|
|
|
: undefined;
|
|
|
|
|
const combinedDescription = [successDescription, archiveNote].filter(Boolean).join(' ');
|
|
|
|
|
toast.success(`Deleted ${deletedIds.length} session${deletedIds.length === 1 ? '' : 's'}`, {
|
|
|
|
|
description: renderToastDescription(combinedDescription || undefined),
|
2026-01-18 18:28:49 +06:00
|
|
|
action: {
|
|
|
|
|
label: 'OK',
|
|
|
|
|
onClick: () => { },
|
|
|
|
|
},
|
2025-12-07 19:32:53 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (failedIds.length > 0) {
|
|
|
|
|
toast.error(`Failed to delete ${failedIds.length} session${failedIds.length === 1 ? '' : 's'}`, {
|
|
|
|
|
description: renderToastDescription('Please try again in a moment.'),
|
|
|
|
|
});
|
|
|
|
|
if (deletedIds.length === 0) {
|
|
|
|
|
setIsProcessingDelete(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 15:40:01 +02:00
|
|
|
if (isWorktreeDelete && deleteDialog.sessions.length === 1 && deleteDialog.worktree) {
|
2026-02-13 19:18:22 +02:00
|
|
|
const removed = await removeSelectedWorktree(deleteDialog.worktree, deleteLocalBranch);
|
|
|
|
|
if (removed) {
|
2026-01-30 15:40:01 +02:00
|
|
|
await loadSessions();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
closeDeleteDialog();
|
|
|
|
|
} finally {
|
|
|
|
|
setIsProcessingDelete(false);
|
|
|
|
|
}
|
2026-01-27 19:59:48 +02:00
|
|
|
}, [
|
|
|
|
|
deleteDialog,
|
|
|
|
|
deleteDialogShouldRemoveRemote,
|
2026-02-13 19:18:22 +02:00
|
|
|
deleteDialogShouldDeleteLocalBranch,
|
2026-01-27 19:59:48 +02:00
|
|
|
deleteSession,
|
|
|
|
|
deleteSessions,
|
|
|
|
|
closeDeleteDialog,
|
|
|
|
|
shouldArchiveWorktree,
|
|
|
|
|
isWorktreeDelete,
|
|
|
|
|
canRemoveRemoteBranches,
|
2026-02-13 19:18:22 +02:00
|
|
|
removeSelectedWorktree,
|
2026-01-27 19:59:48 +02:00
|
|
|
loadSessions,
|
|
|
|
|
]);
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
const targetWorktree = deleteDialog?.worktree ?? deleteDialogSummaries[0]?.metadata ?? null;
|
|
|
|
|
const deleteDialogDescription = deleteDialog
|
|
|
|
|
? deleteDialog.mode === 'worktree'
|
2026-01-03 01:20:56 +02:00
|
|
|
? deleteDialog.sessions.length === 0
|
|
|
|
|
? 'This removes the selected worktree.'
|
|
|
|
|
: `This removes the selected worktree and ${deleteDialog.sessions.length === 1 ? '1 linked session' : `${deleteDialog.sessions.length} linked sessions`}.`
|
2025-12-07 19:32:53 +02:00
|
|
|
: `This action permanently removes ${deleteDialog.sessions.length === 1 ? '1 session' : `${deleteDialog.sessions.length} sessions`}${deleteDialog.dateLabel ? ` from ${deleteDialog.dateLabel}` : ''
|
|
|
|
|
}.`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
const deleteDialogBody = deleteDialog ? (
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className={cn(isWorktreeDelete ? 'space-y-3' : 'space-y-2')}>
|
2026-01-03 01:20:56 +02:00
|
|
|
{deleteDialog.sessions.length > 0 && (
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className={cn(
|
|
|
|
|
isWorktreeDelete ? 'rounded-lg bg-muted/30 p-3' : 'space-y-1.5 rounded-xl border border-border/40 bg-sidebar/60 p-3'
|
|
|
|
|
)}>
|
2026-01-07 22:06:28 +02:00
|
|
|
{isWorktreeDelete && (
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="typography-meta font-medium text-foreground">
|
|
|
|
|
{deleteDialog.sessions.length === 1 ? 'Linked session' : 'Linked sessions'}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="typography-micro text-muted-foreground/70">
|
|
|
|
|
{deleteDialog.sessions.length}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-01-07 22:06:28 +02:00
|
|
|
)}
|
2026-01-17 22:30:30 +02:00
|
|
|
<ul className={cn(isWorktreeDelete ? 'mt-2 space-y-1' : 'space-y-0.5')}>
|
2026-01-07 22:06:28 +02:00
|
|
|
{deleteDialog.sessions.slice(0, 5).map((session) => (
|
2026-01-17 22:30:30 +02:00
|
|
|
<li
|
|
|
|
|
key={session.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
isWorktreeDelete
|
|
|
|
|
? 'flex items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-muted-foreground'
|
|
|
|
|
: 'typography-micro text-muted-foreground/80'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span className={cn(!isWorktreeDelete && 'hidden')}>
|
|
|
|
|
•
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate">
|
|
|
|
|
{session.title || 'Untitled Session'}
|
|
|
|
|
</span>
|
2026-01-03 01:20:56 +02:00
|
|
|
</li>
|
|
|
|
|
))}
|
2026-01-07 22:06:28 +02:00
|
|
|
{deleteDialog.sessions.length > 5 && (
|
2026-01-17 22:30:30 +02:00
|
|
|
<li className={cn(
|
|
|
|
|
isWorktreeDelete
|
|
|
|
|
? 'px-2.5 py-1 text-xs text-muted-foreground/70'
|
|
|
|
|
: 'typography-micro text-muted-foreground/70'
|
|
|
|
|
)}>
|
2026-01-07 22:06:28 +02:00
|
|
|
+{deleteDialog.sessions.length - 5} more
|
2026-01-03 01:20:56 +02:00
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
{isWorktreeDelete ? (
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className="space-y-2 rounded-lg bg-muted/30 p-3">
|
2025-12-07 19:32:53 +02:00
|
|
|
<div className="flex items-center gap-2">
|
2026-01-17 22:30:30 +02:00
|
|
|
<RiGitBranchLine className="h-4 w-4 text-muted-foreground" />
|
2025-12-07 19:32:53 +02:00
|
|
|
<span className="typography-meta font-medium text-foreground">Worktree</span>
|
|
|
|
|
{targetWorktree?.label ? (
|
|
|
|
|
<span className="typography-micro text-muted-foreground/70">{targetWorktree.label}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="typography-micro text-muted-foreground/80 break-all">
|
|
|
|
|
{targetWorktree ? formatPathForDisplay(targetWorktree.path, homeDirectory) : 'Worktree path unavailable.'}
|
|
|
|
|
</p>
|
|
|
|
|
{hasDirtyWorktrees && (
|
2026-02-01 18:29:34 +02:00
|
|
|
<p className="typography-micro text-status-warning">Uncommitted changes will be discarded.</p>
|
2025-12-07 19:32:53 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="rounded-xl border border-border/40 bg-sidebar/60 p-3">
|
|
|
|
|
<p className="typography-meta text-muted-foreground/80">
|
|
|
|
|
Worktree directories stay intact. Subsessions linked to the selected sessions will also be removed.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : null;
|
|
|
|
|
|
2026-01-17 22:30:30 +02:00
|
|
|
const deleteRemoteBranchAction = isWorktreeDelete ? (
|
|
|
|
|
canRemoveRemoteBranches ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (removeRemoteOptionDisabled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setDeleteDialogShouldRemoveRemote((prev) => !prev);
|
|
|
|
|
}}
|
|
|
|
|
disabled={removeRemoteOptionDisabled}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-2 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors',
|
|
|
|
|
removeRemoteOptionDisabled
|
|
|
|
|
? 'cursor-not-allowed opacity-60'
|
|
|
|
|
: 'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{deleteDialogShouldRemoveRemote ? (
|
|
|
|
|
<RiCheckboxLine className="size-4 text-primary" />
|
|
|
|
|
) : (
|
|
|
|
|
<RiCheckboxBlankLine className="size-4" />
|
|
|
|
|
)}
|
|
|
|
|
Delete remote branch
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-xs text-muted-foreground/70">Remote branch info unavailable</span>
|
|
|
|
|
)
|
|
|
|
|
) : null;
|
|
|
|
|
|
2026-02-13 19:18:22 +02:00
|
|
|
const deleteLocalBranchAction = isWorktreeDelete ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (deleteLocalOptionDisabled) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setDeleteDialogShouldDeleteLocalBranch((prev) => !prev);
|
|
|
|
|
}}
|
|
|
|
|
disabled={deleteLocalOptionDisabled}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-2 rounded-md px-2 py-1 text-sm text-muted-foreground transition-colors',
|
|
|
|
|
deleteLocalOptionDisabled
|
|
|
|
|
? 'cursor-not-allowed opacity-60'
|
|
|
|
|
: 'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{deleteDialogShouldDeleteLocalBranch ? (
|
|
|
|
|
<RiCheckboxLine className="size-4 text-primary" />
|
|
|
|
|
) : (
|
|
|
|
|
<RiCheckboxBlankLine className="size-4" />
|
|
|
|
|
)}
|
|
|
|
|
Delete local branch
|
|
|
|
|
</button>
|
|
|
|
|
) : null;
|
|
|
|
|
|
2026-01-17 22:30:30 +02:00
|
|
|
const deleteDialogActions = isWorktreeDelete ? (
|
|
|
|
|
<div className="flex w-full items-center justify-between gap-3">
|
2026-01-27 19:59:48 +02:00
|
|
|
<div className="flex flex-col items-start gap-1">
|
2026-02-13 19:18:22 +02:00
|
|
|
{deleteLocalBranchAction}
|
2026-01-27 19:59:48 +02:00
|
|
|
{deleteRemoteBranchAction}
|
|
|
|
|
</div>
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button variant="ghost" onClick={closeDeleteDialog} disabled={isProcessingDelete}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="destructive" onClick={handleConfirmDelete} disabled={isProcessingDelete}>
|
|
|
|
|
{isProcessingDelete ? 'Deleting…' : 'Delete worktree'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-02-24 03:28:30 +02:00
|
|
|
<div className="flex w-full items-center justify-between gap-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowDeletionDialog(!showDeletionDialog)}
|
|
|
|
|
className="inline-flex items-center gap-1.5 typography-meta text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50"
|
|
|
|
|
aria-pressed={!showDeletionDialog}
|
|
|
|
|
>
|
|
|
|
|
{!showDeletionDialog ? <RiCheckboxLine className="size-4 text-primary" /> : <RiCheckboxBlankLine className="size-4" />}
|
|
|
|
|
Never ask
|
|
|
|
|
</button>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button variant="ghost" onClick={closeDeleteDialog} disabled={isProcessingDelete}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="destructive" onClick={handleConfirmDelete} disabled={isProcessingDelete}>
|
|
|
|
|
{isProcessingDelete
|
|
|
|
|
? 'Deleting…'
|
|
|
|
|
: deleteDialog?.sessions.length === 1
|
|
|
|
|
? 'Delete session'
|
|
|
|
|
: 'Delete sessions'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-07 19:32:53 +02:00
|
|
|
);
|
|
|
|
|
|
2026-01-07 22:06:28 +02:00
|
|
|
const deleteDialogTitle = isWorktreeDelete
|
|
|
|
|
? 'Delete worktree'
|
|
|
|
|
: deleteDialog?.sessions.length === 1
|
|
|
|
|
? 'Delete session'
|
|
|
|
|
: 'Delete sessions';
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{useMobileOverlay ? (
|
|
|
|
|
<MobileOverlayPanel
|
|
|
|
|
open={Boolean(deleteDialog)}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
if (isProcessingDelete) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
closeDeleteDialog();
|
|
|
|
|
}}
|
2026-01-07 22:06:28 +02:00
|
|
|
title={deleteDialogTitle}
|
2025-12-07 19:32:53 +02:00
|
|
|
footer={<div className="flex justify-end gap-2">{deleteDialogActions}</div>}
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-2 pb-2">
|
|
|
|
|
{deleteDialogDescription && (
|
|
|
|
|
<p className="typography-meta text-muted-foreground/80">{deleteDialogDescription}</p>
|
|
|
|
|
)}
|
|
|
|
|
{deleteDialogBody}
|
|
|
|
|
</div>
|
|
|
|
|
</MobileOverlayPanel>
|
|
|
|
|
) : (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={Boolean(deleteDialog)}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
if (isProcessingDelete) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
closeDeleteDialog();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-17 22:30:30 +02:00
|
|
|
<DialogContent
|
|
|
|
|
className={cn(
|
|
|
|
|
isWorktreeDelete
|
|
|
|
|
? 'max-w-xl max-h-[70vh] flex flex-col overflow-hidden gap-3'
|
|
|
|
|
: 'max-w-[min(520px,100vw-2rem)] space-y-2 pb-2'
|
|
|
|
|
)}
|
|
|
|
|
>
|
2025-12-07 19:32:53 +02:00
|
|
|
<DialogHeader>
|
2026-01-17 22:30:30 +02:00
|
|
|
<DialogTitle className={cn(isWorktreeDelete && 'flex items-center gap-2')}>
|
|
|
|
|
{isWorktreeDelete && <RiDeleteBinLine className="h-5 w-5" />}
|
|
|
|
|
{deleteDialogTitle}
|
|
|
|
|
</DialogTitle>
|
2025-12-07 19:32:53 +02:00
|
|
|
{deleteDialogDescription && <DialogDescription>{deleteDialogDescription}</DialogDescription>}
|
|
|
|
|
</DialogHeader>
|
2026-01-17 22:30:30 +02:00
|
|
|
<div className={cn(isWorktreeDelete && 'flex-1 min-h-0 overflow-y-auto space-y-2')}>
|
|
|
|
|
{deleteDialogBody}
|
|
|
|
|
</div>
|
2025-12-07 19:32:53 +02:00
|
|
|
<DialogFooter className="mt-2 gap-2 pt-1 pb-1">{deleteDialogActions}</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<DirectoryExplorerDialog
|
|
|
|
|
open={isDirectoryDialogOpen}
|
|
|
|
|
onOpenChange={setIsDirectoryDialogOpen}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|