import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2/client'; import { Button } from '@/components/ui/button'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; import { toast } from '@/components/ui'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { getWorktreeStatus } from '@/lib/worktrees/worktreeStatus'; import { getWorktreeDisplayName, removeProjectWorktree, type ProjectRef } from '@/lib/worktrees/worktreeManager'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useAllLiveSessions } from '@/sync/sync-context'; import type { WorktreeMetadata } from '@/types/worktree'; type MobileDeleteWorktreeDialogProps = { open: boolean; project: ProjectRef; worktree: WorktreeMetadata | null; onClose: () => void; onDeleted?: () => void; }; const normalizePath = (value?: string | null): string => (value || '').replace(/\\/g, '/').replace(/\/+$/, ''); const getSessionDirectory = (session: Session): string => { const record = session as Session & { directory?: string | null; project?: { worktree?: string | null } | null }; return normalizePath(record.directory ?? record.project?.worktree ?? null); }; /** * Mobile worktree-deletion confirmation. Built directly on the shared * primitives (getWorktreeStatus / removeProjectWorktree / archiveSessions) so * it mirrors the desktop SessionDialogs worktree flow without mounting it: * linked sessions are archived, the worktree is removed, and remote/local * branch deletion are optional. */ export const MobileDeleteWorktreeDialog: React.FC = ({ open, project, worktree, onClose, onDeleted, }) => { const { t } = useI18n(); const liveSessions = useAllLiveSessions(); const globalActiveSessions = useGlobalSessionsStore((state) => state.activeSessions); const archiveSessions = useSessionUIStore((state) => state.archiveSessions); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const [deleteLocalBranch, setDeleteLocalBranch] = React.useState(false); const [deleteRemoteBranch, setDeleteRemoteBranch] = React.useState(false); const [isDirty, setIsDirty] = React.useState(false); const [isProcessing, setIsProcessing] = React.useState(false); const worktreePath = normalizePath(worktree?.path); const hasBranch = typeof worktree?.branch === 'string' && worktree.branch.trim().length > 0; // Sessions attached to this worktree — archived (not deleted) on removal, // matching the desktop behavior. const linkedSessions = React.useMemo(() => { if (!worktreePath) return [] as Session[]; const merged = new Map(); for (const session of [...globalActiveSessions, ...liveSessions]) { if (getSessionDirectory(session) === worktreePath) merged.set(session.id, session); } return Array.from(merged.values()); }, [globalActiveSessions, liveSessions, worktreePath]); React.useEffect(() => { if (!open) { setDeleteLocalBranch(false); setDeleteRemoteBranch(false); setIsDirty(false); setIsProcessing(false); return; } if (!worktree?.path) return; let cancelled = false; void getWorktreeStatus(worktree.path) .then((status) => { if (!cancelled) setIsDirty(Boolean(status?.isDirty)); }) .catch(() => { if (!cancelled) setIsDirty(Boolean(worktree.status?.isDirty)); }); return () => { cancelled = true; }; }, [open, worktree?.path, worktree?.status?.isDirty]); const removeWorktreeInBackground = React.useCallback((target: WorktreeMetadata, sessionIds: string[]) => { const name = getWorktreeDisplayName(target); const toastId = toast.loading(t('sessions.sidebar.sessionDialogs.worktree.removingTitle', { name })); void (async () => { try { if (sessionIds.length > 0) { const { failedIds } = await archiveSessions(sessionIds); if (failedIds.length > 0) { toast.error( failedIds.length === 1 ? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length }) : t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length }), { id: toastId, description: t('sessions.sidebar.dialogs.deleteResult.tryAgain') }, ); return; } } await removeProjectWorktree(project, target, { deleteRemoteBranch: hasBranch && deleteRemoteBranch, deleteLocalBranch: hasBranch && deleteLocalBranch, }); // If the removed worktree was the active directory, fall back to the project root. if (normalizePath(currentDirectory) === worktreePath && normalizePath(project.path)) { useDirectoryStore.getState().setDirectory(normalizePath(project.path), { showOverlay: false }); } toast.success(t('sessions.sidebar.sessionDialogs.worktree.removedTitle', { name }), { id: toastId, description: hasBranch && deleteRemoteBranch ? t('sessions.sidebar.sessionDialogs.worktree.removedWithRemote') : t('sessions.sidebar.sessionDialogs.worktree.removed'), }); onDeleted?.(); } catch (error) { toast.error(t('sessions.sidebar.sessionDialogs.worktree.errorRemoveTitle', { name }), { id: toastId, description: error instanceof Error ? error.message : t('sessions.sidebar.dialogs.deleteResult.tryAgain'), }); } })(); }, [archiveSessions, currentDirectory, deleteLocalBranch, deleteRemoteBranch, hasBranch, onDeleted, project, t, worktreePath]); const handleConfirm = () => { if (!worktree || isProcessing) return; setIsProcessing(true); removeWorktreeInBackground(worktree, linkedSessions.map((session) => session.id)); onClose(); }; if (!worktree) return null; const worktreeName = worktree.branch || worktree.label || worktree.path; const toggle = (checked: boolean, onChange: (value: boolean) => void, label: string, disabled?: boolean) => ( ); return ( } >

{t('mobile.projectEdit.deleteWorktreeConfirm', { name: worktreeName })}

{isDirty ? (

{t('mobile.projectEdit.deleteWorktreeDirty')}

) : null} {linkedSessions.length > 0 ? (

{t('mobile.projectEdit.deleteWorktreeArchiveNote', { count: linkedSessions.length })}

) : null} {hasBranch ? (
{toggle(deleteLocalBranch, setDeleteLocalBranch, t('mobile.projectEdit.deleteLocalBranch'), isProcessing)} {toggle(deleteRemoteBranch, setDeleteRemoteBranch, t('mobile.projectEdit.deleteRemoteBranch'), isProcessing)}
) : null}
); };