import { Folder, LayoutGrid, Search, Shield, Terminal, TerminalSquare, } from "lucide-react"; import React, { memo, useEffect, useRef, useState } from "react"; import { Host, TerminalSession, Workspace } from "../types"; type QuickSwitcherItem = { type: "host" | "tab" | "workspace" | "action"; id: string; data?: Host | TerminalSession | Workspace; }; import { DistroAvatar } from "./DistroAvatar"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { ScrollArea } from "./ui/scroll-area"; interface QuickSwitcherProps { isOpen: boolean; query: string; results: Host[]; sessions: TerminalSession[]; workspaces: Workspace[]; onQueryChange: (value: string) => void; onSelect: (host: Host) => void; onSelectTab: (tabId: string) => void; onClose: () => void; onCreateLocalTerminal?: () => void; onCreateWorkspace?: () => void; } const QuickSwitcherInner: React.FC = ({ isOpen, query, results, sessions, workspaces, onQueryChange, onSelect, onSelectTab, onClose, onCreateLocalTerminal, onCreateWorkspace, }) => { const [isFocused, setIsFocused] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const inputRef = useRef(null); const containerRef = useRef(null); // Reset state when opening useEffect(() => { if (isOpen) { setIsFocused(false); setSelectedIndex(0); // Auto focus the input after a short delay setTimeout(() => { inputRef.current?.focus(); }, 50); } }, [isOpen]); // Handle clicks outside the container useEffect(() => { if (!isOpen) return; const handleClickOutside = (e: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [isOpen, onClose]); if (!isOpen) return null; const showCategorized = isFocused || query.trim().length > 0; // Get orphan sessions (sessions without workspace) const orphanSessions = sessions.filter((s) => !s.workspaceId); // Build categorized items for navigation const buildFlatItems = () => { const items: QuickSwitcherItem[] = []; if (showCategorized) { // Hosts results.forEach((host) => items.push({ type: "host", id: host.id, data: host }), ); // Tabs (built-in + sessions + workspaces) items.push({ type: "tab", id: "vault" }); items.push({ type: "tab", id: "sftp" }); orphanSessions.forEach((s) => items.push({ type: "tab", id: s.id, data: s }), ); workspaces.forEach((w) => items.push({ type: "workspace", id: w.id, data: w }), ); // Quick connect actions items.push({ type: "action", id: "local-terminal" }); } else { // Recent connections only results.forEach((host) => items.push({ type: "host", id: host.id, data: host }), ); } return items; }; const flatItems = buildFlatItems(); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setSelectedIndex((prev) => Math.min(prev + 1, flatItems.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelectedIndex((prev) => Math.max(prev - 1, 0)); } else if (e.key === "Enter" && flatItems.length > 0) { e.preventDefault(); const item = flatItems[selectedIndex]; handleItemSelect(item); } }; const handleItemSelect = (item: QuickSwitcherItem) => { switch (item.type) { case "host": onSelect(item.data as Host); break; case "tab": case "workspace": onSelectTab(item.id); onClose(); break; case "action": if (item.id === "local-terminal" && onCreateLocalTerminal) { onCreateLocalTerminal(); onClose(); } break; } }; // Helper to get item index in flat list const getItemIndex = (type: string, id: string) => { return flatItems.findIndex((item) => item.type === type && item.id === id); }; const renderHostItem = (host: Host) => { const idx = getItemIndex("host", host.id); const isSelected = idx === selectedIndex; return (
{ onSelect(host); }} onMouseEnter={() => setSelectedIndex(idx)} >
{host.label}
{host.group ? `Personal / ${host.group}` : "Personal"}
); }; return (
{/* Search Header */}
{ onQueryChange(e.target.value); setSelectedIndex(0); }} onFocus={() => setIsFocused(true)} onKeyDown={handleKeyDown} placeholder="Search hosts or tabs" className="flex-1 h-8 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 px-0 text-sm" /> Ctrl+K
{!showCategorized ? ( /* Default view: Recent connections with header */
Recent connections
{results.length > 0 ? ( results.map(renderHostItem) ) : (
No recent connections
)}
) : ( /* Focused/searching view: Categorized items */
{/* Jump To hint */}
Jump To Ctrl+Shift+J
{/* Hosts section */} {results.length > 0 && (
Hosts
{results.map(renderHostItem)}
)} {/* Tabs section */}
Tabs
{/* Built-in tabs */} {["vault", "sftp"].map((tabId) => { const idx = getItemIndex("tab", tabId); const isSelected = idx === selectedIndex; const icon = tabId === "vault" ? ( ) : ( ); const label = tabId === "vault" ? "Vaults" : "SFTP"; return (
{ onSelectTab(tabId); onClose(); }} onMouseEnter={() => setSelectedIndex(idx)} >
{icon}
{label}
); })} {/* Workspaces */} {workspaces.map((workspace) => { const idx = getItemIndex("workspace", workspace.id); const isSelected = idx === selectedIndex; return (
{ onSelectTab(workspace.id); onClose(); }} onMouseEnter={() => setSelectedIndex(idx)} >
{workspace.title}
); })} {/* Orphan sessions */} {orphanSessions.map((session) => { const idx = getItemIndex("tab", session.id); const isSelected = idx === selectedIndex; return (
{ onSelectTab(session.id); onClose(); }} onMouseEnter={() => setSelectedIndex(idx)} >
{session.hostLabel}
); })}
{/* Quick connect section */}
Quick connect
{/* Local Terminal */} {onCreateLocalTerminal && (
{ onCreateLocalTerminal(); onClose(); }} onMouseEnter={() => setSelectedIndex(getItemIndex("action", "local-terminal")) } >
Local Terminal
)} {/* Serial removed (not supported) */}
)}
); }; export const QuickSwitcher = memo(QuickSwitcherInner); QuickSwitcher.displayName = "QuickSwitcher";