import { useCallback, useEffect, useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { GitCommit, User, Clock, Copy, Check, ChevronDown, ChevronRight, FileText, } 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 CommitInfo { hash: string; shortHash: string; author: string; authorEmail: string; date: string; subject: string; body: string; files: string[]; } interface ViewCommitsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; worktree: WorktreeInfo | null; } function formatRelativeDate(dateStr: string): string { if (!dateStr) return 'unknown date'; 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 CopyHashButton({ hash }: { hash: string }) { const [copied, setCopied] = useState(false); const handleCopy = async (e: React.MouseEvent) => { e.stopPropagation(); try { await navigator.clipboard.writeText(hash); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast.error('Failed to copy hash'); } }; return ( ); } function CommitEntryItem({ commit, index, isLast, }: { commit: CommitInfo; index: number; isLast: boolean; }) { const [expanded, setExpanded] = useState(false); const hasFiles = commit.files && commit.files.length > 0; return (
{/* Timeline dot and line */}
{!isLast &&
}
{/* Commit content */}

{commit.subject}

{commit.body && (

{commit.body}

)}
{commit.author} {hasFiles && ( )}
{/* Expanded file list */} {expanded && hasFiles && (
{commit.files.map((file) => (
{file}
))}
)}
); } const INITIAL_COMMIT_LIMIT = 30; const LOAD_MORE_INCREMENT = 30; const MAX_COMMIT_LIMIT = 100; export function ViewCommitsDialog({ open, onOpenChange, worktree }: ViewCommitsDialogProps) { const [commits, setCommits] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [error, setError] = useState(null); const [limit, setLimit] = useState(INITIAL_COMMIT_LIMIT); const [hasMore, setHasMore] = useState(false); const fetchCommits = useCallback( async (fetchLimit: number, isLoadMore = false) => { if (isLoadMore) { setIsLoadingMore(true); } else { setIsLoading(true); setError(null); setCommits([]); } try { const api = getHttpApiClient(); const result = await api.worktree.getCommitLog(worktree!.path, fetchLimit); if (result.success && result.result) { // Ensure each commit has a files array (backwards compat if server hasn't been rebuilt) const fetchedCommits = result.result.commits.map((c: CommitInfo) => ({ ...c, files: c.files || [], })); setCommits(fetchedCommits); // If we got back exactly as many commits as we requested, there may be more setHasMore(fetchedCommits.length === fetchLimit && fetchLimit < MAX_COMMIT_LIMIT); } else { setError(result.error || 'Failed to load commits'); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load commits'); } finally { setIsLoading(false); setIsLoadingMore(false); } }, [worktree] ); useEffect(() => { if (!open || !worktree) return; setLimit(INITIAL_COMMIT_LIMIT); setHasMore(false); fetchCommits(INITIAL_COMMIT_LIMIT); }, [open, worktree, fetchCommits]); const handleLoadMore = () => { const newLimit = Math.min(limit + LOAD_MORE_INCREMENT, MAX_COMMIT_LIMIT); setLimit(newLimit); fetchCommits(newLimit, true); }; if (!worktree) return null; return ( Commit History Recent commits on{' '} {worktree.branch}
{isLoading && (
Loading commits...
)} {error && (

{error}

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

No commits found

)} {!isLoading && !error && commits.length > 0 && (
{commits.map((commit, index) => ( ))} {hasMore && (
)}
)}
); }