"use client"; import { useState } from "react"; import { IconCloudDownload, IconEye, IconChevronDown, IconGitBranch, IconGitMerge, IconArrowRight, IconLoader2, IconEdit, IconRoute, } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@kandev/ui/dialog"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@kandev/ui/hover-card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger, } from "@kandev/ui/drawer"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@kandev/ui/dropdown-menu"; import { PanelHeaderBarSplit } from "./panel-primitives"; import { BaseBranchPicker } from "./base-branch-picker"; import type { GitCredentialDisplay } from "./changes-git-credential-display"; import { useTouchDrawer } from "@/hooks/use-compact-task-chrome"; import { useTranslation } from "react-i18next"; import { PerRepoPullMenu } from "./changes-panel-per-repo-menu"; import type { RemoteContributionRelation } from "@/hooks/domains/session/remote-contribution-relation"; import type { ContributionHistoryExplanationTarget } from "@/hooks/domains/session/use-contribution-history-explanation"; import { type RemoteContributionResolutionTarget, type useRemoteContributionResolution, } from "./use-remote-contribution-resolution"; import { RemoteContributionHeaderActions } from "./remote-contribution-header-actions"; import { ComparisonTargetDisplay } from "./changes-panel-comparison-target"; import { buildBranchRows, type BranchRow, type PerRepoStatus } from "./changes-panel-branch-rows"; export type { PerRepoStatus } from "./changes-panel-branch-rows"; type RenameBranchResult = { success: boolean; error?: string; }; function RenameBranchButton({ branch, repositoryName, onRenameBranch, isRenaming, }: { branch: string; repositoryName: string; onRenameBranch?: (newName: string, repo: string) => Promise; isRenaming: boolean; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [newBranchName, setNewBranchName] = useState(branch); const [error, setError] = useState(null); const trimmedBranchName = newBranchName.trim(); const canRename = !!onRenameBranch && trimmedBranchName !== "" && trimmedBranchName !== branch; const submitRename = async () => { if (!onRenameBranch || !canRename) return; setError(null); try { const result = await onRenameBranch(trimmedBranchName, repositoryName); if (!result.success) { setError(result.error || t("task:failedToRenameBranch")); return; } setOpen(false); } catch (err) { setError(err instanceof Error ? err.message : t("task:failedToRenameBranch")); } }; const openDialog = () => { setNewBranchName(branch); setError(null); setOpen(true); }; return ( <> {t("task:editBranch2")}
setNewBranchName(event.target.value)} autoFocus /> {error &&

{error}

}
); } function BranchRowView({ repoLabel, branch, baseBranch, repositoryName, taskId, onRenameBranch, isRenaming, }: BranchRow & { taskId: string | null; onRenameBranch?: (newName: string, repo: string) => Promise; isRenaming: boolean; }) { return (
{repoLabel && ( {repoLabel} )} {branch}
); } function BranchHoverCard({ displayBranch, baseBranchDisplay, rows, taskId, onRenameBranch, isRenaming, credentialDisplay, comparisonTargets, }: { displayBranch: string; baseBranchDisplay: string; rows?: BranchRow[]; taskId: string | null; onRenameBranch?: (newName: string, repo: string) => Promise; isRenaming: boolean; credentialDisplay: GitCredentialDisplay | null; comparisonTargets: string[]; }) { const { t } = useTranslation(); const usesTouchDrawer = useTouchDrawer(); const [open, setOpen] = useState(false); const trigger = ( ); const content = (
{rows && rows.length > 0 ? t("task:yourBranchesLabel") : t("task:yourCodeLivesInLabel")} {t("task:comparingAgainstLabel")}
{rows && rows.length > 0 ? (
{rows!.map((row) => ( ))}
) : ( )} {credentialDisplay && (

{credentialDisplay.source}

{credentialDisplay.detail}

{credentialDisplay.transport}

)}
); if (usesTouchDrawer) { return ( {trigger} {t("task:branchDetails")} {t("task:branchComparisonAndTaskGitCredentials")}
{content}
); } return ( {trigger} {content} ); } function PullTriggerContent({ behindCount, isPulling, isRebasing, }: { behindCount: number; isPulling: boolean; isRebasing: boolean; }) { let label: string; if (isPulling) label = "Pulling…"; else if (isRebasing) label = "Rebasing…"; else label = "Pull"; return ( <> {isPulling || isRebasing ? ( ) : ( )} {label} {behindCount > 0 && !(isPulling || isRebasing) && ( {behindCount} )} {!(isPulling || isRebasing) && ( )} ); } export function PullDropdown({ behindCount, pullDisabled, isLoading, loadingOperation, repoNames, perRepoStatus, onRepoPull, onRepoRebase, onRepoMerge, repoDisplayName, pullDisabledReason, }: { behindCount: number; pullDisabled?: boolean; pullDisabledReason?: string; isLoading: boolean; loadingOperation: string | null; /** Always non-empty (single-repo includes the empty-name entry). */ repoNames: string[]; perRepoStatus: PerRepoStatus[]; onRepoPull: (repo: string) => void; onRepoRebase: (repo: string) => void; onRepoMerge: (repo: string) => void; /** Maps a repository_name to its display label. */ repoDisplayName?: (repositoryName: string) => string | undefined; }) { const { t } = useTranslation(); const triggerBehind = perRepoStatus.length > 0 ? Math.max(behindCount, ...perRepoStatus.map((s) => s.pullBehind ?? 0)) : behindCount; const pullButton = ( ); if (pullDisabled) { return ( {pullButton} {pullDisabledReason ?? t("task:divergedActionsUnavailable")} ); } return ( {pullButton} ); } function ChangesPanelHeaderLeft({ showDiffReview, onOpenDiffAll, onOpenReview, onRequestWalkthrough, requestWalkthroughDisabled, }: { showDiffReview: boolean; onOpenDiffAll?: () => void; onOpenReview?: () => void; onRequestWalkthrough?: () => void; requestWalkthroughDisabled?: boolean; }) { const { t } = useTranslation(); if (!showDiffReview) return null; return ( <> {onRequestWalkthrough ? ( ) : null} ); } function ChangesPanelWalkthroughButton({ onRequestWalkthrough, requestWalkthroughDisabled, }: { onRequestWalkthrough: () => void; requestWalkthroughDisabled?: boolean; }) { const { t } = useTranslation(); const tooltip = requestWalkthroughDisabled ? t("task:loadingChangedFiles") : t("task:walkMeThroughTheseChanges"); return ( {tooltip} ); } type ChangesPanelHeaderProps = { hasChanges: boolean; hasCommits: boolean; hasPRFiles?: boolean; displayBranch: string | null; baseBranchDisplay: string; /** Per-repo merge target, keyed by repository_name. Undefined entries fall * back to baseBranchDisplay. Empty/missing for single-repo workspaces. */ baseBranchByRepo?: Record; behindCount: number; pullDisabled?: boolean; pullDisabledReason?: string; isLoading: boolean; loadingOperation: string | null; onOpenDiffAll?: () => void; onOpenReview?: () => void; onRequestWalkthrough?: () => void; requestWalkthroughDisabled?: boolean; /** Always non-empty (single-repo includes the empty-name entry). */ repoNames: string[]; perRepoStatus: PerRepoStatus[]; onRepoPull: (repo: string) => void; onRepoRebase: (repo: string) => void; onRepoMerge: (repo: string) => void; onRenameBranch?: (newName: string, repo: string) => Promise; credentialDisplay: GitCredentialDisplay | null; comparisonTargets: string[]; repoDisplayName?: (repositoryName: string) => string | undefined; /** Active task id; piped into the base-branch picker so it can resolve * the right task_repositories row to PATCH. Null while task data is * hydrating — the picker falls back to a static label. */ taskId: string | null; relation?: RemoteContributionRelation; contributionHistoryTarget?: ContributionHistoryExplanationTarget | null; resolution?: ReturnType; resolutionTarget?: RemoteContributionResolutionTarget | null; remoteContributionUrl?: string; remoteContributionNumber?: number; }; export function ChangesPanelHeader(props: ChangesPanelHeaderProps) { const { hasChanges, hasCommits, hasPRFiles, displayBranch, baseBranchDisplay, baseBranchByRepo, behindCount, pullDisabled, pullDisabledReason, isLoading, loadingOperation, onOpenDiffAll, onOpenReview, onRequestWalkthrough, requestWalkthroughDisabled, repoNames, perRepoStatus, onRepoPull, onRepoRebase, onRepoMerge, repoDisplayName, taskId, onRenameBranch, credentialDisplay, comparisonTargets, relation, contributionHistoryTarget, resolution, resolutionTarget, remoteContributionUrl, remoteContributionNumber, } = props; const branchRows = buildBranchRows( perRepoStatus, baseBranchByRepo, baseBranchDisplay, repoDisplayName, ); return ( } right={ <> {(displayBranch || branchRows.length > 0) && ( )} } /> ); }