"use client"; import { useState } from "react"; import { IconChevronDown, IconChevronRight, IconCloudUpload, IconGitPullRequest, } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip"; import { CommitRow, type CommitItem } from "./commit-row"; import { groupByRepositoryName } from "@/lib/group-by-repo"; import type { ChangedFile } from "./changes-panel-helpers"; export type RepoGroup = ReturnType>[number]; /** * Per-repo group inside a file-list section (Unstaged / Staged). Renders the * collapsible repo header with optional inline action buttons (Stage all / * Commit / Unstage all). Action labels and handlers are owned by the parent * section so the same shape works for both variants. */ export function RepoGroupItem({ group, collapsed, onToggle, renderRow, primaryLabel, secondaryLabel, onRepoAction, onRepoSecondaryAction, displayName, }: { group: RepoGroup; collapsed: boolean; onToggle: () => void; renderRow: (file: ChangedFile) => React.ReactNode; primaryLabel: string; secondaryLabel?: string; onRepoAction?: (repo: string) => void; onRepoSecondaryAction?: (repo: string) => void; /** Optional display label override; defaults to group.repositoryName. */ displayName?: string; }) { const stop = (e: React.MouseEvent) => e.stopPropagation(); const label = displayName || group.repositoryName || "Repository"; return (
  • {(onRepoAction || onRepoSecondaryAction) && (
    {onRepoAction && ( )} {onRepoSecondaryAction && secondaryLabel && ( )}
    )}
    {!collapsed && }
  • ); } // Inline action buttons rendered in the file-list section header for single-repo // workspaces. Mirrors the per-repo buttons that RepoGroupItem renders for // multi-repo. Handlers receive an empty repo string which routes to the // workspace root in single-repo mode. export function FileSectionActions({ primaryLabel, secondaryLabel, onAction, onSecondaryAction, }: { primaryLabel: string; secondaryLabel?: string; onAction?: (repo: string) => void; onSecondaryAction?: (repo: string) => void; }) { if (!onAction && !onSecondaryAction) return null; return (
    e.stopPropagation()}> {onAction && ( )} {onSecondaryAction && secondaryLabel && ( )}
    ); } export function CommitsGroupActions({ repositoryName, unpushedCount, aheadCount, prExists, canCreatePR, onRepoPush, onRepoCreatePR, stop, }: { repositoryName: string; unpushedCount: number; aheadCount: number; prExists: boolean; canCreatePR: boolean; onRepoPush?: (repo: string) => void; onRepoCreatePR?: (repo: string) => void; stop: (e: React.MouseEvent) => void; }) { return (
    {onRepoPush && (unpushedCount > 0 || aheadCount > 0) && ( )} {onRepoCreatePR && ( {prExists && A pull request already exists for this task} )}
    ); } /** * Per-repo group inside the Commits section. Like {@link RepoGroupItem} but * for commit rows; surfaces a Push button when the repo has unpushed commits. */ export function CommitsRepoGroup({ repositoryName, displayName, groupCommits, aheadCount = 0, existingPrUrl, showHeader = true, onOpenCommitDetail, onAmendCommit, onRevertCommit, onResetToCommit, onRepoPush, onRepoCreatePR, }: { repositoryName: string; displayName?: string; groupCommits: CommitItem[]; aheadCount?: number; existingPrUrl?: string; /** When false, render commits flat without the per-repo header. Single-repo * workspaces use this — the action buttons (Push / PR) move up to the * section header so we don't render a redundant repo sub-header. */ showHeader?: boolean; onOpenCommitDetail?: (sha: string, repo?: string) => void; onAmendCommit?: (currentMessage: string, repo?: string) => void; onRevertCommit?: (sha: string, repo?: string) => void; onResetToCommit?: (sha: string, repo?: string) => void; onRepoPush?: (repo: string) => void; onRepoCreatePR?: (repo: string) => void; /** Base branch passed through; reserved for richer per-repo PR UX. */ baseBranch?: string; }) { const [collapsed, setCollapsed] = useState(false); // Each repo has its own "latest unpushed commit" — revert/amend in this // group must target THIS repo's newest, not the merged-list newest. const firstUnpushedInGroup = groupCommits.findIndex((c) => c.pushed !== true); const unpushedCount = groupCommits.filter((c) => !c.pushed).length; const stop = (e: React.MouseEvent) => e.stopPropagation(); const label = displayName || repositoryName || "Repository"; // Bug 10 acknowledged trade-off: `existingPrUrl` is sourced from a // workspace-scoped `prByRepo` map keyed only by "" today — the kandev task // model has one PR per task, not one PR per repo. As a result every per- // repo group inherits the same PR URL. When the backend grows per-repo PR // tracking, callers should key `prByRepo` by `repositoryName` and the // `?? prByRepo[""]` fallback in CommitsSection should be removed so each // group surfaces only its own PR. Until then, the visual is "PR exists for // this task" rather than "PR exists for this repo" — accept the imprecision. const prExists = !!existingPrUrl; // The PR scope today is workspace-wide (one PR per task). Disable per-repo // Create PR once a PR exists; the user can update it via push instead. const canCreatePR = !!onRepoCreatePR && !prExists; const rows = groupCommits.map((commit, index) => ( )); if (!showHeader) { return <>{rows}; } return (
  • {!collapsed &&
      {rows}
    }
  • ); }