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 */}
{/* Commit content */}
{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 (
);
}