import { useEffect, useState, useCallback } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Archive, ChevronDown, ChevronRight, Clock, FileText, GitBranch, Play, Trash2, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { getHttpApiClient } from '@/lib/http-api-client'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; interface WorktreeInfo { path: string; branch: string; isMain: boolean; hasChanges?: boolean; changedFilesCount?: number; } interface StashEntry { index: number; message: string; branch: string; date: string; files: string[]; } interface ViewStashesDialogProps { open: boolean; onOpenChange: (open: boolean) => void; worktree: WorktreeInfo | null; onStashApplied?: () => void; } function formatRelativeDate(dateStr: string): string { const date = new Date(dateStr); if (isNaN(date.getTime())) return 'Unknown date'; const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); const diffWeeks = Math.floor(diffDays / 7); const diffMonths = Math.floor(diffDays / 30); if (diffSecs < 60) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; if (diffWeeks < 5) return `${diffWeeks}w ago`; if (diffMonths < 12) return `${diffMonths}mo ago`; return date.toLocaleDateString(); } function StashEntryItem({ stash, onApply, onPop, onDrop, isApplying, isDropping, }: { stash: StashEntry; onApply: (index: number) => void; onPop: (index: number) => void; onDrop: (index: number) => void; isApplying: boolean; isDropping: boolean; }) { const [expanded, setExpanded] = useState(false); const isBusy = isApplying || isDropping; // Clean up the stash message for display const displayMessage = stash.message.replace(/^(WIP on|On) [^:]+:\s*[a-f0-9]+\s*/, '').trim() || stash.message; return (
{/* Header */}
{/* Expand toggle & stash icon */} {/* Content */}

{displayMessage}

stash@{'{' + stash.index + '}'} {stash.branch && ( {stash.branch} )} {stash.files.length > 0 && ( {stash.files.length} file{stash.files.length !== 1 ? 's' : ''} )}
{/* Action buttons */}
{/* Expanded file list */} {expanded && stash.files.length > 0 && (
{stash.files.map((file) => (
{file}
))}
)}
); } export function ViewStashesDialog({ open, onOpenChange, worktree, onStashApplied, }: ViewStashesDialogProps) { const [stashes, setStashes] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [applyingIndex, setApplyingIndex] = useState(null); const [droppingIndex, setDroppingIndex] = useState(null); const fetchStashes = useCallback(async () => { if (!worktree) return; setIsLoading(true); setError(null); try { const api = getHttpApiClient(); const result = await api.worktree.stashList(worktree.path); if (result.success && result.result) { setStashes(result.result.stashes); } else { setError(result.error || 'Failed to load stashes'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load stashes'); } finally { setIsLoading(false); } }, [worktree]); useEffect(() => { if (open && worktree) { fetchStashes(); } if (!open) { setStashes([]); setError(null); } }, [open, worktree, fetchStashes]); const handleApply = async (stashIndex: number) => { if (!worktree) return; setApplyingIndex(stashIndex); try { const api = getHttpApiClient(); const result = await api.worktree.stashApply(worktree.path, stashIndex, false); if (result.success && result.result) { if (result.result.hasConflicts) { toast.warning('Stash applied with conflicts', { description: 'Please resolve the merge conflicts.', }); } else { toast.success('Stash applied'); } onStashApplied?.(); } else { toast.error('Failed to apply stash', { description: result.error || 'Unknown error', }); } } catch (err) { toast.error('Failed to apply stash', { description: err instanceof Error ? err.message : 'Unknown error', }); } finally { setApplyingIndex(null); } }; const handlePop = async (stashIndex: number) => { if (!worktree) return; setApplyingIndex(stashIndex); try { const api = getHttpApiClient(); const result = await api.worktree.stashApply(worktree.path, stashIndex, true); if (result.success && result.result) { if (result.result.hasConflicts) { toast.warning('Stash popped with conflicts', { description: 'Please resolve the merge conflicts. The stash was removed.', }); } else { toast.success('Stash popped', { description: 'Changes applied and stash removed.', }); } // Refresh the stash list since the stash was removed await fetchStashes(); onStashApplied?.(); } else { toast.error('Failed to pop stash', { description: result.error || 'Unknown error', }); } } catch (err) { toast.error('Failed to pop stash', { description: err instanceof Error ? err.message : 'Unknown error', }); } finally { setApplyingIndex(null); } }; const handleDrop = async (stashIndex: number) => { if (!worktree) return; setDroppingIndex(stashIndex); try { const api = getHttpApiClient(); const result = await api.worktree.stashDrop(worktree.path, stashIndex); if (result.success) { toast.success('Stash deleted'); // Refresh the stash list await fetchStashes(); } else { toast.error('Failed to delete stash', { description: result.error || 'Unknown error', }); } } catch (err) { toast.error('Failed to delete stash', { description: err instanceof Error ? err.message : 'Unknown error', }); } finally { setDroppingIndex(null); } }; if (!worktree) return null; return ( Stashes Stashed changes in{' '} {worktree.branch}
{isLoading && (
Loading stashes...
)} {error && (

{error}

)} {!isLoading && !error && stashes.length === 0 && (

No stashes found

Use "Stash Changes" to save your uncommitted changes

)} {!isLoading && !error && stashes.length > 0 && (
{stashes.map((stash) => ( ))}
)}
); }