Files
kandev/apps/web/components/task/chat/messages/file-path-button.tsx
T
2026-02-06 00:13:15 +00:00

75 lines
2.6 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>
);
});