Files
kandev/apps/web/components/task/chat/messages/file-path-button.tsx
T
2026-02-19 22:18:01 +00:00

87 lines
2.7 KiB
TypeScript

"use client";
import { memo, useCallback } from "react";
import { cn, toRelativePath } from "@/lib/utils";
type FilePathButtonProps = {
filePath: string;
worktreePath?: string;
onOpenFile?: (path: string) => void;
variant?: "badge" | "list-item" | "link";
className?: string;
};
/**
* Check if a file path looks like a valid, openable file (not a directory or invalid path).
* Returns false for:
* - Empty paths or just whitespace
* - Single dot "." (current directory)
* - Paths ending with "/" (directories)
* - Paths without a file extension in the last segment (likely directories)
*/
function isOpenableFilePath(path: string): boolean {
if (!path || !path.trim()) return false;
const trimmed = path.trim();
if (trimmed === ".") return false;
if (trimmed.endsWith("/")) return false;
// Get the last segment of the path
const lastSegment = trimmed.split("/").pop() || "";
// If last segment has no extension and doesn't start with a dot (hidden file), it's likely a directory
if (!lastSegment.includes(".")) return false;
return true;
}
export const FilePathButton = memo(function FilePathButton({
filePath,
worktreePath,
onOpenFile,
variant = "badge",
className = "",
}: FilePathButtonProps) {
const relativePath = toRelativePath(filePath, worktreePath);
const canOpen = isOpenableFilePath(filePath);
const handleClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
onOpenFile?.(filePath);
},
[onOpenFile, filePath],
);
const baseStyles = "font-mono truncate cursor-pointer transition-colors block w-full text-left";
const variantStyles = {
badge:
"text-xs px-1.5 py-0.5 rounded bg-indigo-500/20 hover:bg-indigo-500/30 text-indigo-400 hover:underline",
"list-item":
"flex items-center px-2 py-1 rounded hover:bg-indigo-500/20 text-indigo-400 hover:underline",
link: "text-xs text-muted-foreground hover:text-foreground underline decoration-muted-foreground/40 hover:decoration-foreground/60",
}[variant];
if (onOpenFile && canOpen) {
return (
<button
type="button"
onClick={handleClick}
className={cn(baseStyles, variantStyles, className)}
title={filePath}
>
{relativePath}
</button>
);
}
const inactiveStyles = {
badge:
"text-xs text-muted-foreground/60 truncate font-mono bg-muted/30 px-1.5 py-0.5 rounded block w-full",
"list-item": "px-2 py-1 font-mono text-muted-foreground truncate block w-full",
link: "text-xs font-mono text-muted-foreground/60 truncate block w-full",
}[variant];
return (
<span className={cn(inactiveStyles, className)} title={filePath}>
{relativePath}
</span>
);
});