"use client";
import {
IconArrowBackUp,
IconPlus,
IconMinus,
IconCheck,
IconLoader2,
IconPencil,
} from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
import { LineStat } from "@/components/diff-stat";
import { FileStatusIcon } from "@/components/shared/file-status-icon";
import { FileIcon } from "@/components/ui/file-icon";
import { getFileCategory } from "@/lib/utils/file-types";
import type { ChangedFile } from "./changes-panel-helpers";
import type { OpenDiffOptions } from "./changes-diff-target";
import { useTranslation } from "react-i18next";
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
const COARSE_POINTER_TARGET_CLASS = "min-h-11 min-w-11";
const splitPath = (path: string) => {
const lastSlash = path.lastIndexOf("/");
if (lastSlash === -1) return { folder: "", file: path };
return {
folder: path.slice(0, lastSlash),
file: path.slice(lastSlash + 1),
};
};
type FileRowProps = {
file: ChangedFile;
isPending: boolean;
isSelected?: boolean;
/** True when this file's diff/editor tab is the currently active dockview panel. */
isActive?: boolean;
onSelect?: (path: string, e: React.MouseEvent) => boolean;
onOpenDiff: (path: string, options?: OpenDiffOptions) => void;
// Multi-repo: handlers receive the file's repository_name so the per-file
// staging op routes to the right git repo. Same-named files (e.g. README.md
// in two repos) cannot be disambiguated by path alone.
onStage: (path: string, repo?: string) => void;
onUnstage: (path: string, repo?: string) => void;
onDiscard: (path: string, repo?: string, anchor?: HTMLElement) => void;
onEditFile: (path: string, repo?: string) => void;
/**
* Tree mode: skip the folder prefix, swap the left-side stage button for a
* filetype icon, and surface the stage action only on row hover (VS Code-
* style). The Unstaged / Staged section split keeps the staged state
* visually obvious even without the always-on left chip.
*/
treeMode?: boolean;
/** Tree mode: left padding in pixels driven by depth. */
indentPx?: number;
};
export function FileRow({
file,
isPending,
isSelected,
isActive,
onSelect,
onOpenDiff,
onStage,
onUnstage,
onDiscard,
onEditFile,
treeMode,
indentPx,
}: FileRowProps) {
const { isFinePointer } = useResponsiveBreakpoint();
const { folder, file: name } = splitPath(file.path);
const showFolder = !treeMode && folder;
const handleClick = (e: React.MouseEvent) => {
if (e.button === 2) return;
const consumed = onSelect?.(file.path, e);
if (!consumed) {
if (getFileCategory(file.path) === "image") {
onEditFile(file.path, file.repositoryName);
return;
}
onOpenDiff(file.path, {
source: "uncommitted",
repositoryName: file.repositoryName,
...(file.changeLayer ? { changeLayer: file.changeLayer } : {}),
});
}
};
return (
{treeMode ? (
) : (
)}
);
}
function FileRowStats({ file, isFinePointer }: { file: ChangedFile; isFinePointer: boolean }) {
return (
);
}
function TreeModeFileActionSlot({
name,
isPending,
isFinePointer,
staged,
path,
repo,
onStage,
onUnstage,
}: {
name: string;
isPending: boolean;
isFinePointer: boolean;
staged: boolean;
path: string;
repo?: string;
onStage: (path: string, repo?: string) => void;
onUnstage: (path: string, repo?: string) => void;
}) {
return (
*]:col-start-1 [&>*]:row-start-1",
isFinePointer ? "size-4" : "size-11",
)}
>
{isFinePointer && (
)}
);
}
function StageButton({
isPending,
staged,
touchSized,
path,
repo,
onStage,
onUnstage,
}: {
isPending: boolean;
staged: boolean;
touchSized?: boolean;
path: string;
repo?: string;
onStage: (path: string, repo?: string) => void;
onUnstage: (path: string, repo?: string) => void;
}) {
const { t } = useTranslation();
if (isPending) {
return (
);
}
if (staged) {
return (
);
}
return (
);
}
function FileRowActions({
path,
repo,
isFinePointer,
onDiscard,
onEditFile,
}: {
path: string;
repo?: string;
isFinePointer: boolean;
onDiscard: (path: string, repo?: string, anchor?: HTMLElement) => void;
onEditFile: (path: string, repo?: string) => void;
}) {
const { t } = useTranslation();
return (
{t("task:discardChanges2")}
{t("common:edit")}
);
}
// --- Bulk action components (used by FileListSection) ---
export function DefaultActionButtons({
actionLabel,
isActionLoading,
onAction,
secondaryActionLabel,
isSecondaryActionLoading,
onSecondaryAction,
}: {
actionLabel: string;
isActionLoading?: boolean;
onAction: () => void;
secondaryActionLabel?: string;
isSecondaryActionLoading?: boolean;
onSecondaryAction?: () => void;
}) {
return (
<>
{onSecondaryAction && secondaryActionLabel && (
)}
>
);
}
export function BulkActionBar({
variant,
selectionCount,
selectedPaths,
onBulkStage,
onBulkUnstage,
onBulkDiscard,
}: {
variant: "unstaged" | "staged";
selectionCount: number;
selectedPaths: Set;
onBulkStage?: (paths: string[]) => void;
onBulkUnstage?: (paths: string[]) => void;
onBulkDiscard?: (paths: string[], anchor?: HTMLElement) => void;
}) {
const { t } = useTranslation();
const paths = [...selectedPaths];
return (
{selectionCount} selected
{variant === "unstaged" && onBulkStage && (
)}
{variant === "staged" && onBulkUnstage && (
)}
{onBulkDiscard && (
)}
);
}