/* eslint-disable @typescript-eslint/no-explicit-any */ import React from "react"; import { HostNotesIndicator } from "../host/HostNotesIndicator"; import { VirtualizedGroupedHostCollection, VirtualizedHostCollection, } from "./VirtualizedHostCollection"; import { VaultEntityIcon, vaultPrimaryIconClass } from "./VaultEntityIcon"; import { clearVaultDropIndicator, getVaultDropIntent, getVaultDropPosition, hasVaultDragType, handleVaultHostDropToGroup, handleVaultRootDrop, markVaultDropIndicator, useVaultGridLayoutAnimation, } from "./vaultReorderDrag"; import { hostCardFocusClassName, isHostClickFocusSelected, resolveGroupActivateAction, resolveHostActivateAction, shouldClearHostFocusOnBackgroundClick, type HostClickBehavior, } from "../../domain/hostClickBehavior"; import type { GroupNode, Host } from "../../domain/models"; import { isPluginHostProtocol } from "../../domain/pluginConnection"; import { OpenDualPaneSftpMenuItem } from "../host/HostTreeContextMenus"; type VaultHostListSectionContext = Record; export const getVaultTreeAutoExpandKey = ( search: string | undefined, selectedTags: string[] | undefined, ): string | undefined => { const normalizedSearch = search?.trim() ?? ""; const normalizedTags = [...(selectedTags ?? [])].sort(); return normalizedSearch || normalizedTags.length > 0 ? JSON.stringify([normalizedSearch, normalizedTags]) : undefined; }; const isRelatedTargetInside = ( currentTarget: HTMLElement, relatedTarget: EventTarget | null, ) => { return ( typeof Node !== "undefined" && relatedTarget instanceof Node && currentTarget.contains(relatedTarget) ); }; const EMPTY_GROUP_PATH_SET = new Set(); export function VaultHostListSection({ ctx }: { ctx: VaultHostListSectionContext }) { const { Badge, Boolean, Button, cancelInlineGroupEdit, CheckSquare, ClipboardCopy, Clock, cn, commitInlineGroupRename, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, Copy, displayedGroups, displayedHosts, DistroAvatar, Edit2, FileSymlink, FolderPlus, FolderTree, getDropTargetClasses, getEffectiveHostDistro, groupConfigs, groupedDisplayHosts, handleCopyCredentials, handleCopyHostname, handleDuplicateHost, handleEditGroupConfig, handleEditHost, handleHostConnect, hostClickBehavior: hostClickBehaviorProp, handleUnmanageGroup, hasHostsSidePanel, hostListScrollRef, HostTreeView, isHostsSectionActive, isMultiSelectMode, lastPinnedId, LayoutGrid, managedGroupPaths, moveGroup, moveHostToGroup, onDeleteHost, Pin, pinnedHosts, Plug, recentHosts, reorderGroup, reorderHost, sanitizeHost, search, selectedGroupPath, selectedGroupPaths, selectedHostIds, selectedTags, sessionCount, setDeleteTargetPath, setDragOverDropTarget, setGroupDragOverDropTarget, setIsDeleteGroupOpen, setIsNewFolderOpen, setLastPinnedId, setNewFolderName, setSelectedGroupPath, setTargetParentPath, shouldHideEmptyRootHostsSection, showRecentHosts, sortMode, Square, Star, startInlineDeleteGroup, startInlineNewGroup, startInlineRenameGroup, t, toggleGroupSelection, toggleHostPinned, toggleHostSelection, Trash2, treeExpandedState, treeViewGroupTree, treeViewHosts, viewMode, visibleDisplayedHosts } = ctx; const hostClickBehavior: HostClickBehavior = hostClickBehaviorProp === 'select' ? 'select' : 'connect'; const multiSelectedGroupPaths: Set = selectedGroupPaths ?? EMPTY_GROUP_PATH_SET; const [draggingHostId, setDraggingHostId] = React.useState(null); const draggingHostIdRef = React.useRef(null); const lastPreviewReorderRef = React.useRef(null); const prepareGridLayoutAnimation = useVaultGridLayoutAnimation(hostListScrollRef); const [focusedHostId, setFocusedHostId] = React.useState(null); const [focusedGroupPath, setFocusedGroupPath] = React.useState(null); const hostListFilterFocusKey = React.useMemo( () => getVaultTreeAutoExpandKey(search, selectedTags) ?? "", [search, selectedTags], ); const [prevHostListFilterFocusKey, setPrevHostListFilterFocusKey] = React.useState( hostListFilterFocusKey, ); // Clear keyboard/selection focus as soon as search or tags change so the // virtual list cannot steal DOM focus back from the search input on the // same commit (useEffect would run too late). if (hostListFilterFocusKey !== prevHostListFilterFocusKey) { setPrevHostListFilterFocusKey(hostListFilterFocusKey); setFocusedHostId(null); setFocusedGroupPath(null); } const hostCollectionLayoutKey = [ displayedGroups.length, hasHostsSidePanel ? "panel" : "full", lastPinnedId ?? "", pinnedHosts.length, recentHosts.length, selectedGroupPath ?? "root", showRecentHosts ? "recent" : "hidden", sortMode, viewMode, ].join("|"); // Stable wrappers so HostTreeView memo can skip parent vault re-renders. const handleTreeDeleteHost = React.useCallback( (host: Host) => { onDeleteHost(host.id); }, [onDeleteHost], ); const handleTreeGroupDropClasses = React.useCallback( (path: string) => getDropTargetClasses({ kind: "group", path }), [getDropTargetClasses], ); const treeAutoExpandGroupsKey = React.useMemo( () => getVaultTreeAutoExpandKey(search, selectedTags), [search, selectedTags], ); React.useEffect(() => { if (isMultiSelectMode) { setFocusedHostId(null); setFocusedGroupPath(null); } }, [isMultiSelectMode]); React.useEffect(() => { setFocusedHostId(null); setFocusedGroupPath(null); }, [selectedGroupPath, viewMode, hostClickBehavior]); const activateHost = React.useCallback((host: Host) => { const action = resolveHostActivateAction({ behavior: hostClickBehavior, isMultiSelectMode, focusedHostId, hostId: host.id, }); if (action === "toggle-multi") { toggleHostSelection(host.id); return; } if (action === "select") { setFocusedHostId(host.id); setFocusedGroupPath(null); return; } handleHostConnect(host); }, [focusedHostId, handleHostConnect, hostClickBehavior, isMultiSelectMode, toggleHostSelection]); const focusHost = React.useCallback((host: Host) => { setFocusedHostId(host.id); setFocusedGroupPath(null); }, []); const mainKeyboardHosts = groupedDisplayHosts ? groupedDisplayHosts.flatMap((group) => group.hosts) : visibleDisplayedHosts; const focusHostAndElement = (host: Host) => { focusHost(host); queueMicrotask(() => { const scrollElement = hostListScrollRef.current as HTMLElement | null; const element = [...(scrollElement?.querySelectorAll("[data-host-id]") ?? [])] .find((candidate) => candidate.dataset.hostId === host.id); element?.focus(); }); }; const focusGroupAndElement = (group: GroupNode) => { setFocusedGroupPath(group.path); setFocusedHostId(null); queueMicrotask(() => { const scrollElement = hostListScrollRef.current as HTMLElement | null; const element = [...(scrollElement?.querySelectorAll("[data-group-path]") ?? [])] .find((candidate) => candidate.dataset.groupPath === group.path); element?.focus(); }); }; const keyboardHostSections = [ { key: "pinned", hasItems: pinnedHosts.length > 0, focusEdge: (direction: "previous" | "next") => focusHostAndElement( direction === "next" ? pinnedHosts[0] : pinnedHosts.at(-1)!, ), }, { key: "recent", hasItems: showRecentHosts && recentHosts.length > 0, focusEdge: (direction: "previous" | "next") => focusHostAndElement( direction === "next" ? recentHosts[0] : recentHosts.at(-1)!, ), }, { key: "groups", hasItems: displayedGroups.length > 0, focusEdge: (direction: "previous" | "next") => focusGroupAndElement( direction === "next" ? displayedGroups[0] : displayedGroups.at(-1)!, ), }, { key: "main", hasItems: mainKeyboardHosts.length > 0, focusEdge: (direction: "previous" | "next") => focusHostAndElement( direction === "next" ? mainKeyboardHosts[0] : mainKeyboardHosts.at(-1)!, ), }, ]; const navigateHostSection = ( sectionKey: "pinned" | "recent" | "groups" | "main", direction: "previous" | "next", ) => { const currentSectionIndex = keyboardHostSections.findIndex((section) => section.key === sectionKey); const step = direction === "next" ? 1 : -1; let sectionIndex = currentSectionIndex + step; while (sectionIndex >= 0 && sectionIndex < keyboardHostSections.length) { const section = keyboardHostSections[sectionIndex]; if (section.hasItems) { section.focusEdge(direction); return; } sectionIndex += step; } }; const initialKeyboardHostId = pinnedHosts[0]?.id ?? (showRecentHosts ? recentHosts[0]?.id : undefined) ?? groupedDisplayHosts?.[0]?.hosts[0]?.id ?? visibleDisplayedHosts[0]?.id; const focusedHostIsVisible = Boolean( focusedHostId && [ ...pinnedHosts, ...(showRecentHosts ? recentHosts : []), ...mainKeyboardHosts, ].some((host) => host.id === focusedHostId), ); const getHostTabIndex = (hostId: string) => ( (focusedHostIsVisible ? focusedHostId === hostId : initialKeyboardHostId === hostId) ? 0 : -1 ); const initialKeyboardGroupPath = displayedGroups[0]?.path; const focusedGroupIsVisible = Boolean( focusedGroupPath && displayedGroups.some((group) => group.path === focusedGroupPath), ); const getGroupTabIndex = (groupPath: string) => ( (focusedGroupIsVisible ? focusedGroupPath === groupPath : initialKeyboardGroupPath === groupPath) ? 0 : -1 ); const isHostFocusSelected = (hostId: string) => ( isHostClickFocusSelected({ behavior: hostClickBehavior, isMultiSelectMode, focusedHostId, hostId, }) ); const isGroupFocusSelected = (groupPath: string) => ( hostClickBehavior === "select" && !isMultiSelectMode && focusedGroupPath === groupPath ); const activateGroup = React.useCallback((groupPath: string) => { if (isMultiSelectMode) { toggleGroupSelection(groupPath); return; } const action = resolveGroupActivateAction({ behavior: hostClickBehavior, focusedGroupPath, groupPath, }); if (action === "select") { setFocusedGroupPath(groupPath); setFocusedHostId(null); return; } setSelectedGroupPath(groupPath); }, [focusedGroupPath, hostClickBehavior, isMultiSelectMode, setSelectedGroupPath, toggleGroupSelection]); const handleHostListClick = React.useCallback((event: React.MouseEvent) => { const target = event.target; const clickedWithinHostList = target instanceof Node && event.currentTarget.contains(target); const clickedHostOrGroup = target instanceof Element && !!target.closest("[data-host-id], [data-group-path]"); if (!shouldClearHostFocusOnBackgroundClick({ behavior: hostClickBehavior, isMultiSelectMode, clickedWithinHostList, clickedHostOrGroup, })) return; setFocusedHostId(null); setFocusedGroupPath(null); }, [hostClickBehavior, isMultiSelectMode]); const resetHostDragState = React.useCallback(() => { draggingHostIdRef.current = null; setDraggingHostId(null); lastPreviewReorderRef.current = null; setDragOverDropTarget(null); }, [setDragOverDropTarget]); const handleHostDragStart = React.useCallback((e: React.DragEvent, hostId: string) => { // copyMove: vault reorder uses move; focus-sidebar append uses copy. e.dataTransfer.effectAllowed = "copyMove"; e.dataTransfer.setData("host-id", hostId); draggingHostIdRef.current = hostId; setDraggingHostId(hostId); lastPreviewReorderRef.current = null; // Grid preview reorder can move the card into another virtual row, and rows // are separate React parents, so the source node gets unmounted mid-drag. // A detached node no longer bubbles dragend up to the container handler, so // bind the cleanup natively on the node itself - it still receives dragend. const sourceNode = e.currentTarget as HTMLElement; const handleNativeDragEnd = () => { sourceNode.removeEventListener("dragend", handleNativeDragEnd); clearVaultDropIndicator(); resetHostDragState(); }; sourceNode.addEventListener("dragend", handleNativeDragEnd); }, [resetHostDragState]); const renderHostEditButton = (host: any, compact = false) => ( ); const renderGroupEditButton = (groupPath: string, compact = false) => ( ); return
{ const target = (e.target as Element | null)?.closest("[data-host-id], [data-group-path]"); if (target) e.preventDefault(); if (!(target instanceof HTMLElement)) return; const draggedGroupPath = e.dataTransfer.getData("group-path"); const isDraggingGroup = hasVaultDragType(e.dataTransfer, "group-path"); const targetGroupPath = target.getAttribute("data-group-path"); if (isDraggingGroup && targetGroupPath && draggedGroupPath !== targetGroupPath) { const intent = getVaultDropIntent(target, e.clientX, e.clientY, viewMode === "grid"); if (intent === "inside") { clearVaultDropIndicator(); return; } markVaultDropIndicator(target, intent, viewMode === "grid" ? "x" : "y"); return; } if (viewMode !== "grid") { markVaultDropIndicator(target, getVaultDropPosition(target, e.clientX, e.clientY)); return; } const draggedHostId = draggingHostIdRef.current || e.dataTransfer.getData("host-id"); const targetHostId = target.getAttribute("data-host-id"); if (!draggedHostId || !targetHostId || draggedHostId === targetHostId) return; const position = getVaultDropPosition(target, e.clientX, e.clientY, true); const previewKey = `${draggedHostId}:${targetHostId}:${position}`; if (lastPreviewReorderRef.current === previewKey) return; prepareGridLayoutAnimation(); lastPreviewReorderRef.current = previewKey; reorderHost(draggedHostId, targetHostId, position); }} onDragOver={(e) => { const target = (e.target as Element | null)?.closest("[data-host-id], [data-group-path]"); if (!(target instanceof HTMLElement) || viewMode === "grid") return; const draggedGroupPath = e.dataTransfer.getData("group-path"); const isDraggingGroup = hasVaultDragType(e.dataTransfer, "group-path"); const targetGroupPath = target.getAttribute("data-group-path"); if (isDraggingGroup && targetGroupPath && draggedGroupPath !== targetGroupPath) { const intent = getVaultDropIntent(target, e.clientX, e.clientY, false); if (intent === "inside") { clearVaultDropIndicator(); return; } markVaultDropIndicator(target, intent); return; } markVaultDropIndicator(target, getVaultDropPosition(target, e.clientX, e.clientY)); }} onDropCapture={(e) => { clearVaultDropIndicator(); const draggedHostId = e.dataTransfer.getData("host-id"); const draggedGroupPath = e.dataTransfer.getData("group-path"); const target = (e.target as Element | null)?.closest("[data-host-id], [data-group-path]"); // Always clear the dimmed drag styling: in grid view the live preview // reorder can leave the dragged card itself under the cursor, which // used to fall through every branch below without resetting. if (draggedHostId) resetHostDragState(); if (!(target instanceof HTMLElement)) return; const targetHostId = target.getAttribute("data-host-id"); const targetGroupPath = target.getAttribute("data-group-path"); if (draggedHostId && targetHostId && draggedHostId !== targetHostId) { e.preventDefault(); e.stopPropagation(); const position = getVaultDropPosition(target, e.clientX, e.clientY, viewMode === "grid"); const previewKey = `${draggedHostId}:${targetHostId}:${position}`; if (viewMode !== "grid" || lastPreviewReorderRef.current !== previewKey) { prepareGridLayoutAnimation(); reorderHost(draggedHostId, targetHostId, position); } resetHostDragState(); return; } if (draggedGroupPath && targetGroupPath && draggedGroupPath !== targetGroupPath) { const intent = getVaultDropIntent(target, e.clientX, e.clientY, viewMode === "grid"); if (intent === "inside") return; prepareGridLayoutAnimation(); const handled = reorderGroup(draggedGroupPath, targetGroupPath, intent); if (handled) { e.preventDefault(); e.stopPropagation(); } } }} onDragEndCapture={() => { clearVaultDropIndicator(); resetHostDragState(); }} > {viewMode !== "tree" && (
{selectedGroupPath && selectedGroupPath .split("/") .filter(Boolean) .map((part, idx, arr) => { const crumbPath = arr.slice(0, idx + 1).join("/"); const isLast = idx === arr.length - 1; return ( ); })}
)} {/* Pinned hosts section - only at root level */} {viewMode !== "tree" && !selectedGroupPath && pinnedHosts.length > 0 && (

{t("vault.hosts.pinned")}

items={pinnedHosts} itemKey={(host) => host.id} scrollRef={hostListScrollRef} viewMode={viewMode} layoutKey={`pinned:${hostCollectionLayoutKey}`} ariaLabel={t("vault.hosts.pinned")} onActiveItemChange={focusHost} activeItemKey={focusedHostId} onBoundaryNavigation={(direction) => navigateHostSection("pinned", direction)} renderItem={(host) => { const safeHost = sanitizeHost(host); const effectiveDistro = getEffectiveHostDistro(safeHost); const distroBadge = { text: (safeHost.os || "L")[0].toUpperCase(), label: effectiveDistro || safeHost.os || "Linux", }; return (
{ if (lastPinnedId === host.id) setLastPinnedId(null); }} draggable={!isMultiSelectMode} onDragStart={(e) => handleHostDragStart(e, host.id)} onClick={() => { activateHost(safeHost); }} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); activateHost(safeHost); }} > {viewMode === "grid" && ( )}
{isMultiSelectMode && (
{selectedHostIds.has(host.id) ? ( ) : ( )}
)}
{safeHost.label} {viewMode !== "grid" && renderHostEditButton(host, true)}
{safeHost.username}@{safeHost.hostname}
{viewMode === "grid" && renderHostEditButton(host)}
handleHostConnect(host)}> {t('vault.hosts.connect')} handleEditHost(host)}> {t('action.edit')} handleDuplicateHost(host)}> {t('action.duplicate')} {!isPluginHostProtocol(host.protocol) ? ( handleCopyHostname(host)}> {t('terminal.statusbar.copyHostname.label')} ) : null} handleCopyCredentials(host)}> {t('vault.hosts.copyCredentials')} toggleHostPinned(host.id)}> {t('vault.hosts.unpin')} onDeleteHost(host.id)}> {t('action.delete')}
); }} />
)} {/* Recently Connected section - only at root level, toggleable */} {viewMode !== "tree" && !selectedGroupPath && showRecentHosts && recentHosts.length > 0 && (

{t("vault.hosts.recentlyConnected")}

items={recentHosts} itemKey={(host) => host.id} scrollRef={hostListScrollRef} viewMode={viewMode} layoutKey={`recent:${hostCollectionLayoutKey}`} ariaLabel={t("vault.hosts.recentlyConnected")} onActiveItemChange={focusHost} activeItemKey={focusedHostId} onBoundaryNavigation={(direction) => navigateHostSection("recent", direction)} renderItem={(host) => { const safeHost = sanitizeHost(host); const effectiveDistro = getEffectiveHostDistro(safeHost); const distroBadge = { text: (safeHost.os || "L")[0].toUpperCase(), label: effectiveDistro || safeHost.os || "Linux", }; return (
handleHostDragStart(e, host.id)} onClick={() => { activateHost(safeHost); }} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); activateHost(safeHost); }} >
{isMultiSelectMode && (
{selectedHostIds.has(host.id) ? ( ) : ( )}
)}
{safeHost.label} {viewMode !== "grid" && renderHostEditButton(host, true)}
{safeHost.username}@{safeHost.hostname}
{viewMode === "grid" && renderHostEditButton(host)}
handleHostConnect(host)}> {t('vault.hosts.connect')} handleEditHost(host)}> {t('action.edit')} handleDuplicateHost(host)}> {t('action.duplicate')} {!isPluginHostProtocol(host.protocol) ? ( handleCopyHostname(host)}> {t('terminal.statusbar.copyHostname.label')} ) : null} handleCopyCredentials(host)}> {t('vault.hosts.copyCredentials')} toggleHostPinned(host.id)}> {host.pinned ? t('vault.hosts.unpin') : t('vault.hosts.pinToTop')} onDeleteHost(host.id)}> {t('action.delete')}
); }} />
)} {viewMode !== "tree" && displayedGroups.length > 0 && (

{t("vault.groups.title")}

{t("vault.groups.total", { count: displayedGroups.length })}
)} {viewMode !== "tree" && ( items={displayedGroups} itemKey={(node) => node.path} scrollRef={hostListScrollRef} viewMode={viewMode} layoutKey={`groups:${hostCollectionLayoutKey}`} ariaLabel={t("vault.groups.title")} activeItemKey={focusedGroupIsVisible ? focusedGroupPath : null} onActiveItemChange={(node) => { setFocusedGroupPath(node.path); setFocusedHostId(null); }} onBoundaryNavigation={(direction) => navigateHostSection("groups", direction)} onDragOver={(e) => { e.preventDefault(); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); if (handleVaultHostDropToGroup({ dataTransfer: e.dataTransfer, groupPath: selectedGroupPath, moveHostToGroup, resetHostDragState, })) return; const groupPath = e.dataTransfer.getData("group-path"); if (groupPath && selectedGroupPath !== null) moveGroup(groupPath, selectedGroupPath); }} renderItem={(node) => (
e.dataTransfer.setData("group-path", node.path) } onDoubleClick={() => { if (!isMultiSelectMode) setSelectedGroupPath(node.path); }} onClick={() => activateGroup(node.path)} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); activateGroup(node.path); }} onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); if (hasVaultDragType(e.dataTransfer, "group-path")) { const intent = getVaultDropIntent(e.currentTarget, e.clientX, e.clientY, viewMode === "grid"); if (intent !== "inside") { setDragOverDropTarget((current) => current?.kind === "group" && current.path === node.path ? null : current, ); return; } } setDragOverDropTarget({ kind: "group", path: node.path }); }} onDragLeave={(e) => { const nextTarget = e.relatedTarget; if (isRelatedTargetInside(e.currentTarget, nextTarget)) { return; } setDragOverDropTarget((current) => current?.kind === "group" && current.path === node.path ? null : current, ); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); setDragOverDropTarget(null); if (handleVaultHostDropToGroup({ dataTransfer: e.dataTransfer, groupPath: node.path, moveHostToGroup, resetHostDragState, })) return; const groupPath = e.dataTransfer.getData("group-path"); if (groupPath) { const intent = getVaultDropIntent(e.currentTarget, e.clientX, e.clientY, viewMode === "grid"); if (intent === "inside") moveGroup(groupPath, node.path); } }} >
: : } />
{node.name} {!isMultiSelectMode && viewMode !== "grid" && renderGroupEditButton(node.path, true)} {managedGroupPaths.has(node.path) && ( Managed )}
{t("vault.groups.hostsCount", { count: node.totalHostCount ?? node.hosts.length })}
{!isMultiSelectMode && viewMode === "grid" && renderGroupEditButton(node.path)}
{ setTargetParentPath(node.path); setNewFolderName(""); setIsNewFolderOpen(true); }} > {t("vault.groups.newSubgroup")} handleEditGroupConfig(node.path)} > {t("vault.groups.settings")} { setDeleteTargetPath(node.path); setIsDeleteGroupOpen(true); }} > {t("vault.groups.delete")}
)} /> )} {!shouldHideEmptyRootHostsSection && (

{t("vault.nav.hosts")}

{t("vault.hosts.header.entries", { count: viewMode === "tree" ? treeViewHosts.length : visibleDisplayedHosts.length })}
{t("vault.hosts.header.live", { count: sessionCount })}
{viewMode === "tree" ? ( ) : sortMode === "group" && groupedDisplayHosts ? ( <> groups={groupedDisplayHosts} itemKey={(host) => host.id} scrollRef={hostListScrollRef} viewMode={viewMode} layoutKey={hostCollectionLayoutKey} ariaLabel={t("vault.nav.hosts")} onActiveItemChange={focusHost} activeItemKey={focusedHostId} onBoundaryNavigation={(direction) => navigateHostSection("main", direction)} renderGroupHeader={(group) => (
{group.name || t("vault.groups.ungrouped")} ({group.hosts.length})
)} renderItem={(host: Host, group) => { const safeHost = sanitizeHost(host); const effectiveDistro = getEffectiveHostDistro(safeHost); const distroBadge = { text: (safeHost.os || "L")[0].toUpperCase(), label: effectiveDistro || safeHost.os || "Linux", }; return (
handleHostDragStart(e, host.id)} onClick={() => { activateHost(safeHost); }} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); activateHost(safeHost); }} > {host.pinned && viewMode === "grid" && ( )}
{isMultiSelectMode && ( )}
{safeHost.label} {viewMode !== "grid" && renderHostEditButton(host, true)} {safeHost.managedSourceId && ( managed )}
{safeHost.username}@{safeHost.hostname}
{viewMode === "grid" && renderHostEditButton(host)}
handleHostConnect(host)} > {t('vault.hosts.connect')} handleEditHost(host)} > {t('action.edit')} handleDuplicateHost(host)} > {t('action.duplicate')} {!isPluginHostProtocol(host.protocol) ? ( handleCopyHostname(host)}> {t('terminal.statusbar.copyHostname.label')} ) : null} handleCopyCredentials(host)} > {t('vault.hosts.copyCredentials')} toggleHostPinned(host.id)}> {host.pinned ? t('vault.hosts.unpin') : t('vault.hosts.pinToTop')} onDeleteHost(host.id)} > {t('action.delete')}
); }} /> {groupedDisplayHosts.length === 0 && (

{t('vault.hosts.empty.title')}

{t('vault.hosts.empty.desc')}

)} ) : ( <> items={visibleDisplayedHosts} itemKey={(host) => host.id} scrollRef={hostListScrollRef} viewMode={viewMode} layoutKey={hostCollectionLayoutKey} ariaLabel={t("vault.nav.hosts")} onActiveItemChange={focusHost} activeItemKey={focusedHostId} onBoundaryNavigation={(direction) => navigateHostSection("main", direction)} renderItem={(host: Host) => { const safeHost = sanitizeHost(host); const effectiveDistro = getEffectiveHostDistro(safeHost); const distroBadge = { text: (safeHost.os || "L")[0].toUpperCase(), label: effectiveDistro || safeHost.os || "Linux", }; return (
handleHostDragStart(e, host.id)} onClick={() => { activateHost(safeHost); }} onKeyDown={(event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); activateHost(safeHost); }} > {host.pinned && viewMode === "grid" && ( )}
{isMultiSelectMode && ( )}
{safeHost.label} {viewMode !== "grid" && renderHostEditButton(host, true)} {safeHost.managedSourceId && ( managed )}
{safeHost.username}@{safeHost.hostname}
{viewMode === "grid" && renderHostEditButton(host)}
handleHostConnect(host)} > {t('vault.hosts.connect')} handleEditHost(host)} > {t('action.edit')} handleDuplicateHost(host)} > {t('action.duplicate')} {!isPluginHostProtocol(host.protocol) ? ( handleCopyHostname(host)}> {t('terminal.statusbar.copyHostname.label')} ) : null} handleCopyCredentials(host)} > {t('vault.hosts.copyCredentials')} toggleHostPinned(host.id)}> {host.pinned ? t('vault.hosts.unpin') : t('vault.hosts.pinToTop')} onDeleteHost(host.id)} > {t('action.delete')}
); }} /> {displayedHosts.length === 0 && (

{t('vault.hosts.empty.title')}

{t('vault.hosts.empty.desc')}

)} )}
)}
; }