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 (
);
}