"use client"; import { IconGitCommit, IconGitPullRequest, IconCloudUpload, IconChevronDown, IconLoader2, } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@kandev/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import type { FileInfo } from "@/lib/state/store"; import { LineStat } from "@/components/diff-stat"; import { FileStatusIcon } from "./file-status-icon"; import { FileRow } from "./changes-panel-file-row"; import { CommitRow, type CommitItem } from "./commit-row"; // --- Timeline visual components --- type ChangedFile = { path: string; status: FileInfo["status"]; staged: boolean; plus: number | undefined; minus: number | undefined; oldPath: string | undefined; }; // --- Timeline section dot colors --- const DOT_COLORS = { unstaged: "bg-yellow-500", staged: "bg-emerald-500", commits: "bg-blue-500", pr: "bg-purple-500", action: "bg-muted-foreground/25", } as const; function TimelineDot({ color }: { color: string }) { return
; } function TimelineSection({ dotColor, label, count, action, isLast, children, "data-testid": testId, }: { dotColor: string; label?: string; count?: number; action?: React.ReactNode; isLast?: boolean; children?: React.ReactNode; "data-testid"?: string; }) { return (
{/* Vertical line + dot */}
{!isLast &&
}
{/* Content */}
{/* Header */} {label && (
{label} {typeof count === "number" && ( ({count}) )} {action}
)} {/* Children (file list, buttons, etc.) */} {children}
); } // --- Commits section --- type CommitsSectionProps = { commits: CommitItem[]; isLast: boolean; onOpenCommitDetail?: (sha: string) => void; onRevertCommit?: (sha: string) => void; onAmendCommit?: (currentMessage: string) => void; onResetToCommit?: (sha: string) => void; }; export function CommitsSection({ commits, isLast, onOpenCommitDetail, onRevertCommit, onAmendCommit, onResetToCommit, }: CommitsSectionProps) { return (
    {commits.map((commit, index) => ( ))}
); } // --- Action buttons section (Create PR / Push) --- type ActionButtonsSectionProps = { onOpenPRDialog: () => void; onPush: () => void; onForcePush: () => void; isLoading: boolean; loadingOperation: string | null; aheadCount: number; canPush: boolean; canCreatePR: boolean; existingPrUrl?: string; }; export function ActionButtonsSection({ onOpenPRDialog, onPush, onForcePush, isLoading, loadingOperation, aheadCount, canPush, canCreatePR, existingPrUrl, }: ActionButtonsSectionProps) { const prExists = !!existingPrUrl; const createPrDisabled = !canCreatePR || prExists || isLoading; const pushDisabled = !canPush || isLoading; const isPushing = loadingOperation === "push"; const isCreatingPR = loadingOperation === "create_pr"; let pushTooltip: string | null = null; if (isLoading) pushTooltip = "A git operation is in progress"; else if (!canPush) pushTooltip = "No commits ahead of remote"; return (
{prExists && A pull request already exists for this task} Force Push (with lease) {pushTooltip && {pushTooltip}}
); } // --- File list sections (Unstaged / Staged) --- type FileListSectionProps = { variant: "unstaged" | "staged"; files: ChangedFile[]; pendingStageFiles: Set; isLast: boolean; actionLabel: string; isActionLoading?: boolean; onAction: () => void; secondaryActionLabel?: string; isSecondaryActionLoading?: boolean; onSecondaryAction?: () => void; onOpenDiff: (path: string) => void; onEditFile: (path: string) => void; onStage: (path: string) => void; onUnstage: (path: string) => void; onDiscard: (path: string) => void; }; export function FileListSection({ variant, files, pendingStageFiles, isLast, actionLabel, isActionLoading, onAction, secondaryActionLabel, isSecondaryActionLoading, onSecondaryAction, onOpenDiff, onEditFile, onStage, onUnstage, onDiscard, }: FileListSectionProps) { const dotColor = variant === "unstaged" ? DOT_COLORS.unstaged : DOT_COLORS.staged; const label = variant === "unstaged" ? "Unstaged" : "Staged"; return ( {files.length > 0 && (
    {files.map((file) => ( ))}
)} {files.length > 0 && (
{onSecondaryAction && secondaryActionLabel && ( )}
)}
); } // --- PR files section (read-only, from GitHub PR diff) --- export type PRChangedFile = { path: string; status: FileInfo["status"]; plus: number | undefined; minus: number | undefined; oldPath: string | undefined; }; type PRFilesSectionProps = { files: PRChangedFile[]; isLast: boolean; onOpenDiff: (path: string) => void; }; export function PRFilesSection({ files, isLast, onOpenDiff }: PRFilesSectionProps) { return ( {files.length > 0 && (
    {files.map((file) => ( ))}
)}
); } function PRFileRow({ file, onOpenDiff, }: { file: PRChangedFile; onOpenDiff: (path: string) => void; }) { const lastSlash = file.path.lastIndexOf("/"); const folder = lastSlash === -1 ? "" : file.path.slice(0, lastSlash); const name = lastSlash === -1 ? file.path : file.path.slice(lastSlash + 1); return (
  • onOpenDiff(file.path)} >
  • ); } // --- PR commits section --- type PRCommitItem = { sha: string; message: string; author_login: string; author_date: string; }; type PRCommitsSectionProps = { commits: PRCommitItem[]; isLast: boolean; onOpenCommitDetail?: (sha: string) => void; }; export function PRCommitsSection({ commits, isLast, onOpenCommitDetail }: PRCommitsSectionProps) { return (
      {commits.map((commit, index) => (
    • onOpenCommitDetail?.(commit.sha)} > {commit.sha.slice(0, 7)} {commit.message} {commit.author_login && ( {commit.author_login} )}
    • ))}
    ); } // --- Review progress bar --- type ReviewProgressBarProps = { reviewedCount: number; totalFileCount: number; onOpenReview?: () => void; }; export function ReviewProgressBar({ reviewedCount, totalFileCount, onOpenReview, }: ReviewProgressBarProps) { const progressPercent = totalFileCount > 0 ? (reviewedCount / totalFileCount) * 100 : 0; if (totalFileCount <= 0) return null; return (
    {reviewedCount}/{totalFileCount} reviewed
    {reviewedCount} of {totalFileCount} files reviewed ); }