"use client"; import { useState } from "react"; import { IconGitPullRequest, IconCloudUpload, IconChevronDown, IconChevronRight, 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, collapsible = true, "data-testid": testId, }: { dotColor: string; label?: string; count?: number; action?: React.ReactNode; isLast?: boolean; children?: React.ReactNode; collapsible?: boolean; "data-testid"?: string; }) { const [collapsed, setCollapsed] = useState(false); const canCollapse = collapsible && !!label; return (
{/* Vertical line + dot */}
{!isLast &&
}
{/* Content */}
{/* Header */} {label && (
{canCollapse ? ( ) : ( {label} {typeof count === "number" && ( ({count}) )} )} {action}
)} {/* Children (file list, buttons, etc.) */} {!collapsed && 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) { // Find the first unpushed commit for context menu actions (amend/revert only on latest unpushed) const firstUnpushedIndex = commits.findIndex((c) => c.pushed !== true); 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)} >
  • ); } // --- 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 ); }