import {
  Activity,
  BookMarked,
  CheckSquare,
  ChevronDown,
  ClipboardCopy,
  Clock,
  Copy,
  Download,
  Edit2,
  FileCode,
  FileSymlink,
  FolderPlus,
  FolderTree,
  Globe,
  Key,
  LayoutGrid,
  List,
  Network,
  Pin,
  Plug,
  Plus,
  Search,
  Settings,
  Square,
  Star,
  TerminalSquare,
  Trash2,
  Upload,
  Usb,
  X,
  Zap,
} from "lucide-react";
import React, { Suspense, lazy, memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useI18n } from "../application/i18n/I18nProvider";
import { useStoredViewMode } from "../application/state/useStoredViewMode";
import { useStoredBoolean } from "../application/state/useStoredBoolean";
import { useStoredNumber } from "../application/state/useStoredNumber";
import { useStoredString } from "../application/state/useStoredString";
import { useTreeExpandedState } from "../application/state/useTreeExpandedState";
import { sanitizeCredentialValue } from "../domain/credentials";
import { resolveGroupDefaults, applyGroupDefaults } from "../domain/groupConfig";
import {
  getEffectiveHostDistro,
  resolveTelnetPassword,
  resolveTelnetPort,
  resolveTelnetUsername,
  sanitizeHost,
  upsertHostById,
} from "../domain/host";
import { exportHostsToCsvWithStats } from "../domain/vaultImport";
import {
  reorderVaultItems,
  reorderVaultStrings,
  type VaultOrderPosition,
} from "../domain/vaultOrder";
import {
  STORAGE_KEY_VAULT_HOSTS_SORT_MODE,
  STORAGE_KEY_VAULT_HOSTS_TREE_EXPANDED,
  STORAGE_KEY_VAULT_HOSTS_VIEW_MODE,
  STORAGE_KEY_VAULT_SIDEBAR_COLLAPSED,
  STORAGE_KEY_VAULT_SIDEBAR_WIDTH,
} from "../infrastructure/config/storageKeys";
import { cn } from "../lib/utils";
import {
  ConnectionLog,
  GroupConfig,
  Host,
  HostProtocol,
  Identity,
  KnownHost,
  ManagedSource,
  ProxyProfile,
  SerialConfig,
  SSHKey,
  ShellHistoryEntry,
  Snippet,
} from "../types";
import { AppLogo } from "./AppLogo";
import { connectHostsStaggered } from "./connectHostsStaggered";
import { DistroAvatar } from "./DistroAvatar";
import GroupDetailsPanel from "./GroupDetailsPanel";
import HostDetailsPanel from "./HostDetailsPanel";
import { HostTreeView } from "./HostTreeView";
import KeychainManager from "./KeychainManager";
import PortForwarding from "./PortForwardingNew";
import ProxyProfilesManager from "./ProxyProfilesManager";
import QuickConnectWizard from "./QuickConnectWizard";
import { isQuickConnectInput, parseQuickConnectInputWithWarnings } from "../domain/quickConnect";
import SerialConnectModal from "./SerialConnectModal";
import SerialHostDetailsPanel from "./SerialHostDetailsPanel";
import SnippetsManager from "./SnippetsManager";
import { ImportVaultDialog } from "./vault/ImportVaultDialog";
import { HostTreeGroupDeleteDialog } from "./host/HostTreeGroupDeleteDialog";
import { useHostTreeInlineGroupActions } from "./vault/useHostTreeInlineGroupActions";
import { useHostTreeInlineHostActions } from "./vault/useHostTreeInlineHostActions";
import { useRegisterVaultHostTreeActions } from "./vault/useRegisterVaultHostTreeActions";
import { Button } from "./ui/button";
import { RippleButton } from "./ui/ripple";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuTrigger,
} from "./ui/context-menu";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "./ui/dialog";
import { Dropdown, DropdownContent, DropdownTrigger } from "./ui/dropdown";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { SortDropdown, SortMode } from "./ui/sort-dropdown";
import { TagFilterDropdown } from "./ui/tag-filter-dropdown";
import { toast } from "./ui/toast";
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "./ui/tooltip";
import { Badge } from "./ui/badge";
import { HotkeyScheme, KeyBinding } from "../domain/models";
import { VaultViewLayout } from "./vault/VaultViewLayout";
import { useVaultHostCollections } from "./vault/useVaultHostCollections";
import { useVaultImportHandlers } from "./vault/useVaultImportHandlers";
import { useVaultGroupDragHandlers } from "./vault/useVaultGroupDragHandlers";

const LazyProtocolSelectDialog = lazy(() => import("./ProtocolSelectDialog"));
const LazyConnectionLogsManager = lazy(() => import("./ConnectionLogsManager"));

export type VaultSection = "hosts" | "keys" | "proxies" | "snippets" | "port" | "knownhosts" | "logs";

const haveSameHostOrderResult = (previous: Host[], next: Host[]) => {
  if (previous.length !== next.length) return false;
  return next.every((host, index) => {
    const current = previous[index];
    return (
      current?.id === host.id &&
      current.order === host.order &&
      current.group === host.group &&
      current.label === host.label &&
      current.managedSourceId === host.managedSourceId
    );
  });
};

const haveSameGroupConfigs = (previous: GroupConfig[], next: GroupConfig[]) => {
  if (previous.length !== next.length) return false;
  return next.every((config, index) => {
    const current = previous[index];
    return current?.path === config.path && current.order === config.order;
  });
};

const VAULT_SIDEBAR_MIN_WIDTH = 56;
const VAULT_SIDEBAR_DEFAULT_WIDTH = 208;
const VAULT_SIDEBAR_MAX_WIDTH = 320;
const VAULT_SIDEBAR_LABEL_THRESHOLD = 132;

const isSortMode = (value: string): value is SortMode =>
  value === "manual" ||
  value === "az" ||
  value === "za" ||
  value === "newest" ||
  value === "oldest" ||
  value === "group";

// Props without isActive - it's now subscribed internally
interface VaultViewProps {
  hosts: Host[];
  keys: SSHKey[];
  identities: Identity[];
  proxyProfiles: ProxyProfile[];
  snippets: Snippet[];
  snippetPackages: string[];
  customGroups: string[];
  knownHosts: KnownHost[];
  shellHistory: ShellHistoryEntry[];
  connectionLogs: ConnectionLog[];
  managedSources: ManagedSource[];
  sessionCount: number;
  hotkeyScheme: HotkeyScheme;
  keyBindings: KeyBinding[];
  terminalThemeId: string;
  terminalFontSize: number;
  onOpenSettings: () => void;
  onOpenQuickSwitcher: () => void;
  onCreateLocalTerminal: () => void;
  onConnectSerial?: (config: SerialConfig, options?: { charset?: string }) => void;
  onDeleteHost: (id: string) => void;
  onConnect: (host: Host) => void;
  onUpdateHosts: (hosts: Host[]) => void;
  onUpdateKeys: (keys: SSHKey[]) => void;
  onImportOrReuseKey: (draft: Partial<SSHKey>) => SSHKey;
  onUpdateIdentities: (identities: Identity[]) => void;
  onUpdateProxyProfiles: (profiles: ProxyProfile[]) => void;
  onUpdateSnippets: (snippets: Snippet[]) => void;
  onUpdateSnippetPackages: (pkgs: string[]) => void;
  onUpdateCustomGroups: (groups: string[]) => void;
  onUpdateKnownHosts: (knownHosts: KnownHost[]) => void;
  onUpdateManagedSources: (managedSources: ManagedSource[]) => void;
  onClearAndRemoveManagedSource?: (source: ManagedSource) => Promise<boolean>;
  onClearAndRemoveManagedSources?: (sources: ManagedSource[]) => Promise<void>;
  onUnmanageSource?: (sourceId: string) => void;
  onConvertKnownHost: (knownHost: KnownHost) => void;
  onToggleConnectionLogSaved: (id: string) => void;
  onDeleteConnectionLog: (id: string) => void;
  onClearUnsavedConnectionLogs: () => void;
  onOpenLogView: (log: ConnectionLog) => void;
  onRunSnippet?: (snippet: Snippet, targetHosts: Host[]) => void;
  groupConfigs: GroupConfig[];
  onUpdateGroupConfigs: (configs: GroupConfig[]) => void;
  showRecentHosts: boolean;
  showOnlyUngroupedHostsInRoot: boolean;
  // Optional: navigate to a specific section on mount or when changed
  navigateToSection?: VaultSection | null;
  onNavigateToSectionHandled?: () => void;
  terminalSettings?: { keepaliveInterval: number; keepaliveCountMax: number };
}

const VaultViewInner: React.FC<VaultViewProps> = ({
  hosts,
  keys,
  identities,
  proxyProfiles,
  snippets,
  snippetPackages,
  customGroups,
  knownHosts,
  shellHistory,
  connectionLogs,
  managedSources,
  sessionCount,
  hotkeyScheme,
  keyBindings,
  terminalThemeId,
  terminalFontSize,
  onOpenSettings,
  onOpenQuickSwitcher,
  onCreateLocalTerminal,
  onConnectSerial,
  onDeleteHost,
  onConnect,
  onUpdateHosts,
  onUpdateKeys,
  onImportOrReuseKey,
  onUpdateIdentities,
  onUpdateProxyProfiles,
  onUpdateSnippets,
  onUpdateSnippetPackages,
  onUpdateCustomGroups,
  onUpdateKnownHosts,
  onUpdateManagedSources,
  onClearAndRemoveManagedSource,
  onClearAndRemoveManagedSources,
  onUnmanageSource,
  onConvertKnownHost,
  onToggleConnectionLogSaved,
  onDeleteConnectionLog,
  onClearUnsavedConnectionLogs,
  onOpenLogView,
  onRunSnippet,
  groupConfigs,
  onUpdateGroupConfigs,
  showRecentHosts,
  showOnlyUngroupedHostsInRoot,
  navigateToSection,
  onNavigateToSectionHandled,
  terminalSettings,
}) => {
  const { t } = useI18n();
  const rootRef = useRef<HTMLDivElement>(null);
  const hostsRef = useRef(hosts);
  hostsRef.current = hosts;
  const [currentSection, setCurrentSection] = useState<VaultSection>("hosts");
  const [search, setSearch] = useState("");
  const [selectedGroupPath, setSelectedGroupPath] = useState<string | null>(
    null,
  );
  const [isNewFolderOpen, setIsNewFolderOpen] = useState(false);
  const [newFolderName, setNewFolderName] = useState("");
  const [targetParentPath, setTargetParentPath] = useState<string | null>(null);
  const [isRenameGroupOpen, setIsRenameGroupOpen] = useState(false);
  const [renameTargetPath, setRenameTargetPath] = useState<string | null>(null);
  const [renameGroupName, setRenameGroupName] = useState("");
  const [renameGroupError, setRenameGroupError] = useState<string | null>(null);
  const [isImportOpen, setIsImportOpen] = useState(false);
  const [isSerialModalOpen, setIsSerialModalOpen] = useState(false);
  const [isDeleteGroupOpen, setIsDeleteGroupOpen] = useState(false);
  const [deleteTargetPath, setDeleteTargetPath] = useState<string | null>(null);
  const [deleteGroupWithHosts, setDeleteGroupWithHosts] = useState(false);

  // Sidebar collapsed state with localStorage persistence
  const [storedSidebarCollapsed, setStoredSidebarCollapsed] = useStoredBoolean(
    STORAGE_KEY_VAULT_SIDEBAR_COLLAPSED,
    false,
  );
  const [sidebarWidth, setSidebarWidth, persistSidebarWidth] = useStoredNumber(
    STORAGE_KEY_VAULT_SIDEBAR_WIDTH,
    storedSidebarCollapsed ? VAULT_SIDEBAR_MIN_WIDTH : VAULT_SIDEBAR_DEFAULT_WIDTH,
    {
      min: VAULT_SIDEBAR_MIN_WIDTH,
      max: VAULT_SIDEBAR_MAX_WIDTH,
    },
  );
  const sidebarCollapsed = sidebarWidth < VAULT_SIDEBAR_LABEL_THRESHOLD;
  const setSidebarCollapsed = useCallback((nextCollapsed: boolean) => {
    const nextWidth = nextCollapsed ? VAULT_SIDEBAR_MIN_WIDTH : VAULT_SIDEBAR_DEFAULT_WIDTH;
    setSidebarWidth(nextWidth);
    persistSidebarWidth(nextWidth);
    setStoredSidebarCollapsed(nextCollapsed);
  }, [persistSidebarWidth, setSidebarWidth, setStoredSidebarCollapsed]);
  const handleSidebarWidthCommit = useCallback((nextWidth: number) => {
    const clampedWidth = Math.max(
      VAULT_SIDEBAR_MIN_WIDTH,
      Math.min(VAULT_SIDEBAR_MAX_WIDTH, nextWidth),
    );
    setSidebarWidth(clampedWidth);
    persistSidebarWidth(clampedWidth);
    setStoredSidebarCollapsed(clampedWidth < VAULT_SIDEBAR_LABEL_THRESHOLD);
  }, [persistSidebarWidth, setSidebarWidth, setStoredSidebarCollapsed]);

  // Handle external navigation requests
  useEffect(() => {
    if (navigateToSection) {
      setCurrentSection(navigateToSection);
      onNavigateToSectionHandled?.();
    }
  }, [navigateToSection, onNavigateToSectionHandled]);

  // View mode, sorting, and tag filter state
  const [viewMode, setViewMode] = useStoredViewMode(
    STORAGE_KEY_VAULT_HOSTS_VIEW_MODE,
    "grid",
  );
  const treeExpandedState = useTreeExpandedState(STORAGE_KEY_VAULT_HOSTS_TREE_EXPANDED);
  const [sortMode, setSortMode] = useStoredString<SortMode>(
    STORAGE_KEY_VAULT_HOSTS_SORT_MODE,
    "manual",
    isSortMode,
  );
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
  const [selectedHostIds, setSelectedHostIds] = useState<Set<string>>(new Set());
  const [isMultiSelectMode, setIsMultiSelectMode] = useState(false);

  // Host panel state (local to hosts section)
  const [isHostPanelOpen, setIsHostPanelOpen] = useState(false);
  const [editingHost, setEditingHost] = useState<Host | null>(null);
  const [newHostGroupPath, setNewHostGroupPath] = useState<string | null>(null);

  // When the side panel is open, Tailwind's viewport-based grid-cols-* can't
  // react to the narrowed content area, so we drive the host-grid column count
  // off the actual container width (measured below). A fixed column count keeps
  // a lone card at one column's width instead of stretching it across the row
  // the way auto-fit + 1fr would. The count is published as a CSS variable
  // (set imperatively) rather than React state, so re-flowing the grid never
  // re-renders this (large) component during panel transitions / window resize.
  const hostListScrollRef = useRef<HTMLDivElement>(null);
  const splitGridColsRef = useRef(0);

  // Close host panel if the host being edited was deleted.
  // Track previous host IDs so we only close for actual deletions, not for
  // unsaved new/duplicated hosts whose IDs were never in the hosts array.
  const knownHostIdsRef = useRef(new Set(hosts.map(h => h.id)));
  useEffect(() => {
    const currentIds = new Set(hosts.map(h => h.id));
    // Check against previous IDs before updating the ref
    if (editingHost && knownHostIdsRef.current.has(editingHost.id) && !currentIds.has(editingHost.id)) {
      setIsHostPanelOpen(false);
      setEditingHost(null);
      setNewHostGroupPath(null);
    }
    knownHostIdsRef.current = currentIds;
  }, [hosts, editingHost]);

  // Group panel state
  const [isGroupPanelOpen, setIsGroupPanelOpen] = useState(false);
  const [editingGroupPath, setEditingGroupPath] = useState<string | null>(null);

  // Compute inherited group defaults for the host being edited
  const editingHostGroupDefaults = useMemo(() => {
    const group = editingHost?.group || newHostGroupPath || selectedGroupPath;
    if (!group) return undefined;
    return resolveGroupDefaults(group, groupConfigs);
  }, [editingHost, newHostGroupPath, selectedGroupPath, groupConfigs]);
  const proxyProfileIdSet = useMemo(
    () => new Set(proxyProfiles.map((profile) => profile.id)),
    [proxyProfiles],
  );
  // Quick connect state
  const [quickConnectTarget, setQuickConnectTarget] = useState<{
    hostname: string;
    username?: string;
    port?: number;
  } | null>(null);
  const [isQuickConnectOpen, setIsQuickConnectOpen] = useState(false);
  const [quickConnectWarnings, setQuickConnectWarnings] = useState<string[]>([]);

  // Protocol select state (for hosts with multiple protocols)
  const [protocolSelectHost, setProtocolSelectHost] = useState<Host | null>(
    null,
  );

  // Check if search input is a quick connect address
  const isSearchQuickConnect = useMemo(() => {
    return isQuickConnectInput(search);
  }, [search]);

  // Handle connect button click - detect quick connect or regular search
  const handleConnectClick = useCallback(() => {
    if (isSearchQuickConnect) {
      const parsed = parseQuickConnectInputWithWarnings(search);
      if (parsed.target) {
        setQuickConnectTarget(parsed.target);
        setQuickConnectWarnings(parsed.warnings);
        setIsQuickConnectOpen(true);
      }
    } else {
      onOpenQuickSwitcher();
    }
  }, [isSearchQuickConnect, search, onOpenQuickSwitcher]);

  // Handle search input keydown for quick connect
  const handleSearchKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === "Enter" && isSearchQuickConnect) {
        e.preventDefault();
        handleConnectClick();
      }
    },
    [isSearchQuickConnect, handleConnectClick],
  );

  // Handle host connect. Resolution order:
  //   Telnet set as default (protocol === 'telnet')  -> connect Telnet
  //   Telnet enabled but not the default             -> ask (protocol picker)
  //   Mosh enabled                                   -> connect Mosh
  //   otherwise                                      -> connect SSH
  const handleHostConnect = useCallback(
    (host: Host) => {
      const effective = host.group
        ? applyGroupDefaults(host, resolveGroupDefaults(host.group, groupConfigs, { validProxyProfileIds: proxyProfileIdSet }), { validProxyProfileIds: proxyProfileIdSet })
        : applyGroupDefaults(host, {}, { validProxyProfileIds: proxyProfileIdSet });
      // Only prompt when Telnet is available but isn't the host's default protocol.
      if (effective.telnetEnabled && effective.protocol !== "telnet") {
        setProtocolSelectHost(effective);
      } else if (effective.protocol === "telnet") {
        // Telnet-as-default wins over a stray moshEnabled flag.
        onConnect({ ...host, moshEnabled: false });
      } else {
        onConnect(host);
      }
    },
    [onConnect, groupConfigs, proxyProfileIdSet],
  );

  // Handle protocol selection
  const handleProtocolSelect = useCallback(
    (protocol: HostProtocol, port: number) => {
      if (protocolSelectHost) {
        const hostWithProtocol: Host = {
          ...protocolSelectHost,
          protocol: (protocol === "mosh" || protocol === "et") ? "ssh" : protocol,
          port,
          moshEnabled: protocol === "mosh",
          etEnabled: protocol === "et",
        };
        onConnect(hostWithProtocol);
        setProtocolSelectHost(null);
      }
    },
    [protocolSelectHost, onConnect],
  );

  // Handle quick connect
  const handleQuickConnect = useCallback(
    (host: Host) => {
      onConnect(host);
      setIsQuickConnectOpen(false);
      setQuickConnectTarget(null);
      setQuickConnectWarnings([]);
      setSearch("");
    },
    [onConnect],
  );

  // Handle quick connect save host
  const handleQuickConnectSaveHost = useCallback(
    (host: Host) => {
      onUpdateHosts([...hosts, host]);
    },
    [hosts, onUpdateHosts],
  );

  const handleNewHost = useCallback(() => {
    setIsGroupPanelOpen(false);
    setEditingGroupPath(null);
    setEditingHost(null);
    setNewHostGroupPath(null);
    setIsHostPanelOpen(true);
  }, []);

  const handleEditHost = useCallback((host: Host) => {
    setIsGroupPanelOpen(false);
    setEditingGroupPath(null);
    setEditingHost(host);
    setIsHostPanelOpen(true);
  }, []);

  const handleDuplicateHost = useCallback((host: Host) => {
    setCurrentSection("hosts");
    setIsGroupPanelOpen(false);
    setEditingGroupPath(null);
    // Create a copy of the host with a new ID and modified label
    const duplicatedHost: Host = {
      ...host,
      id: crypto.randomUUID(),
      label: `${host.label} (${t('action.copy')})`,
      createdAt: Date.now(),
      pinned: undefined,
      lastConnectedAt: undefined,
    };
    // Open the edit panel with the duplicated host for modification
    setEditingHost(duplicatedHost);
    setIsHostPanelOpen(true);
  }, [t]);

  // Export hosts to CSV
  const handleExportHosts = useCallback(() => {
    if (hosts.length === 0) {
      toast.warning(t('vault.hosts.export.toast.noHosts'));
      return;
    }

    const { csv, exportedCount, skippedCount } = exportHostsToCsvWithStats(hosts);

    if (exportedCount === 0) {
      toast.warning(t('vault.hosts.export.toast.noHosts'));
      return;
    }

    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = `hosts_export_${new Date().toISOString().slice(0, 10)}.csv`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(url);

    if (skippedCount > 0) {
      toast.warning(t('vault.hosts.export.toast.successWithSkipped', { count: exportedCount, skipped: skippedCount }));
    } else {
      toast.success(t('vault.hosts.export.toast.success', { count: exportedCount }));
    }
  }, [hosts, t]);

  // Copy host credentials to clipboard
  const handleCopyCredentials = useCallback((host: Host) => {
    // Apply group defaults so inherited credentials are included
    const effective = host.group
      ? applyGroupDefaults(host, resolveGroupDefaults(host.group, groupConfigs, { validProxyProfileIds: proxyProfileIdSet }), { validProxyProfileIds: proxyProfileIdSet })
      : applyGroupDefaults(host, {}, { validProxyProfileIds: proxyProfileIdSet });
    // Only use telnet-specific port and credentials when protocol is explicitly telnet
    // Don't treat telnetEnabled as primary - that's just an optional protocol
    const isTelnet = effective.protocol === "telnet";

    const defaultPort = isTelnet ? 23 : 22;
    const effectivePort = isTelnet ? resolveTelnetPort(effective) : (effective.port ?? 22);

    // Bracket IPv6 addresses when appending non-default port
    let address: string;
    if (effectivePort !== defaultPort) {
      const isIPv6 = effective.hostname.includes(":") && !effective.hostname.startsWith("[");
      const hostname = isIPv6 ? `[${effective.hostname}]` : effective.hostname;
      address = `${hostname}:${effectivePort}`;
    } else {
      address = effective.hostname;
    }

    // Resolve credentials from identity if configured, otherwise use host credentials
    // For telnet hosts, use telnet-specific credentials
    const identity = effective.identityId
      ? identities.find((i) => i.id === effective.identityId)
      : undefined;

    const username = isTelnet
      ? resolveTelnetUsername(effective)
      : (identity?.username?.trim() || effective.username?.trim());

    const rawPassword = isTelnet
      ? resolveTelnetPassword(effective)
      : (identity?.password || effective.password);
    const password = sanitizeCredentialValue(rawPassword);

    if (!password) {
      toast.warning(t('vault.hosts.copyCredentials.toast.noPassword'));
      return;
    }

    const text = `host: ${address}\nusername: ${username ?? ''}\npassword: ${password}`;
    navigator.clipboard.writeText(text).then(() => {
      toast.success(t('vault.hosts.copyCredentials.toast.success'));
    });
  }, [identities, groupConfigs, proxyProfileIdSet, t]);

  const [lastPinnedId, setLastPinnedId] = useState<string | null>(null);
  const toggleHostPinned = useCallback((hostId: string) => {
    const host = hostsRef.current.find((h) => h.id === hostId);
    const isPinning = host && !host.pinned;
    startTransition(() => {
      onUpdateHosts(hostsRef.current.map((h) =>
        h.id === hostId ? { ...h, pinned: !h.pinned } : h
      ));
    });
    setLastPinnedId(isPinning ? hostId : null);
  }, [onUpdateHosts]);

  const toggleHostSelection = useCallback((hostId: string) => {
    setSelectedHostIds(prev => {
      const next = new Set(prev);
      if (next.has(hostId)) {
        next.delete(hostId);
      } else {
        next.add(hostId);
      }
      return next;
    });
  }, []);

  const clearHostSelection = useCallback(() => {
    setSelectedHostIds(new Set());
    setIsMultiSelectMode(false);
  }, []);

  const deleteSelectedHosts = useCallback(() => {
    if (selectedHostIds.size === 0) return;
    const updatedHosts = hosts.filter(h => !selectedHostIds.has(h.id));
    onUpdateHosts(updatedHosts);
    clearHostSelection();
    toast.success(t("vault.hosts.deleteMultiple.success", { count: selectedHostIds.size }));
  }, [selectedHostIds, hosts, onUpdateHosts, clearHostSelection, t]);

  const connectSelectedHosts = useCallback(() => {
    if (selectedHostIds.size === 0) return;
    // Connect each selected host in list order with its default protocol.
    // We call onConnect directly (not handleHostConnect) so multi-protocol hosts
    // connect with their configured protocol instead of opening a per-host dialog.
    const targets = hosts.filter(h => selectedHostIds.has(h.id));
    // Stagger the connects across frames so mounting N terminals (each creating
    // a WebGL context) doesn't block one frame and freeze the UI. The first host
    // still connects synchronously so its tab appears immediately.
    connectHostsStaggered(targets, onConnect);
    clearHostSelection();
    toast.success(t("vault.hosts.connectMultiple.success", { count: targets.length }));
  }, [selectedHostIds, hosts, onConnect, clearHostSelection, t]);
  const { handleImportFileSelected } = useVaultImportHandlers({
    customGroups,
    hosts,
    managedSources,
    onUpdateCustomGroups,
    onUpdateHosts,
    onUpdateManagedSources,
    setIsImportOpen,
    t,
  });

  const {
    allGroupPaths,
    allTags,
    displayedGroups,
    displayedHosts,
    groupedDisplayHosts,
    handleDeleteTag,
    handleEditTag,
    knownHostsManagerElement,
    pinnedHosts,
    pinnedRecentIds,
    recentHosts,
    shouldHideEmptyRootHostsSection,
    treeViewGroupTree,
    treeViewHosts,
    visibleDisplayedHosts,
  } = useVaultHostCollections({
    customGroups,
    groupConfigs,
    hosts,
    knownHosts,
    onConvertKnownHost,
    onUpdateHosts,
    onUpdateKnownHosts,
    search,
    selectedGroupPath,
    selectedTags,
    showOnlyUngroupedHostsInRoot,
    showRecentHosts,
    sortMode,
    viewMode,
  });


  const submitNewFolder = () => {
    if (!newFolderName.trim()) return;
    const fullPath = targetParentPath
      ? `${targetParentPath}/${newFolderName.trim()}`
      : newFolderName.trim();
    onUpdateCustomGroups(Array.from(new Set([...customGroups, fullPath])));
    setNewFolderName("");
    setTargetParentPath(null);
    setIsNewFolderOpen(false);
  };

  const submitRenameGroup = () => {
    if (!renameTargetPath) return;

    const nextName = renameGroupName.trim();
    if (!nextName) {
      setRenameGroupError(t("vault.groups.errors.required"));
      return;
    }
    if (nextName.includes("/") || nextName.includes("\\")) {
      setRenameGroupError(t("vault.groups.errors.invalidChars"));
      return;
    }

    const segments = renameTargetPath.split("/").filter(Boolean);
    const parent = segments.slice(0, -1).join("/");
    const nextPath = parent ? `${parent}/${nextName}` : nextName;
    if (nextPath === renameTargetPath) {
      setIsRenameGroupOpen(false);
      return;
    }

    const updatedGroups = customGroups.map((g) => {
      if (g === renameTargetPath) return nextPath;
      if (g.startsWith(renameTargetPath + "/"))
        return nextPath + g.slice(renameTargetPath.length);
      return g;
    });
    const updatedHosts = hosts.map((h) => {
      const g = h.group || "";
      if (g === renameTargetPath) return { ...h, group: nextPath };
      if (g.startsWith(renameTargetPath + "/"))
        return { ...h, group: nextPath + g.slice(renameTargetPath.length) };
      return h;
    });

    // Update managed sources if any match the renamed group path
    const updatedManagedSources = managedSources.map((s) => {
      if (s.groupName === renameTargetPath) return { ...s, groupName: nextPath };
      if (s.groupName.startsWith(renameTargetPath + "/"))
        return { ...s, groupName: nextPath + s.groupName.slice(renameTargetPath.length) };
      return s;
    });
    if (updatedManagedSources.some((s, i) => s !== managedSources[i])) {
      onUpdateManagedSources(updatedManagedSources);
    }

    onUpdateCustomGroups(Array.from(new Set(updatedGroups)));
    onUpdateHosts(updatedHosts);
    if (
      selectedGroupPath &&
      (selectedGroupPath === renameTargetPath ||
        selectedGroupPath.startsWith(renameTargetPath + "/"))
    ) {
      const suffix =
        selectedGroupPath === renameTargetPath
          ? ""
          : selectedGroupPath.slice(renameTargetPath.length);
      setSelectedGroupPath(nextPath + suffix);
    }

    setIsRenameGroupOpen(false);
  };

  const handleEditGroupConfig = useCallback((groupPath: string) => {
    setIsHostPanelOpen(false);
    setEditingHost(null);
    setEditingGroupPath(groupPath);
    setIsGroupPanelOpen(true);
  }, []);

  const handleSaveGroupConfig = useCallback((config: GroupConfig, _newName?: string, _newParent?: string | null) => {
    const oldPath = editingGroupPath!;
    const newPath = config.path; // Panel already computed the correct path

    // Validate no duplicate path on rename/reparent
    if (newPath !== oldPath && customGroups.includes(newPath)) {
      toast.error(t('vault.groups.errors.duplicatePath'));
      return;
    }

    // Save config (use new path)
    const updatedConfigs = [...groupConfigs.filter(c => c.path !== oldPath), config];

    // Handle path change (rename or parent change)
    if (newPath !== oldPath) {
      // Update groups, hosts, managed sources, and configs for path change
      const updatedGroups = customGroups.map((g) => {
        if (g === oldPath) return newPath;
        if (g.startsWith(oldPath + '/')) return newPath + g.slice(oldPath.length);
        return g;
      });
      const updatedHosts = hosts.map((h) => {
        const g = h.group || '';
        if (g === oldPath) return { ...h, group: newPath };
        if (g.startsWith(oldPath + '/')) return { ...h, group: newPath + g.slice(oldPath.length) };
        return h;
      });
      const updatedManagedSources = managedSources.map((s) => {
        if (s.groupName === oldPath) return { ...s, groupName: newPath };
        if (s.groupName.startsWith(oldPath + '/')) return { ...s, groupName: newPath + s.groupName.slice(oldPath.length) };
        return s;
      });
      if (updatedManagedSources.some((s, i) => s !== managedSources[i])) {
        onUpdateManagedSources(updatedManagedSources);
      }
      onUpdateCustomGroups(Array.from(new Set(updatedGroups)));
      onUpdateHosts(updatedHosts);
      // Update child config paths too
      const finalConfigs = updatedConfigs.map(c => {
        if (c.path.startsWith(oldPath + '/')) return { ...c, path: newPath + c.path.slice(oldPath.length) };
        return c;
      });
      onUpdateGroupConfigs(finalConfigs);
      if (selectedGroupPath === oldPath) setSelectedGroupPath(newPath);
      if (selectedGroupPath?.startsWith(oldPath + '/')) {
        setSelectedGroupPath(newPath + selectedGroupPath.slice(oldPath.length));
      }
    } else {
      onUpdateGroupConfigs(updatedConfigs);
    }

    setIsGroupPanelOpen(false);
    setEditingGroupPath(null);
  }, [groupConfigs, editingGroupPath, customGroups, hosts, managedSources, selectedGroupPath, onUpdateGroupConfigs, onUpdateCustomGroups, onUpdateHosts, onUpdateManagedSources, t]);

  const deleteGroupPath = async (path: string, deleteHosts: boolean = false) => {
    const keepGroups = customGroups.filter(
      (g) => !(g === path || g.startsWith(path + "/")),
    );

    // Find all managed sources under the deleted path (exact match or subgroups)
    const sourcesToRemove = managedSources.filter(s =>
      s.groupName === path || s.groupName.startsWith(path + "/")
    );

    // Clear managed blocks in SSH config files before removing sources
    // Use batch removal to avoid race conditions when multiple sources are removed
    if (sourcesToRemove.length > 0 && onClearAndRemoveManagedSources) {
      await onClearAndRemoveManagedSources(sourcesToRemove);
    } else if (sourcesToRemove.length > 0 && onClearAndRemoveManagedSource) {
      // Fallback to single removal (may have race conditions with multiple sources)
      await Promise.all(sourcesToRemove.map(s => onClearAndRemoveManagedSource(s)));
    } else if (sourcesToRemove.length > 0) {
      // Fallback: just remove sources without clearing (if callback not provided)
      const updatedSources = managedSources.filter(s =>
        s.groupName !== path && !s.groupName.startsWith(path + "/")
      );
      onUpdateManagedSources(updatedSources);
    }

    // Check if this is a subgroup under a managed group (that won't be deleted)
    // Use the most specific (deepest) matching managed source
    const parentManagedSource = managedSources
      .filter(s => path.startsWith(s.groupName + "/") && s.groupName !== path)
      .sort((a, b) => b.groupName.length - a.groupName.length)[0];

    let keepHosts: Host[];
    if (deleteHosts) {
      keepHosts = hosts.filter((h) => {
        const g = h.group || "";
        return !(g === path || g.startsWith(path + "/"));
      });
    } else {
      keepHosts = hosts.map((h) => {
        const g = h.group || "";
        if (g === path || g.startsWith(path + "/")) {
          // If deleting a subgroup under a managed group, keep managedSourceId
          // so hosts remain managed and sync to the SSH config
          if (parentManagedSource) {
            return { ...h, group: "" };
          }
          return { ...h, group: "", managedSourceId: undefined };
        }
        return h;
      });
    }

    onUpdateCustomGroups(keepGroups);
    onUpdateHosts(keepHosts);
    // Remove configs for deleted group and its children
    const updatedGroupConfigs = groupConfigs.filter(
      (c) => c.path !== path && !c.path.startsWith(path + '/')
    );
    if (updatedGroupConfigs.length !== groupConfigs.length) {
      onUpdateGroupConfigs(updatedGroupConfigs);
    }
    if (
      selectedGroupPath &&
      (selectedGroupPath === path || selectedGroupPath.startsWith(path + "/"))
    ) {
      setSelectedGroupPath(null);
    }
  };

  const moveGroup = (sourcePath: string, targetParent: string | null) => {
    const name = sourcePath.split("/").filter(Boolean).pop() || "";
    const newPath = targetParent ? `${targetParent}/${name}` : name;
    if (newPath === sourcePath || newPath.startsWith(sourcePath + "/")) return;
    if (customGroups.includes(newPath)) {
      toast.error(t('vault.groups.errors.duplicatePath'));
      return;
    }
    const updatedGroups = customGroups.map((g) => {
      if (g === sourcePath) return newPath;
      if (g.startsWith(sourcePath + "/")) return newPath + g.slice(sourcePath.length);
      return g;
    });
    const updatedHosts = hosts.map((h) => {
      const g = h.group || "";
      if (g === sourcePath) return { ...h, group: newPath };
      if (g.startsWith(sourcePath + "/"))
        return { ...h, group: newPath + g.slice(sourcePath.length) };
      return h;
    });
    // Update managed sources if any match the moved group path
    const updatedManagedSources = managedSources.map((s) => {
      if (s.groupName === sourcePath) return { ...s, groupName: newPath };
      if (s.groupName.startsWith(sourcePath + "/"))
        return { ...s, groupName: newPath + s.groupName.slice(sourcePath.length) };
      return s;
    });
    if (updatedManagedSources.some((s, i) => s !== managedSources[i])) {
      onUpdateManagedSources(updatedManagedSources);
    }
    onUpdateCustomGroups(Array.from(new Set(updatedGroups)));
    onUpdateHosts(updatedHosts);
    // Update group configs for moved paths
    const updatedGroupConfigs = groupConfigs.map((c) => {
      if (c.path === sourcePath) return { ...c, path: newPath };
      if (c.path.startsWith(sourcePath + '/'))
        return { ...c, path: newPath + c.path.slice(sourcePath.length) };
      return c;
    });
    if (updatedGroupConfigs.some((c, i) => c !== groupConfigs[i])) {
      onUpdateGroupConfigs(updatedGroupConfigs);
    }
    if (
      selectedGroupPath &&
      (selectedGroupPath === sourcePath ||
        selectedGroupPath.startsWith(sourcePath + "/"))
    ) {
      setSelectedGroupPath(newPath);
    }
  };

  const reorderHost = useCallback((sourceHostId: string, targetHostId: string, position: VaultOrderPosition) => {
    const source = hostsRef.current.find((host) => host.id === sourceHostId);
    const target = hostsRef.current.find((host) => host.id === targetHostId);
    if (!source || !target) return;
    const targetGroup = target.group || "";
    const targetManagedSource = managedSources
      .filter((sourceInfo) => targetGroup === sourceInfo.groupName || targetGroup.startsWith(`${sourceInfo.groupName}/`))
      .sort((a, b) => b.groupName.length - a.groupName.length)[0];
    const updatedHosts = hostsRef.current.map((host) =>
      host.id === sourceHostId
        ? {
          ...host,
          label:
            targetManagedSource && (!host.protocol || host.protocol === "ssh")
              ? host.label.replace(/\s/g, "")
              : host.label,
          group: targetGroup,
          managedSourceId:
            targetManagedSource && (!host.protocol || host.protocol === "ssh")
              ? targetManagedSource.id
              : undefined,
        }
        : host,
    );
    const reorderedHosts = reorderVaultItems(updatedHosts, sourceHostId, targetHostId, position);
    if (haveSameHostOrderResult(hostsRef.current, reorderedHosts)) return;
    onUpdateHosts(reorderedHosts);
    setSortMode("manual");
  }, [managedSources, onUpdateHosts, setSortMode]);

  const reorderGroup = useCallback((sourcePath: string, targetPath: string, position: VaultOrderPosition) => {
    const parentOf = (path: string) => {
      const parts = path.split("/").filter(Boolean);
      return parts.slice(0, -1).join("/");
    };
    if (parentOf(sourcePath) !== parentOf(targetPath)) return false;
    const sortableGroups = Array.from(new Set([...customGroups, sourcePath, targetPath]));
    const updatedGroups = reorderVaultStrings(sortableGroups, sourcePath, targetPath, position);
    const orderByPath = new Map(updatedGroups.map((path, index) => [path, (index + 1) * 1000]));
    const configByPath = new Map<string, GroupConfig>(groupConfigs.map((config) => [config.path, config]));
    const nextConfigs: GroupConfig[] = [
      ...updatedGroups.map((path) => {
        const existing = configByPath.get(path);
        const base: GroupConfig = existing ? { ...existing } : { path };
        return {
          ...base,
          order: orderByPath.get(path),
        };
      }),
      ...groupConfigs.filter((config) => !orderByPath.has(config.path)),
    ];
    if (
      updatedGroups.length === customGroups.length &&
      updatedGroups.every((path, index) => path === customGroups[index]) &&
      haveSameGroupConfigs(groupConfigs, nextConfigs)
    ) {
      return true;
    }
    onUpdateCustomGroups(updatedGroups);
    onUpdateGroupConfigs(nextConfigs);
    setSortMode("manual");
    return true;
  }, [customGroups, groupConfigs, onUpdateCustomGroups, onUpdateGroupConfigs, setSortMode]);
  const {
    getDropTargetClasses,
    handleUnmanageGroup,
    managedGroupPaths,
    moveHostToGroup,
    setDragOverDropTarget,
    setGroupDragOverDropTarget,
  } = useVaultGroupDragHandlers({
    hosts,
    managedSources,
    onUnmanageSource,
    onUpdateHosts,
    onUpdateManagedSources,
    t,
  });


  const {
    startInlineNewGroup,
    startInlineRenameGroup,
    startInlineDeleteGroup,
    commitInlineGroupRename,
    cancelInlineGroupEdit,
  } = useHostTreeInlineGroupActions({
    customGroups,
    hosts,
    managedSources,
    onUpdateCustomGroups,
    onUpdateHosts,
    onUpdateManagedSources,
    selectedGroupPath,
    setSelectedGroupPath,
    ensurePathExpanded: treeExpandedState.ensurePathExpanded,
    unnamedGroupLabel: t("vault.groups.unnamed"),
    t,
  });

  const {
    startInlineRenameHost,
    commitInlineHostRename,
    cancelInlineHostEdit,
  } = useHostTreeInlineHostActions({
    hosts,
    onUpdateHosts,
    t,
  });

  useRegisterVaultHostTreeActions({
    handleCopyCredentials,
    handleDuplicateHost,
    startInlineRenameHost,
    onDeleteHost,
    handleUnmanageGroup,
    moveHostToGroup,
    moveGroup,
    reorderHost,
    reorderGroup,
    managedGroupPaths,
    startInlineNewGroup,
    startInlineRenameGroup,
    startInlineDeleteGroup,
    commitInlineGroupRename,
    cancelInlineGroupEdit,
    commitInlineHostRename,
    cancelInlineHostEdit,
  });

  const isHostsSectionActive = currentSection === "hosts";
  const hasHostsSidePanel =
    isHostsSectionActive &&
    ((isGroupPanelOpen && !!editingGroupPath) || isHostPanelOpen);
  // Fixed N columns (not auto-fit) so populated rows fill the width with no
  // trailing gap AND a section with a single card (e.g. Pinned) keeps it at one
  // column's width instead of stretching it across the whole row — matching the
  // no-panel grid-cols-* behaviour, just measured from the container. The actual
  // column count rides on the --vault-grid-cols custom property (set by the
  // ResizeObserver below); the fallback applies until the first measurement.
  const splitViewGridStyle = hasHostsSidePanel
    ? { gridTemplateColumns: "var(--vault-grid-cols, repeat(2, minmax(0, 1fr)))" }
    : undefined;

  // Track the host-list container width and derive the column count the same way
  // the auto-fit grid did (≈220px min card + 12px gap), but as a fixed count so
  // lone cards don't stretch. We write the whole grid-template-columns value into
  // a CSS variable imperatively (no setState) and only when the count actually
  // changes, so panel transitions / window resizing reflow the grid natively
  // without re-rendering this component.
  useEffect(() => {
    const el = hostListScrollRef.current;
    if (!el || typeof ResizeObserver === "undefined") return;
    const GAP = 12; // matches gap-3
    const MIN_CARD = 220;
    const PADDING_X = 32; // matches px-4 on both sides
    const recompute = () => {
      const usable = el.clientWidth - PADDING_X;
      if (usable <= 0) return; // hidden / not laid out yet
      const next = Math.max(1, Math.floor((usable + GAP) / (MIN_CARD + GAP)));
      if (next === splitGridColsRef.current) return;
      splitGridColsRef.current = next;
      el.style.setProperty("--vault-grid-cols", `repeat(${next}, minmax(0, 1fr))`);
    };
    recompute();
    const observer = new ResizeObserver(recompute);
    observer.observe(el);
    return () => observer.disconnect();
  }, []);
  return (
    <>
      <HostTreeGroupDeleteDialog
        managedGroupPaths={managedGroupPaths}
        onConfirmDelete={deleteGroupPath}
      />
      <VaultViewLayout ctx={{ Activity, allGroupPaths, allTags, AppLogo, Array, Badge, BookMarked, Boolean, Button, CheckSquare, ChevronDown, cancelInlineGroupEdit, clearHostSelection, ClipboardCopy, Clock, cn, commitInlineGroupRename, connectionLogs, connectSelectedHosts, ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, Copy, currentSection, customGroups, deleteGroupPath, deleteGroupWithHosts, deleteSelectedHosts, deleteTargetPath, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, displayedGroups, displayedHosts, DistroAvatar, Download, Dropdown, DropdownContent, DropdownTrigger, Edit2, editingGroupPath, editingHost, editingHostGroupDefaults, FileCode, FileSymlink, FolderPlus, FolderTree, getDropTargetClasses, getEffectiveHostDistro, Globe, groupConfigs, GroupDetailsPanel, groupedDisplayHosts, handleConnectClick, handleCopyCredentials, handleDeleteTag, handleDuplicateHost, handleEditGroupConfig, handleEditHost, handleEditTag, handleExportHosts, handleHostConnect, handleImportFileSelected, handleNewHost, handleProtocolSelect, handleQuickConnect, handleQuickConnectSaveHost, handleSaveGroupConfig, handleSearchKeyDown, handleUnmanageGroup, hasHostsSidePanel, HostDetailsPanel, hostListScrollRef, hosts, HostTreeView, hotkeyScheme, identities, ImportVaultDialog, Input, isDeleteGroupOpen, isGroupPanelOpen, isHostPanelOpen, isHostsSectionActive, isImportOpen, isMultiSelectMode, isNewFolderOpen, isQuickConnectOpen, isRenameGroupOpen, isSearchQuickConnect, isSerialModalOpen, Key, keyBindings, KeychainManager, keys, knownHostsManagerElement, Label, lastPinnedId, LayoutGrid, LazyConnectionLogsManager, LazyProtocolSelectDialog, List, managedGroupPaths, managedSources, moveGroup, moveHostToGroup, Network, newFolderName, newHostGroupPath, onClearUnsavedConnectionLogs, onConnectSerial, onCreateLocalTerminal, onDeleteConnectionLog, onDeleteHost, onImportOrReuseKey, onOpenLogView, onOpenSettings, onRunSnippet, onToggleConnectionLogSaved, onUpdateCustomGroups, onUpdateGroupConfigs, onUpdateHosts, onUpdateIdentities, onUpdateKeys, onUpdateProxyProfiles, onUpdateSnippetPackages, onUpdateSnippets, Pin, pinnedHosts, pinnedRecentIds, Plug, Plus, PortForwarding, protocolSelectHost, proxyProfiles, ProxyProfilesManager, quickConnectTarget, quickConnectWarnings, QuickConnectWizard, recentHosts, renameGroupError, renameGroupName, renameTargetPath, reorderGroup, reorderHost, RippleButton, rootRef, sanitizeHost, search, Search, selectedGroupPath, selectedHostIds, selectedTags, SerialConnectModal, SerialHostDetailsPanel, sessionCount, Set, setCurrentSection, setDeleteGroupWithHosts, setDeleteTargetPath, setDragOverDropTarget, setEditingGroupPath, setEditingHost, setGroupDragOverDropTarget, setIsDeleteGroupOpen, setIsGroupPanelOpen, setIsHostPanelOpen, setIsImportOpen, setIsMultiSelectMode, setIsNewFolderOpen, setIsQuickConnectOpen, setIsRenameGroupOpen, setIsSerialModalOpen, setLastPinnedId, setNewFolderName, setNewHostGroupPath, setProtocolSelectHost, setQuickConnectTarget, setQuickConnectWarnings, setRenameGroupError, setRenameGroupName, setRenameTargetPath, setSearch, setSelectedGroupPath, setSelectedHostIds, setSelectedTags, setSidebarCollapsed, setSidebarWidth, handleSidebarWidthCommit, setSortMode, setTargetParentPath, Settings, setViewMode, shellHistory, shouldHideEmptyRootHostsSection, showRecentHosts, sidebarCollapsed, sidebarWidth, snippetPackages, snippets, SnippetsManager, SortDropdown, sortMode, splitViewGridStyle, Square, Star, startInlineDeleteGroup, startInlineNewGroup, startInlineRenameGroup, submitNewFolder, submitRenameGroup, Suspense, t, TagFilterDropdown, targetParentPath, terminalFontSize, terminalSettings, TerminalSquare, terminalThemeId, toggleHostPinned, toggleHostSelection, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Trash2, treeExpandedState, treeViewGroupTree, treeViewHosts, Upload, upsertHostById, Usb, viewMode, visibleDisplayedHosts, X, Zap }} />
    </>
  );
};

// Only re-render when data props change - isActive is now managed internally via store subscription
export const vaultViewAreEqual = (
  prev: VaultViewProps,
  next: VaultViewProps,
): boolean => {
  const isEqual =
    prev.hosts === next.hosts &&
    prev.keys === next.keys &&
    prev.identities === next.identities &&
    prev.proxyProfiles === next.proxyProfiles &&
    prev.snippets === next.snippets &&
    prev.snippetPackages === next.snippetPackages &&
    prev.customGroups === next.customGroups &&
    prev.knownHosts === next.knownHosts &&
    prev.shellHistory === next.shellHistory &&
    prev.connectionLogs === next.connectionLogs &&
    prev.sessionCount === next.sessionCount &&
    prev.managedSources === next.managedSources &&
    prev.groupConfigs === next.groupConfigs &&
    prev.terminalThemeId === next.terminalThemeId &&
    prev.terminalFontSize === next.terminalFontSize &&
    prev.navigateToSection === next.navigateToSection &&
    // Only the keepalive fields of terminalSettings are forwarded to
    // PortForwarding inside the vault, so compare them directly. Other
    // terminal settings (fonts, themes, etc.) don't affect this subtree
    // and we don't want to re-render for them.
    prev.terminalSettings?.keepaliveInterval === next.terminalSettings?.keepaliveInterval &&
    prev.terminalSettings?.keepaliveCountMax === next.terminalSettings?.keepaliveCountMax;

  return isEqual;
};

const MemoizedVaultViewInner = memo(VaultViewInner, vaultViewAreEqual);

// Just export the memoized component directly
// Visibility control is handled by parent (App.tsx)
export const VaultView = MemoizedVaultViewInner;
VaultView.displayName = "VaultView";
