import {
Folder,
LayoutGrid,
Search,
Shield,
Terminal,
TerminalSquare,
} from "lucide-react";
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useI18n } from "../application/i18n/I18nProvider";
import { Host, TerminalSession, Workspace } from "../types";
import { KeyBinding } from "../domain/models";
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";
// Compute once at module level
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
// Memoized host item component to prevent unnecessary re-renders
const HostItem = memo(({
host,
isSelected,
onSelect,
onMouseEnter,
}: {
host: Host;
isSelected: boolean;
onSelect: (host: Host) => void;
onMouseEnter: () => void;
}) => (
onSelect(host)}
onMouseEnter={onMouseEnter}
>
{host.label}
{host.group ? `Personal / ${host.group}` : "Personal"}
));
HostItem.displayName = "HostItem";
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;
keyBindings?: KeyBinding[];
}
const QuickSwitcherInner: React.FC = ({
isOpen,
query,
results,
sessions,
workspaces,
onQueryChange,
onSelect,
onSelectTab,
onClose,
onCreateLocalTerminal,
onCreateWorkspace,
keyBindings,
}) => {
const { t } = useI18n();
// Get hotkey display strings
const getHotkeyLabel = useCallback((actionId: string) => {
const binding = keyBindings?.find(k => k.id === actionId);
if (!binding) return '';
return IS_MAC ? binding.mac : binding.pc;
}, [keyBindings]);
const quickSwitchKey = getHotkeyLabel('quick-switch');
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]);
// Memoize orphan sessions
const orphanSessions = useMemo(
() => sessions.filter((s) => !s.workspaceId),
[sessions]
);
const showCategorized = isFocused || query.trim().length > 0;
// Memoize flat items list and index map
const { flatItems, itemIndexMap } = useMemo(() => {
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 }),
);
}
// Build index map for O(1) lookup
const indexMap = new Map();
items.forEach((item, idx) => {
indexMap.set(`${item.type}:${item.id}`, idx);
});
return { flatItems: items, itemIndexMap: indexMap };
}, [showCategorized, results, orphanSessions, workspaces]);
// O(1) index lookup
const getItemIndex = useCallback((type: string, id: string) => {
return itemIndexMap.get(`${type}:${id}`) ?? -1;
}, [itemIndexMap]);
if (!isOpen) return null;
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;
}
};
return (
{/* Search Header */}
{
onQueryChange(e.target.value);
setSelectedIndex(0);
}}
onFocus={() => setIsFocused(true)}
onKeyDown={handleKeyDown}
placeholder={t("qs.search.placeholder")}
className="flex-1 h-8 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 px-0 text-sm"
/>
{quickSwitchKey && (
{quickSwitchKey.replace(/ \+ /g, '+')}
)}
{!showCategorized ? (
/* Default view: Recent connections with header */
{t("qs.recentConnections")}
{results.length > 0 ? (
results.map((host) => (
setSelectedIndex(getItemIndex("host", host.id))}
/>
))
) : (
No recent connections
)}
) : (
/* Focused/searching view: Categorized items */
{/* Jump To hint */}
{t("qs.jumpTo")}
{quickSwitchKey && (
{quickSwitchKey.replace(/ \+ /g, '+')}
)}
{/* Hosts section */}
{results.length > 0 && (
Hosts
{results.map((host) => (
setSelectedIndex(getItemIndex("host", host.id))}
/>
))}
)}
{/* 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"))
}
>
{t("qs.localTerminal")}
)}
{/* Serial removed (not supported) */}
)}
);
};
export const QuickSwitcher = memo(QuickSwitcherInner);
QuickSwitcher.displayName = "QuickSwitcher";