import { useState, useCallback, useRef, useEffect } from "react"; import type { DaemonDirectory } from "../../lib/api"; interface DirectoryInputProps { value: string; onChange: (value: string) => void; suggestions: DaemonDirectory[]; placeholder?: string; /** Repository URL to extract repo name for home directory suggestions */ repoUrl?: string | null; className?: string; disabled?: boolean; } /** Extract repository name from URL */ function extractRepoName(url: string | null | undefined): string | null { if (!url) return null; // Handle various URL formats: // https://github.com/user/repo.git -> repo // https://github.com/user/repo -> repo // git@github.com:user/repo.git -> repo // /path/to/local/repo -> repo let name = url; // Remove trailing .git if (name.endsWith(".git")) { name = name.slice(0, -4); } // Remove trailing slash if (name.endsWith("/")) { name = name.slice(0, -1); } // Get the last path segment const lastSlash = name.lastIndexOf("/"); if (lastSlash !== -1) { name = name.slice(lastSlash + 1); } // Handle git@host:user/repo format const colonIndex = name.lastIndexOf(":"); if (colonIndex !== -1) { const afterColon = name.slice(colonIndex + 1); const slashIndex = afterColon.lastIndexOf("/"); if (slashIndex !== -1) { name = afterColon.slice(slashIndex + 1); } else { name = afterColon; } } return name || null; } export function DirectoryInput({ value, onChange, suggestions, placeholder = "/path/to/directory", repoUrl, className = "", disabled = false, }: DirectoryInputProps) { const [showSuggestions, setShowSuggestions] = useState(false); const [highlightedIndex, setHighlightedIndex] = useState(-1); const inputRef = useRef(null); const dropdownRef = useRef(null); // Extract repo name for home directory suggestions const repoName = extractRepoName(repoUrl); // Process suggestions to add repo name to home directory const processedSuggestions = suggestions.map((dir) => { if (dir.directoryType === "home" && repoName) { return { ...dir, path: `${dir.path}/${repoName}`, label: `${dir.label} (${repoName})`, }; } return dir; }); // Filter suggestions based on current input const filteredSuggestions = processedSuggestions.filter((dir) => { if (!value) return true; const lowerValue = value.toLowerCase(); return ( dir.path.toLowerCase().includes(lowerValue) || dir.label.toLowerCase().includes(lowerValue) ); }); // Handle clicking outside to close dropdown useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(e.target as Node) && inputRef.current && !inputRef.current.contains(e.target as Node) ) { setShowSuggestions(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const handleFocus = useCallback(() => { setShowSuggestions(true); setHighlightedIndex(-1); }, []); const handleBlur = useCallback(() => { // Delay hiding to allow click on suggestion setTimeout(() => { setShowSuggestions(false); }, 150); }, []); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (!showSuggestions || filteredSuggestions.length === 0) return; switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightedIndex((prev) => prev < filteredSuggestions.length - 1 ? prev + 1 : 0 ); break; case "ArrowUp": e.preventDefault(); setHighlightedIndex((prev) => prev > 0 ? prev - 1 : filteredSuggestions.length - 1 ); break; case "Enter": e.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < filteredSuggestions.length) { onChange(filteredSuggestions[highlightedIndex].path); setShowSuggestions(false); } break; case "Escape": setShowSuggestions(false); break; } }, [showSuggestions, filteredSuggestions, highlightedIndex, onChange] ); const handleSelectSuggestion = useCallback( (path: string) => { onChange(path); setShowSuggestions(false); inputRef.current?.focus(); }, [onChange] ); return (
onChange(e.target.value)} onFocus={handleFocus} onBlur={handleBlur} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={disabled} className="w-full bg-[#0a1525] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3] disabled:opacity-50" /> {/* Suggestions dropdown */} {showSuggestions && filteredSuggestions.length > 0 && (
{filteredSuggestions.map((dir, index) => ( ))}
)}
); }