﻿import SidebarConnectionRail from './sidebar/SidebarConnectionRail';
import Modal from './common/ResizableDraggableModal';
import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel';
import SlowQueryRailButton from './sidebar/SlowQueryRailButton';
import SqlAuditRailButton from './sidebar/SqlAuditRailButton';
import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu';
import {
  getMetadataDialect,
  loadSchemas,
  shouldHideSchemaPrefix,
  splitQualifiedName,
} from './sidebar/sidebarMetadataLoaders';
import {
  useSidebarBatchExport,
} from './sidebar/useSidebarBatchExport';
import { SidebarEntityModals } from './sidebar/SidebarEntityModals';
import { SavedQueryGroupModal } from './sidebar/SavedQueryGroupModal';
import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle';
import {
  useSidebarV2ContextMenu,
} from './sidebar/useSidebarV2ContextMenu';
import {
  useSidebarObjectActions,
  type SidebarMessagePublishTarget,
} from './sidebar/useSidebarObjectActions';
import { useSidebarSearchModel } from './sidebar/useSidebarSearchModel';
import { useSidebarFilterPersistence } from './sidebar/useSidebarFilterPersistence';
import { useSidebarV2ActionHandlers } from './sidebar/useSidebarV2ActionHandlers';
import { useSidebarCommandSearchRunner } from './sidebar/useSidebarCommandSearchRunner';
import { useSidebarTitleRender } from './sidebar/useSidebarTitleRender';
import {
  normalizeDriverType,
  useSidebarTreeLoaders,
} from './sidebar/useSidebarTreeLoaders';
export { formatSidebarDriverAgentUpdateWarning } from './sidebar/useSidebarTreeLoaders';
import {
  ExternalSQLFileModal,
  useSidebarExternalSqlWorkflow,
} from './sidebar/SidebarExternalSqlWorkflow';
export {
  buildSQLFileExecutionFooter,
  SQLFileExecutionProgressContent,
} from './sidebar/SidebarExternalSqlWorkflow';
export type {
  SQLFileExecutionProgressState,
  SQLFileExecutionStatus,
} from './sidebar/SidebarExternalSqlWorkflow';
import {
  V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
  formatSidebarRowCount,
  hasSidebarLazyChildren,
  shouldClearSidebarActiveContextOnEmptySelect,
  shouldLoadSidebarNodeOnExpand,
  getV2RailConnectionGroupBadgeText,
  type V2ExplorerFilter,
} from './sidebar/sidebarHelpers';
// 重新导出，保持外部测试文件的 `from './Sidebar'` 兼容
export {
  V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
  formatSidebarRowCount,
  hasSidebarLazyChildren,
  shouldClearSidebarActiveContextOnEmptySelect,
  shouldLoadSidebarNodeOnExpand,
  getV2RailConnectionGroupBadgeText,
  isV2SidebarObjectNode,
  resolveV2ObjectGroupTitle,
  resolveSidebarTableNameForCopy,
  resolveSidebarDatabaseNameForCopy,
  parseV2CommandSearchQuery,
} from './sidebar/sidebarHelpers';
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
import { createPortal } from 'react-dom';
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Radio, Select, Tooltip } from 'antd';
import { APP_POPUP_Z_INDEX } from '../utils/overlayZIndex';
	import {
	  CaretDownFilled,
	  DatabaseOutlined,
	  TableOutlined,
	  ConsoleSqlOutlined,
  HddOutlined,
  FolderOutlined,
  FolderOpenOutlined,
  FileTextOutlined,
  CopyOutlined,
  ExportOutlined,
  FolderAddOutlined,
  SaveOutlined,
  EditOutlined,
  SearchOutlined,
  KeyOutlined,
  ThunderboltOutlined,
  UnorderedListOutlined,
  FunctionOutlined,
  LinkOutlined,
  FileAddOutlined,
  ImportOutlined,
  PlusOutlined,
  ReloadOutlined,
  SendOutlined,
  DeleteOutlined,
  DisconnectOutlined,
  CheckSquareOutlined,
  FilterOutlined,
  DashboardOutlined,
  WarningOutlined,
  AimOutlined,
  MoreOutlined,
  MenuFoldOutlined,
  SettingOutlined
	} from '@ant-design/icons';
import {
    buildSidebarRootConnectionToken,
    buildSidebarRootTagToken,
    useStore,
} from '../store';
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
import {
    selectRecentSidebarSqlLogs,
    selectSidebarCommandSearchSqlLogs,
} from './sidebar/sidebarSqlLogSelector';
		import { SavedConnection, SavedQuery, SavedQueryGroup, ExternalSQLDirectory, ExternalSQLTreeEntry, SchemaVisibilityRule } from '../types';
import { getDbIcon } from './DatabaseIcons';
		import { ListSQLDirectory } from '../../wailsjs/go/app/App';
import { supportsTableTruncateAction } from './tableDataDangerActions';
  import { EventsOn } from '../../wailsjs/runtime/runtime';
  import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import { useWorkbenchTabs } from '../hooks/useWorkbenchTabs';
import FindInDatabaseModal from './FindInDatabaseModal';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { resolveDataSourceType } from '../utils/dataSourceCapabilities';
import { isConnectionStructureEditRestricted } from '../utils/connectionReadOnly';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import {
  resolveSidebarRuntimeDatabase,
} from '../utils/sidebarMetadata';
import {
    findSidebarNodePathByKey,
    findSidebarNodePathForLocate,
    normalizeSidebarLocateObjectRequest,
    normalizeSidebarLocateObjectRequestFromTab,
    resolveSidebarLocateTarget,
    type SidebarLocateTreeNodeLike,
} from '../utils/sidebarLocate';
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual';
import {
  getSavedQueryGroupIdFromToken,
  getSavedQueryGroupOwnerIds,
  getSavedQueryIdFromGroupToken,
  isSavedQueryGroupQueryToken,
  isSavedQueryGroupToken,
  normalizeSavedQueryGroups,
  resolveSavedQueryGroupChildOrder,
} from '../utils/savedQueryGroups';
import {
  getSchemaVisibilityRule,
  moveSchemaVisibilityRule,
  updateSchemaVisibilityRule,
} from '../utils/schemaVisibility';
import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation';
import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions';
import {
  DATA_IMPORT_WORKBENCH_TAB_ID,
  resolveDataImportWorkbenchLaunchTab,
} from '../utils/dataImportTab';
import { useExportProgressDialog } from './ExportProgressModal';
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
import { resolveSidebarTableMetadataFields } from '../utils/sidebarTableMetadata';
import { filterSidebarTreeByHiddenObjectGroups } from '../utils/sidebarObjectVisibility';
import { t } from '../i18n';
import MessagePublishModal from './MessagePublishModal';
import {
  SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
  SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
  resolveSidebarContextMenuPosition,
  type SearchScope,
} from './sidebarCoreUtils';
export { resolveSidebarContextMenuPosition } from './sidebarCoreUtils';
export type { ExternalSQLFileModalMode, SearchScope } from './sidebarCoreUtils';
import {
  buildSidebarTableChildrenForUi,
  buildSidebarConnectionTagTree,
  buildV2RailConnectionGroups,
  buildV2SidebarTableSectionedChildren,
  collectSidebarSubtreeKeys,
  estimateV2TreeHorizontalScrollWidth,
  filterV2CommandSearchTreeItems,
  filterV2ExplorerTreeByKind,
  isSidebarTablePinned,
  isConnectionTagDescendant,
  normalizeSidebarTreeRelativeDropPosition,
  resolveSidebarConnectionIdFromKey,
  resolveSidebarDropInsertBefore,
  resolveSidebarDropNodeFromDomEvent,
  resolveSidebarDropTargetMetricsFromDomEvent,
  resolveSidebarDatabaseTreePruneKeys,
  resolveSidebarNodeConnectionId,
  resolveV2ActiveConnectionId,
  resolveV2SelectedDatabaseName,
  resolveV2CommandSearchPersistentFilter,
  shouldClearSidebarNodeChildrenOnCollapse,
  shouldSkipSidebarLoadOnExpandWhileDragging,
  shouldSkipSidebarSelectWhileDragging,
  shouldCloseV2CommandSearchOnGlobalKey,
  shouldRunV2CommandSearchEnter,
  sortSidebarTableEntries,
  type SidebarConnectionState,
  type SidebarTreeNode as TreeNode,
  type V2CommandSearchItem,
} from './sidebarV2Utils';

export {
  buildSidebarTableChildrenForUi,
  buildSidebarConnectionTagTree,
  buildV2RailConnectionGroups,
  buildV2SidebarTableSectionedChildren,
  collectSidebarSubtreeKeys,
  estimateV2TreeHorizontalScrollWidth,
  filterV2CommandSearchTreeItems,
  filterV2ExplorerTreeByKind,
  isSidebarTablePinned,
  isConnectionTagDescendant,
  normalizeSidebarTreeRelativeDropPosition,
  resolveSidebarConnectionIdFromKey,
  resolveSidebarDropInsertBefore,
  resolveSidebarDropNodeFromDomEvent,
  resolveSidebarDropTargetMetricsFromDomEvent,
  resolveSidebarDatabaseTreePruneKeys,
  resolveSidebarNodeConnectionId,
  resolveV2ActiveConnectionId,
  resolveV2CommandSearchPersistentFilter,
  shouldClearSidebarNodeChildrenOnCollapse,
  shouldSkipSidebarLoadOnExpandWhileDragging,
  shouldSkipSidebarSelectWhileDragging,
  shouldCloseV2CommandSearchOnGlobalKey,
  shouldRunV2CommandSearchEnter,
  sortSidebarTableEntries,
};
export { resolveSidebarTagDropInsertBefore } from './sidebarV2Utils';
export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils';

type SidebarTreeSwitcherNodeLike = {
  key?: React.Key;
  data?: TreeNode;
  isLeaf?: boolean;
  loading?: boolean;
};

export const resolveSidebarSwitcherLoadKey = (node: SidebarTreeSwitcherNodeLike | null | undefined): string | null => {
  const treeNode = node?.data;
  const dataRef = treeNode?.dataRef;
  if (!treeNode) {
    return null;
  }

  if (treeNode.type === 'connection') {
    const connectionId = String(dataRef?.id || treeNode.key || node?.key || '').trim();
    return connectionId ? `dbs-${connectionId}` : null;
  }

  if (treeNode.type === 'database') {
    const connectionId = String(dataRef?.id || '').trim();
    const dbName = String(dataRef?.dbName || '').trim();
    return connectionId && dbName ? `tables-${connectionId}-${dbName}` : null;
  }

  if (treeNode.type === 'jvm-mode' || treeNode.type === 'jvm-resource') {
    const connectionId = String(dataRef?.id || '').trim();
    const providerMode = String(dataRef?.providerMode || '').trim().toLowerCase();
    const parentPath = treeNode.type === 'jvm-resource' ? String(dataRef?.resourcePath || '').trim() : '';
    return connectionId && providerMode ? `jvm-resources-${connectionId}-${providerMode}-${parentPath}` : null;
  }

  return null;
};

export const shouldKeepSidebarSwitcherCollapsedWhileLoading = (
  node: SidebarTreeSwitcherNodeLike | null | undefined,
  loadingKeys: ReadonlySet<string>,
): boolean => {
  if (!node || node.isLeaf) {
    return false;
  }
  if (node.loading) {
    return true;
  }
  const loadKey = resolveSidebarSwitcherLoadKey(node);
  return !!loadKey && loadingKeys.has(loadKey);
};

const { Search } = Input;
const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50;
const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
const SIDEBAR_CACHED_DATABASE_TREE_LIMIT = 12;

// resolveV2ObjectGroupTitle 已迁移到 ./sidebar/sidebarHelpers

// shouldLoadSidebarNodeOnExpand 已迁移到 ./sidebar/sidebarHelpers

// resolveSidebarTableNameForCopy 已迁移到 ./sidebar/sidebarHelpers

const buildConnectionRootQueryTabTitle = () => t('query.new');

const buildConnectionRootRedisCommandTabTitle = (redisDbLabel = 'db0') =>
  t('sidebar.tab.redis_command', { database: redisDbLabel });

const buildConnectionRootRedisMonitorTabTitle = (redisDbLabel = 'db0') =>
  t('sidebar.tab.redis_monitor', { database: redisDbLabel });

const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: string }> = [
  { key: 'all', labelKey: 'sidebar.command_search.object_kind.all' },
  { key: 'tables', labelKey: 'sidebar.command_search.object_kind.tables' },
  { key: 'views', labelKey: 'sidebar.command_search.object_kind.views' },
  { key: 'sequences', labelKey: 'sidebar.command_search.object_kind.sequences' },
  { key: 'routines', labelKey: 'sidebar.command_search.object_kind.routines' },
  { key: 'packages', labelKey: 'sidebar.command_search.object_kind.packages' },
  { key: 'events', labelKey: 'sidebar.command_search.object_kind.events' },
];

const buildConnectionReloadSignature = (conn?: SavedConnection | null): string => {
  if (!conn) return '';
  return JSON.stringify({
    config: conn.config || {},
    includeDatabases: conn.includeDatabases || [],
    includeRedisDatabases: conn.includeRedisDatabases || [],
    schemaVisibilityByDatabase: conn.schemaVisibilityByDatabase || {},
  });
};

const isConnectionTreeKey = (key: React.Key, connectionId: string): boolean => {
  const text = String(key);
  return text === connectionId || text.startsWith(`${connectionId}-`);
};

const isPostgresSchemaDialect = (dialect: string): boolean => (
  ['postgres', 'kingbase', 'highgo', 'vastbase', 'opengauss'].includes(normalizeDriverType(dialect))
);

const isSavedQueryUnmatchedForConnectionIds = (query: SavedQuery, connectionIds: Set<string>): boolean => (
  query.bindingStatus === 'orphan' || !connectionIds.has(query.connectionId)
);

export const buildAllSavedQueriesTreeNode = (
  savedQueries: SavedQuery[],
  connections: SavedConnection[],
  savedQueryGroups: SavedQueryGroup[] = [],
): TreeNode | null => {
  const normalizedGroups = normalizeSavedQueryGroups(
    savedQueryGroups,
    savedQueries.map((query) => query.id),
  );
  if (savedQueries.length === 0 && normalizedGroups.length === 0) {
      return null;
  }

  const createQueryNode = (query: SavedQuery): TreeNode => ({
      title: query.name || t('sidebar.tree.untitled_query'),
      key: `all-saved-query-${query.id}`,
      icon: <FileTextOutlined />,
      type: 'saved-query',
      dataRef: query,
      isLeaf: true,
  });
  const buildDatabaseGroups = (queries: SavedQuery[], keyPrefix: string): TreeNode[] => {
      const groupedByDatabase = new Map<string, SavedQuery[]>();
      queries.forEach((query) => {
          const dbName = String(query.dbName || '').trim() || t('sidebar.tree.default_database');
          groupedByDatabase.set(dbName, [...(groupedByDatabase.get(dbName) || []), query]);
      });
      return Array.from(groupedByDatabase.entries()).map(([dbName, items]) => ({
          title: dbName,
          key: `${keyPrefix}-db-${encodeURIComponent(dbName)}`,
          icon: <DatabaseOutlined />,
          type: 'saved-query-group',
          selectable: false,
          isLeaf: false,
          children: items.map(createQueryNode),
      }));
  };

  const buildAutomaticChildren = (queries: SavedQuery[]): TreeNode[] => {
      const connectionIds = new Set(connections.map((conn) => conn.id));
      const unmatchedSavedQueries = queries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIds));
      const unmatchedIds = new Set(unmatchedSavedQueries.map((query) => query.id));
      const groupedByConnection = new Map<string, SavedQuery[]>();
      queries.forEach((query) => {
          if (unmatchedIds.has(query.id)) return;
          groupedByConnection.set(query.connectionId, [
              ...(groupedByConnection.get(query.connectionId) || []),
              query,
          ]);
      });

      const automaticChildren: TreeNode[] = [];
      connections.forEach((conn) => {
          const connectionQueries = groupedByConnection.get(conn.id);
          if (!connectionQueries || connectionQueries.length === 0) return;
          const iconType = resolveConnectionIconType(conn);
          const iconColor = resolveConnectionAccentColor(conn);
          automaticChildren.push({
              title: conn.name || conn.id,
              key: `all-saved-queries-connection-${conn.id}`,
              icon: getDbIcon(iconType, iconColor, 22),
              type: 'saved-query-group',
              selectable: false,
              isLeaf: false,
              children: buildDatabaseGroups(connectionQueries, `all-saved-queries-connection-${conn.id}`),
          });
      });

      if (unmatchedSavedQueries.length > 0) {
          const groupedByOriginalConnection = new Map<string, SavedQuery[]>();
          unmatchedSavedQueries.forEach((query) => {
              const originalConnectionId = String(query.originalConnectionId || query.connectionId || t('sidebar.tree.unknown_connection')).trim() || t('sidebar.tree.unknown_connection');
              groupedByOriginalConnection.set(originalConnectionId, [
                  ...(groupedByOriginalConnection.get(originalConnectionId) || []),
                  query,
              ]);
          });
          automaticChildren.push({
              title: t('sidebar.tree.unmatched_saved_queries'),
              key: 'all-saved-queries-unmatched',
              icon: <WarningOutlined />,
              type: 'saved-query-group',
              selectable: false,
              isLeaf: false,
              children: Array.from(groupedByOriginalConnection.entries()).map(([connectionLabel, items]) => ({
                  title: connectionLabel,
                  key: `all-saved-queries-unmatched-${encodeURIComponent(connectionLabel)}`,
                  icon: <FolderOpenOutlined />,
                  type: 'saved-query-group',
                  selectable: false,
                  isLeaf: false,
                  children: buildDatabaseGroups(items, `all-saved-queries-unmatched-${encodeURIComponent(connectionLabel)}`),
              })),
          });
      }
      return automaticChildren;
  };

  const queryById = new Map(savedQueries.map((query) => [query.id, query]));
  const groupById = new Map(normalizedGroups.map((group) => [group.id, group]));
  const groupOwners = getSavedQueryGroupOwnerIds(normalizedGroups);
  const buildManualGroupNode = (group: SavedQueryGroup, ancestors = new Set<string>()): TreeNode => {
      const nextAncestors = new Set(ancestors);
      nextAncestors.add(group.id);
      const children = resolveSavedQueryGroupChildOrder(group.id, normalizedGroups).flatMap((token): TreeNode[] => {
          if (isSavedQueryGroupQueryToken(token)) {
              const query = queryById.get(getSavedQueryIdFromGroupToken(token));
              return query ? [createQueryNode(query)] : [];
          }
          if (isSavedQueryGroupToken(token)) {
              const childGroupId = getSavedQueryGroupIdFromToken(token);
              const childGroup = groupById.get(childGroupId);
              if (!childGroup || childGroup.parentGroupId !== group.id || nextAncestors.has(childGroup.id)) return [];
              return [buildManualGroupNode(childGroup, nextAncestors)];
          }
          return [];
      });
      return {
          title: group.name || t('sidebar.saved_query_group.untitled'),
          key: `saved-query-manual-group-${group.id}`,
          icon: <FolderOutlined />,
          type: 'saved-query-manual-group',
          dataRef: group,
          selectable: false,
          isLeaf: false,
          children,
      };
  };

  const automaticChildren = buildAutomaticChildren(
      savedQueries.filter((query) => !groupOwners.has(query.id)),
  );
  const children: TreeNode[] = normalizedGroups
      .filter((group) => !group.parentGroupId)
      .map((group) => buildManualGroupNode(group));

  if (normalizedGroups.length === 0) {
      children.push(...automaticChildren);
  } else if (automaticChildren.length > 0) {
      children.push({
          title: t('sidebar.tree.ungrouped_saved_queries'),
          key: 'all-saved-queries-ungrouped',
          icon: <FolderOpenOutlined />,
          type: 'saved-query-group',
          selectable: false,
          isLeaf: false,
          children: automaticChildren,
      });
  }

  return {
      title: t('sidebar.tree.all_saved_queries'),
      key: 'all-saved-queries',
      icon: <FolderOpenOutlined />,
      type: 'all-saved-queries',
      isLeaf: false,
      selectable: false,
      children,
  };
};

const Sidebar: React.FC<{
  onCreateConnection?: () => void;
  onEditConnection?: (conn: SavedConnection) => void;
  onOpenSettings?: () => void;
  onToggleAI?: () => void;
  onToggleLogPanel?: () => void;
  uiVersion?: 'legacy' | 'v2';
  onFocusCommandSearch?: () => void;
  onCollapseSidebar?: () => void;
  onExpandSidebar?: () => void;
  collapseSidebarLabel?: string;
  collapseSidebarButtonRef?: React.Ref<HTMLButtonElement>;
  expandSidebarLabel?: string;
  expandSidebarButtonRef?: React.Ref<HTMLButtonElement>;
  isTreePanelCollapsed?: boolean;
}> = React.memo(({
  onCreateConnection,
  onEditConnection,
  onOpenSettings,
  onToggleAI,
  onToggleLogPanel,
  uiVersion,
  onFocusCommandSearch,
  onCollapseSidebar,
  onExpandSidebar,
  collapseSidebarLabel,
  collapseSidebarButtonRef,
  expandSidebarLabel,
  expandSidebarButtonRef,
  isTreePanelCollapsed = false,
}) => {
  const connections = useStore(state => state.connections);
  const savedQueries = useStore(state => state.savedQueries);
  const savedQueryGroups = useStore(state => state.savedQueryGroups);
  const externalSQLDirectories = useStore(state => state.externalSQLDirectories);
  const saveQuery = useStore(state => state.saveQuery);
  const deleteQuery = useStore(state => state.deleteQuery);
  const saveSavedQueryGroup = useStore(state => state.saveSavedQueryGroup);
  const deleteSavedQueryGroup = useStore(state => state.deleteSavedQueryGroup);
  const moveSavedQueryToGroup = useStore(state => state.moveSavedQueryToGroup);
  const reloadSavedQueryGroups = useStore(state => state.reloadSavedQueryGroups);
  const saveExternalSQLDirectory = useStore(state => state.saveExternalSQLDirectory);
  const deleteExternalSQLDirectory = useStore(state => state.deleteExternalSQLDirectory);
  const updateRecentSQLFilePath = useStore(state => state.updateRecentSQLFilePath);
  const removeRecentSQLFilesByPath = useStore(state => state.removeRecentSQLFilesByPath);
  const moveRecentSQLFilesByDirectory = useStore(state => state.moveRecentSQLFilesByDirectory);
  const removeRecentSQLFilesByDirectory = useStore(state => state.removeRecentSQLFilesByDirectory);
  const addConnection = useStore(state => state.addConnection);
  const updateConnection = useStore(state => state.updateConnection);
  const addTab = useStore(state => state.addTab);
  const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft);
  const tabs = useWorkbenchTabs();
  const activeTabId = useStore(state => state.activeTabId);
  const setActiveContext = useStore(state => state.setActiveContext);
  const removeConnection = useStore(state => state.removeConnection);
  const connectionTags = useStore(state => state.connectionTags);
  const sidebarRootOrder = useStore(state => state.sidebarRootOrder);
  const addConnectionTag = useStore(state => state.addConnectionTag);
  const updateConnectionTag = useStore(state => state.updateConnectionTag);
  const removeConnectionTag = useStore(state => state.removeConnectionTag);
  const moveConnectionToTag = useStore(state => state.moveConnectionToTag);
  const moveConnectionTag = useStore(state => state.moveConnectionTag);
  const closeTabsByConnection = useStore(state => state.closeTabsByConnection);
  const closeTabsByDatabase = useStore(state => state.closeTabsByDatabase);
  const theme = useStore(state => state.theme);
  const appearance = useStore(state => state.appearance);
  const activeContext = useStore(state => state.activeContext);
  const tableAccessCount = useStore(state => state.tableAccessCount);
  const tableSortPreference = useStore(state => state.tableSortPreference);
  const pinnedSidebarTables = useStore(state => state.pinnedSidebarTables);
  const recordTableAccess = useStore(state => state.recordTableAccess);
  const setTableSortPreference = useStore(state => state.setTableSortPreference);
  const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned);
  const queryOptions = useStore(state => state.queryOptions);
  const setQueryOptions = useStore(state => state.setQueryOptions);
  const addSqlLog = useStore(state => state.addSqlLog);
  const hideSqlLogFromRecent = useStore(state => state.hideSqlLogFromRecent);
  const clearRecentSqlLogs = useStore(state => state.clearRecentSqlLogs);
  const shortcutOptions = useStore(state => state.shortcutOptions);
  const languagePreference = useStore(state => state.languagePreference);
  const setAppearance = useStore(state => state.setAppearance);
  const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
  const addAIContext = useStore(state => state.addAIContext);
  void languagePreference;
  const darkMode = theme === 'dark';
  const resolvedAppearance = resolveAppearanceValues(appearance);
  const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
  const sidebarTableMetadataFields = useMemo(
      () => resolveSidebarTableMetadataFields(
          queryOptions?.sidebarTableMetadataFields,
          queryOptions?.showSidebarTableComment === true,
          queryOptions?.sidebarTableMetadataFieldOrder,
      ),
      [queryOptions?.showSidebarTableComment, queryOptions?.sidebarTableMetadataFieldOrder, queryOptions?.sidebarTableMetadataFields],
  );
  const { exportProgressModal, runExportWithProgress } = useExportProgressDialog();
  const disableLocalBackdropFilter = isMacLikePlatform();
  const autoFetchVisible = useAutoFetchVisibility();
  const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform());
  const focusSidebarSearchShortcut = resolveShortcutDisplay(shortcutOptions, 'focusSidebarSearch', activeShortcutPlatform);
  const focusSidebarSearchShortcutTokens = focusSidebarSearchShortcut === '-'
      ? []
      : focusSidebarSearchShortcut.match(/Ctrl|Alt|Shift|Esc|Space|[⌘⌃⌥⇧↵↑↓←→]|[^+]/g) ?? [];
  const isV2Ui = (uiVersion ?? appearance.uiVersion) === 'v2';
  const [treeData, setTreeData] = useState<TreeNode[]>([]);
  const activeTab = useMemo(() => tabs.find(tab => tab.id === activeTabId) || null, [tabs, activeTabId]);
  const activeTabLocateRequest = useMemo(() => normalizeSidebarLocateObjectRequestFromTab(activeTab), [activeTab]);
  const canLocateActiveTab = !!activeTabLocateRequest;

  // Background Helper (Duplicate logic for now, ideally shared)
  const getBg = (darkHex: string) => {
      if (!darkMode) return `rgba(255, 255, 255, ${opacity})`;
      const hex = darkHex.replace('#', '');
      const r = parseInt(hex.substring(0, 2), 16);
      const g = parseInt(hex.substring(2, 4), 16);
      const b = parseInt(hex.substring(4, 6), 16);
      return `rgba(${r}, ${g}, ${b}, ${opacity})`;
  };
  const bgMain = getBg('#141414');
  const overlayTheme = useMemo(
      () => buildOverlayWorkbenchTheme(darkMode, {
          disableBackdropFilter: disableLocalBackdropFilter,
          uiVersion: isV2Ui ? 'v2' : 'legacy',
      }),
      [darkMode, disableLocalBackdropFilter, isV2Ui],
  );
  const modalPanelStyle = useMemo(() => ({
      background: overlayTheme.shellBg,
      border: overlayTheme.shellBorder,
      boxShadow: overlayTheme.shellShadow,
      backdropFilter: overlayTheme.shellBackdropFilter,
  }), [overlayTheme]);
  const modalSectionStyle = useMemo(() => ({
      padding: 14,
      borderRadius: 14,
      border: overlayTheme.sectionBorder,
      background: overlayTheme.sectionBg,
  }), [overlayTheme]);
  const modalScrollSectionStyle = useMemo(() => ({
      maxHeight: 400,
      overflow: 'auto' as const,
      border: overlayTheme.sectionBorder,
      borderRadius: 14,
      padding: 12,
      background: overlayTheme.sectionBg,
  }), [overlayTheme]);
  const modalHintTextStyle = useMemo(() => ({
      color: overlayTheme.mutedText,
      fontSize: 12,
      lineHeight: 1.6,
  }), [overlayTheme]);
  const renderSidebarModalTitle = (icon: React.ReactNode, title: string, description: string) => (
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
          <div style={{ width: 34, height: 34, borderRadius: 12, display: 'grid', placeItems: 'center', background: overlayTheme.iconBg, color: overlayTheme.iconColor, flexShrink: 0 }}>
              {icon}
          </div>
          <div style={{ minWidth: 0 }}>
              <div style={{ fontSize: 16, fontWeight: 700, color: overlayTheme.titleText }}>{title}</div>
              <div style={{ marginTop: 4, color: overlayTheme.mutedText, fontSize: 12, lineHeight: 1.6 }}>{description}</div>
          </div>
      </div>
  );
  const v2SidebarSearchMode = appearance.v2SidebarSearchMode ?? 'command';
  const v2UseLegacySidebarFilter = isV2Ui && v2SidebarSearchMode === 'filter';
  const v2CommandSearchPersistentFilterEnabled = appearance.v2CommandSearchPersistentFilterEnabled === true;
  const v2PersistedSidebarFilter = appearance.v2SidebarPersistedFilter ?? '';
  const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data';
  const [searchValue, setSearchValue] = useState(v2PersistedSidebarFilter);
  const deferredSearchValue = useDeferredValue(searchValue);
  const [searchScopes, setSearchScopes] = useState<SearchScope[]>(['smart']);
  const [v2ExplorerFilter, setV2ExplorerFilter] = useState<V2ExplorerFilter>('all');
  const [isSearchScopePopoverOpen, setIsSearchScopePopoverOpen] = useState(false);
  const searchInputRef = useRef<any>(null);
  const commandSearchInputRef = useRef<any>(null);
  const [isV2CommandSearchOpen, setIsV2CommandSearchOpen] = useState(false);
  const commandSearchSqlLogs = useStore(
      state => selectSidebarCommandSearchSqlLogs(state, isV2Ui && isV2CommandSearchOpen),
  );
  const recentSqlLogs = useMemo(
      () => selectRecentSidebarSqlLogs(commandSearchSqlLogs),
      [commandSearchSqlLogs],
  );
  const [v2CommandSearchValue, setV2CommandSearchValue] = useState('');
  const deferredV2CommandSearchValue = useDeferredValue(v2CommandSearchValue);
  const [v2CommandActiveIndex, setV2CommandActiveIndex] = useState(0);
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
  const [autoExpandParent, setAutoExpandParent] = useState(true);
  const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
  const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
  const selectedNodesRef = useRef<any[]>([]);
  const loadingNodesRef = useRef<Set<string>>(new Set());
  const databaseTreeTouchedAtRef = useRef<Record<string, number>>({});
  const pruneLoadedDatabaseTreesRef = useRef<() => void>(() => {});
  const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const treeDragSelectSuppressUntilRef = useRef(0);
  const treeDragSelectionSnapshotRef = useRef<{
      selectedKeys: React.Key[];
      selectedNodes: any[];
      activeContext: { connectionId: string; dbName: string } | null;
  }>({
      selectedKeys: [],
      selectedNodes: [],
      activeContext: null,
  });
  const connectionReloadSignaturesRef = useRef<Record<string, string>>({});
  const connectionIds = useMemo(() => connections.map((conn) => conn.id), [connections]);
  const connectionIdSet = useMemo(() => new Set(connectionIds), [connectionIds]);
  const unmatchedSavedQueries = useMemo(
      () => savedQueries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIdSet)),
      [connectionIdSet, savedQueries],
  );
  const allSavedQueriesNode = useMemo<TreeNode | null>(() => {
      return buildAllSavedQueriesTreeNode(savedQueries, connections, savedQueryGroups);
  }, [connections, savedQueries, savedQueryGroups]);
  const sidebarHiddenObjectGroups = appearance.sidebarHiddenObjectGroups;
  const visibleSidebarTreeData = useMemo(
      () => filterSidebarTreeByHiddenObjectGroups(treeData, sidebarHiddenObjectGroups),
      [sidebarHiddenObjectGroups, treeData],
  );
  const sidebarObjectVisibilitySignature = sidebarHiddenObjectGroups.join('|') || 'all';
  const snapshotTreeSelectionBeforeDrag = useCallback(() => {
      treeDragSelectionSnapshotRef.current = {
          selectedKeys: [...selectedKeys],
          selectedNodes: [...selectedNodesRef.current],
          activeContext: activeContext ? { ...activeContext } : null,
      };
  }, [activeContext, selectedKeys]);

  const restoreTreeSelectionAfterDrag = useCallback(() => {
      const snapshot = treeDragSelectionSnapshotRef.current;
      treeDragSelectSuppressUntilRef.current = Date.now() + 1000;
      setSelectedKeys(snapshot.selectedKeys);
      selectedNodesRef.current = snapshot.selectedNodes;
      setActiveContext(snapshot.activeContext);
  }, [setActiveContext]);

  const openV2CommandSearch = useCallback(() => {
      pruneLoadedDatabaseTreesRef.current();
      setIsV2CommandSearchOpen(true);
      setV2CommandActiveIndex(0);
  }, []);

  const commitV2CommandSearchPersistentFilter = useCallback((value = v2CommandSearchValue) => {
      if (!v2CommandSearchPersistentFilterEnabled) {
          return;
      }
      const nextFilter = value.trim();
      setSearchValue(nextFilter);
      if (nextFilter !== v2PersistedSidebarFilter) {
          setAppearance({ v2SidebarPersistedFilter: nextFilter });
      }
  }, [setAppearance, v2CommandSearchPersistentFilterEnabled, v2CommandSearchValue, v2PersistedSidebarFilter]);

  const closeV2CommandSearch = useCallback(() => {
      commitV2CommandSearchPersistentFilter();
      setIsV2CommandSearchOpen(false);
      setV2CommandSearchValue('');
      setV2CommandActiveIndex(0);
  }, [commitV2CommandSearchPersistentFilter]);

  useEffect(() => {
      setSearchValue(v2PersistedSidebarFilter);
  }, [v2PersistedSidebarFilter]);

  const persistV2SidebarFilter = useCallback((nextFilter: string) => {
      setAppearance({ v2SidebarPersistedFilter: nextFilter });
  }, [setAppearance]);

  useSidebarFilterPersistence({
      enabled: v2UseLegacySidebarFilter,
      searchValue,
      persistedFilter: v2PersistedSidebarFilter,
      onPersist: persistV2SidebarFilter,
  });

  const handleV2CommandSearchValueChange = useCallback((value: string) => {
      setV2CommandSearchValue(value);
  }, []);

  useEffect(() => {
      if (!v2CommandSearchPersistentFilterEnabled) {
          return;
      }
      if (!isV2CommandSearchOpen) {
          return;
      }
      const nextFilter = resolveV2CommandSearchPersistentFilter({
          commandSearchValue: deferredV2CommandSearchValue,
          persistedFilter: v2PersistedSidebarFilter,
          enabled: v2CommandSearchPersistentFilterEnabled,
          isOpen: isV2CommandSearchOpen,
      });
      setSearchValue(nextFilter);
      const timer = window.setTimeout(() => {
          setAppearance({ v2SidebarPersistedFilter: nextFilter });
      }, 160);
      return () => window.clearTimeout(timer);
  }, [deferredV2CommandSearchValue, isV2CommandSearchOpen, setAppearance, v2CommandSearchPersistentFilterEnabled, v2PersistedSidebarFilter]);

  const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => {
      const nextFilter = enabled ? v2CommandSearchValue.trim() : '';
      setSearchValue(nextFilter);
      setAppearance({
          v2CommandSearchPersistentFilterEnabled: enabled,
          v2SidebarPersistedFilter: nextFilter,
      });
      message.success(
          enabled
              ? t('sidebar.message.sidebar_filter_sync_enabled')
              : t('sidebar.message.sidebar_filter_sync_disabled'),
      );
  }, [setAppearance, v2CommandSearchValue]);

  const resetV2SidebarFilter = useCallback(() => {
      setSearchValue('');
      setAppearance({
          v2CommandSearchPersistentFilterEnabled: false,
          v2SidebarPersistedFilter: '',
      });
      message.success(t('sidebar.message.sidebar_filter_reset'));
  }, [setAppearance]);
  
  // Virtual Scroll State
  const [treeHeight, setTreeHeight] = useState(500);
  const [treeViewportWidth, setTreeViewportWidth] = useState(0);
  const treeContainerRef = useRef<HTMLDivElement>(null);
  const treeRef = useRef<any>(null);
  const treeDataRef = useRef<TreeNode[]>([]);
  const externalSQLDirectoryTreesRef = useRef<Record<string, ExternalSQLTreeEntry[]>>({});
  const findTreeNodeByKeyRef = useRef<(nodes: TreeNode[], targetKey: React.Key) => TreeNode | null>(() => null);
  const expandConnectionFromRailRef = useRef<(connectionId: string) => void>(() => {});
  useEffect(() => {
      treeDataRef.current = treeData;
  }, [treeData]);

  useEffect(() => {
      if (!treeContainerRef.current) return;
      const resizeObserver = new ResizeObserver(entries => {
          for (let entry of entries) {
              setTreeHeight(entry.contentRect.height);
              setTreeViewportWidth(entry.contentRect.width);
          }
      });
      resizeObserver.observe(treeContainerRef.current);
      return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
      const handleFocusSidebarSearch = () => {
          if (isV2Ui && !v2UseLegacySidebarFilter) {
              openV2CommandSearch();
              return;
          }
          const inputEl = searchInputRef.current?.input as HTMLInputElement | undefined;
          if (!inputEl) {
              return;
          }
          inputEl.focus();
          inputEl.select();
      };
      window.addEventListener('gonavi:focus-sidebar-search', handleFocusSidebarSearch as EventListener);
      return () => {
          window.removeEventListener('gonavi:focus-sidebar-search', handleFocusSidebarSearch as EventListener);
      };
  }, [isV2Ui, openV2CommandSearch, v2UseLegacySidebarFilter]);

  useEffect(() => {
      if (!isV2CommandSearchOpen) return;
      const timer = window.setTimeout(() => {
          const inputEl = commandSearchInputRef.current?.input as HTMLInputElement | undefined;
          inputEl?.focus();
          inputEl?.select();
      }, 0);
      return () => window.clearTimeout(timer);
  }, [isV2CommandSearchOpen]);

  useEffect(() => {
      if (!isV2CommandSearchOpen) return;
      const handleV2CommandSearchGlobalKeyDown = (event: KeyboardEvent) => {
          if (!shouldCloseV2CommandSearchOnGlobalKey({ key: event.key, isOpen: isV2CommandSearchOpen })) {
              return;
          }
          event.preventDefault();
          event.stopPropagation();
          closeV2CommandSearch();
      };
      window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true);
      return () => window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true);
  }, [closeV2CommandSearch, isV2CommandSearchOpen]);
  
  // Connection Status State: key -> 'loading' | 'success' | 'error'
  const [connectionStates, setConnectionStates] = useState<Record<string, SidebarConnectionState>>({});
  const [isTreeDragging, setIsTreeDragging] = useState(false);

  // Create Database Modal
  const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false);
  const [createDbForm] = Form.useForm();
  const [targetConnection, setTargetConnection] = useState<any>(null);
  const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false);
  const [createSchemaForm] = Form.useForm();
  const [createSchemaTarget, setCreateSchemaTarget] = useState<any>(null);
  const [isRenameSchemaModalOpen, setIsRenameSchemaModalOpen] = useState(false);
  const [renameSchemaForm] = Form.useForm();
  const [renameSchemaTarget, setRenameSchemaTarget] = useState<any>(null);
  const [schemaVisibilityForm] = Form.useForm<{
      mode: SchemaVisibilityRule['mode'];
      schemas: string[];
  }>();
  const [schemaVisibilityTarget, setSchemaVisibilityTarget] = useState<{
      connection: SavedConnection;
      dbName: string;
      databaseNodeKey: React.Key;
      availableSchemas: string[];
  } | null>(null);
  const [isSavingSchemaVisibility, setIsSavingSchemaVisibility] = useState(false);
  const [isRenameDbModalOpen, setIsRenameDbModalOpen] = useState(false);
  const [renameDbForm] = Form.useForm();
  const [renameDbTarget, setRenameDbTarget] = useState<any>(null);
  const [isRenameTableModalOpen, setIsRenameTableModalOpen] = useState(false);
  const [renameTableForm] = Form.useForm();
  const [renameTableTarget, setRenameTableTarget] = useState<any>(null);
  const [messagePublishTarget, setMessagePublishTarget] = useState<SidebarMessagePublishTarget | null>(null);
  const [isRenameViewModalOpen, setIsRenameViewModalOpen] = useState(false);
  const [renameViewForm] = Form.useForm();
  const [renameViewTarget, setRenameViewTarget] = useState<any>(null);
  const [isRenameSavedQueryModalOpen, setIsRenameSavedQueryModalOpen] = useState(false);
  const [renameSavedQueryForm] = Form.useForm();
  const [renameSavedQueryTarget, setRenameSavedQueryTarget] = useState<SavedQuery | null>(null);
  const [isSavedQueryGroupModalOpen, setIsSavedQueryGroupModalOpen] = useState(false);
  const [savedQueryGroupTargetId, setSavedQueryGroupTargetId] = useState<string | null>(null);
  const [savedQueryGroupInitialParentId, setSavedQueryGroupInitialParentId] = useState<string | null>(null);
  // Connection Tag Modals
  const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false);
  const [createTagForm] = Form.useForm();

  const {
      handleExportDatabaseSQL,
      handleExportSchemaSQL,
      openBatchTableWorkbench,
      openBatchDatabaseWorkbench,
  } = useSidebarBatchExport({
      connections,
      selectedNodesRef,
      addTab,
  });
  // Find in Database Modal
  const [findInDbContext, setFindInDbContext] = useState<{ open: boolean; connectionId: string; dbName: string }>({ open: false, connectionId: '', dbName: '' });

  useEffect(() => {
      if (!autoFetchVisible) {
          return;
      }

      expandedKeys.forEach(key => {
          const node = findTreeNodeByKey(treeData, key);
          if (node && node.type === 'database') {
              loadTables(node);
          }
      });
  }, [autoFetchVisible, savedQueries]);

  useEffect(() => {
    const previousSignatures = connectionReloadSignaturesRef.current;
    const nextSignatures: Record<string, string> = {};
    const staleConnectionIds = new Set<string>();

    connections.forEach((conn) => {
      const signature = buildConnectionReloadSignature(conn);
      nextSignatures[conn.id] = signature;
      if (previousSignatures[conn.id] && previousSignatures[conn.id] !== signature) {
        staleConnectionIds.add(conn.id);
      }
    });
    connectionReloadSignaturesRef.current = nextSignatures;

    if (staleConnectionIds.size > 0) {
      const staleIds = Array.from(staleConnectionIds);
      setLoadedKeys((prev) =>
        prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))),
      );
      setExpandedKeys((prev) =>
        prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))),
      );
      setConnectionStates((prev) => {
        const next = { ...prev };
        staleIds.forEach((id) => {
          Object.keys(next).forEach((key) => {
            if (isConnectionTreeKey(key, id)) {
              delete next[key];
            }
          });
        });
        return next;
      });
      staleIds.forEach((id) => {
        Array.from(loadingNodesRef.current).forEach((key) => {
          if (key === `dbs-${id}` || key.startsWith(`tables-${id}-`)) {
            loadingNodesRef.current.delete(key);
          }
        });
      });
    }

    setTreeData((prev) => {
      const prevMap = new Map<string, TreeNode>();

      // We need to recursively extract connections from old tag structures
      // so if a user expands a connection that was tagged, the state remains
      const recurseCollect = (nodes: TreeNode[]) => {
          nodes.forEach((node) => {
            if (node.type === 'tag') {
               if (node.children) recurseCollect(node.children);
            } else if (node.type === 'connection') {
               prevMap.set(String(node.key), node);
            }
          });
      };
      recurseCollect(prev);

      const buildConnectionNode = (conn: SavedConnection): TreeNode => {
        const existing = prevMap.get(conn.id);
        const iconType = resolveConnectionIconType(conn);
        const iconColor = resolveConnectionAccentColor(conn);
        const preserveChildren = existing && !staleConnectionIds.has(conn.id);
        return {
          title: conn.name,
          key: conn.id,
          icon: getDbIcon(iconType, iconColor, 22),
          type: 'connection',
          dataRef: conn,
          isLeaf: false,
          children: preserveChildren ? existing.children : undefined,
        } as TreeNode;
      };

      const buildTreeNode = (item: ReturnType<typeof buildSidebarConnectionTagTree>[number]): TreeNode => {
        if (item.kind === 'connection') {
          return buildConnectionNode(item.connection);
        }
        return {
          title: item.tag.name,
          key: `tag-${item.tag.id}`,
          icon: (
            <span
              className="gn-v2-tree-folder-icon"
              data-sidebar-tree-folder-icon="true"
            >
              <FolderOutlined />
            </span>
          ),
          type: 'tag',
          dataRef: item.tag,
          isLeaf: false,
          children: item.children.map(buildTreeNode),
        } as TreeNode;
      };

      const orderedNodes = buildSidebarConnectionTagTree(
        connections,
        connectionTags,
        sidebarRootOrder,
      ).map(buildTreeNode);
      if (allSavedQueriesNode) {
        orderedNodes.push(allSavedQueriesNode);
      }
      const externalSQLRootNode = prev.find((node) => node.type === 'external-sql-root');
      return externalSQLRootNode ? [...orderedNodes, externalSQLRootNode] : orderedNodes;
    });
  }, [connections, connectionTags, sidebarRootOrder, allSavedQueriesNode]);

  const handleDuplicateConnection = async (conn: SavedConnection) => {
    if (!conn?.id) return;

    const backendApp = (window as any).go?.app?.App;
    if (typeof backendApp?.DuplicateConnection !== 'function') {
      message.error(t('connection.sidebar.duplicate.backendUnavailable'));
      return;
    }

    try {
      const duplicatedConnection = await backendApp.DuplicateConnection(conn.id);
      if (!duplicatedConnection) {
        throw new Error(t('connection.sidebar.duplicate.noResult'));
      }
      addConnection(duplicatedConnection);
      message.success(t('connection.sidebar.duplicate.success', {
        name: duplicatedConnection.name,
      }));
    } catch (error: any) {
      message.error(error?.message || t('connection.sidebar.duplicate.failureFallback'));
    }
  };
  const updateTreeData = (
    list: TreeNode[],
    key: React.Key,
    children: TreeNode[] | undefined,
    dataRef?: unknown,
  ): TreeNode[] => {
    return list.map(node => {
      if (node.key === key) {
        return {
          ...node,
          children,
          ...(dataRef === undefined ? {} : { dataRef }),
        };
      }
      if (node.children) {
        return {
          ...node,
          children: updateTreeData(node.children, key, children, dataRef),
        };
      }
      return node;
    });
  };

  const findTreeNodeByKey = (nodes: TreeNode[], targetKey: React.Key): TreeNode | null => {
    for (const node of nodes) {
      if (node.key === targetKey) {
        return node;
      }
      if (node.children) {
        const child = findTreeNodeByKey(node.children, targetKey);
        if (child) {
          return child;
        }
      }
    }
    return null;
  };

  findTreeNodeByKeyRef.current = findTreeNodeByKey;

  const replaceTreeNodeChildren = (
    key: React.Key,
    children: TreeNode[] | undefined,
    dataRef?: unknown,
  ): TreeNode[] => {
      const nextTreeData = updateTreeData(treeDataRef.current, key, children, dataRef);
      treeDataRef.current = nextTreeData;
      setTreeData(nextTreeData);
      return nextTreeData;
  };

  const clearTreeNodeChildrenByKeys = useCallback((keysToClear: string[]) => {
      const keysToClearSet = new Set(keysToClear.map((key) => String(key || '').trim()).filter(Boolean));
      if (keysToClearSet.size === 0) {
          return;
      }

      const clearChildren = (nodes: TreeNode[]): TreeNode[] => (
          nodes.map((node) => {
              const nodeKey = String(node.key || '').trim();
              if (keysToClearSet.has(nodeKey)) {
                  return { ...node, children: undefined };
              }
              if (node.children?.length) {
                  return { ...node, children: clearChildren(node.children) };
              }
              return node;
          })
      );

      setTreeData((prev) => {
          const nextTreeData = clearChildren(prev);
          treeDataRef.current = nextTreeData;
          return nextTreeData;
      });
      setLoadedKeys((prev) => prev.filter((key) => !keysToClearSet.has(String(key))));
      keysToClearSet.forEach((key) => {
          delete databaseTreeTouchedAtRef.current[key];
      });
  }, []);

  const pruneLoadedDatabaseTrees = useCallback(() => {
      const activeDatabaseKey = activeContext?.connectionId && activeContext?.dbName
          ? `${activeContext.connectionId}-${activeContext.dbName}`
          : '';
      const keysToClear = resolveSidebarDatabaseTreePruneKeys({
          treeData: treeDataRef.current,
          expandedKeys,
          selectedKeys,
          activeDatabaseKey,
          touchedAtByDatabaseKey: databaseTreeTouchedAtRef.current,
          maxLoadedDatabases: SIDEBAR_CACHED_DATABASE_TREE_LIMIT,
      });
      if (keysToClear.length === 0) {
          return;
      }
      clearTreeNodeChildrenByKeys(keysToClear);
  }, [activeContext?.connectionId, activeContext?.dbName, clearTreeNodeChildrenByKeys, expandedKeys, selectedKeys]);
  pruneLoadedDatabaseTreesRef.current = pruneLoadedDatabaseTrees;

  const mergeExpandedTreeKeys = (requiredKeys: React.Key[]) => {
      setExpandedKeys(prev => {
          const merged = [...prev];
          requiredKeys.forEach(key => {
              if (!merged.includes(key)) merged.push(key);
          });
          return merged;
      });
      setAutoExpandParent(true);
  };

  const scrollSidebarTreeToKey = (key: React.Key) => {
      const runAfterFrame = typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function'
          ? window.requestAnimationFrame.bind(window)
          : (callback: FrameRequestCallback) => window.setTimeout(() => callback(Date.now()), 0);

      runAfterFrame(() => {
          treeRef.current?.scrollTo?.({ key, align: 'auto' });
          runAfterFrame(() => {
              const selectedNode = treeContainerRef.current?.querySelector('.ant-tree-treenode-selected') as HTMLElement | null;
              selectedNode?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
          });
      });
  };

  const decorateExternalSQLTreeNode = (node: ExternalSQLTreeNode): TreeNode => {
    const icon = (() => {
      switch (node.type) {
        case 'external-sql-root':
          return <FolderOpenOutlined />;
        case 'external-sql-directory':
          return <HddOutlined />;
        case 'external-sql-folder':
          return <FolderOutlined />;
        default:
          return <FileTextOutlined />;
      }
    })();

    return {
      ...node,
      icon,
      children: node.children?.map((child) => decorateExternalSQLTreeNode(child)),
    };
  };

  const buildExternalSQLRootTreeNode = useCallback((
      directories: ExternalSQLDirectory[] = externalSQLDirectories,
      directoryTrees: Record<string, ExternalSQLTreeEntry[]> = externalSQLDirectoryTreesRef.current,
  ): TreeNode => decorateExternalSQLTreeNode(buildExternalSQLRootNode({
      directories,
      directoryTrees,
  })), [externalSQLDirectories]);

  const refreshGlobalExternalSQLRootNode = useCallback(async (
      showSuccess = false,
      directoriesOverride?: ExternalSQLDirectory[],
  ) => {
      const targetDirectories = directoriesOverride || externalSQLDirectories;
      const directoryTrees: Record<string, ExternalSQLTreeEntry[]> = {};
      await Promise.all(targetDirectories.map(async (directory) => {
          const directoryRes = await ListSQLDirectory(directory.path);
          if (!directoryRes.success) {
              message.warning({
                  key: `external-sql-${directory.id}`,
                  content: t('sidebar.message.external_sql_directory_read_failed', {
                      name: directory.name,
                      error: directoryRes.message,
                  }),
              });
              directoryTrees[directory.id] = [];
              return;
          }
          directoryTrees[directory.id] = Array.isArray(directoryRes.data)
              ? directoryRes.data as ExternalSQLTreeEntry[]
              : [];
      }));
      externalSQLDirectoryTreesRef.current = directoryTrees;
      const rootNode = buildExternalSQLRootTreeNode(targetDirectories, directoryTrees);
      setTreeData((prev) => {
          const withoutExternalRoot = prev.filter((node) => node.type !== 'external-sql-root');
          const nextTreeData = [...withoutExternalRoot, rootNode];
          treeDataRef.current = nextTreeData;
          return nextTreeData;
      });
      if (showSuccess) {
          message.success(t('sidebar.message.external_sql_directory_refreshed'));
      }
  }, [buildExternalSQLRootTreeNode, externalSQLDirectories]);

  useEffect(() => {
      void refreshGlobalExternalSQLRootNode(false);
  }, [refreshGlobalExternalSQLRootNode]);

  const {
      handleRunSQLFile,
      handleOpenSQLFileFromToolbar,
      openExternalSQLFile,
      openCreateExternalSQLFileModal,
      openRenameExternalSQLFileModal,
      openCreateExternalSQLDirectoryModal,
      openRenameExternalSQLDirectoryModal,
      handleDeleteExternalSQLFile,
      handleDeleteExternalSQLDirectory,
      handleAddExternalSQLDirectory,
      handleRemoveExternalSQLDirectory,
      handleRefreshExternalSQLDirectory,
      externalSQLFileModalProps,
  } = useSidebarExternalSqlWorkflow({
      connections,
      externalSQLDirectories,
      activeTab,
      connectionIds,
      selectedNodesRef,
      addTab,
      saveExternalSQLDirectory,
      deleteExternalSQLDirectory,
      updateRecentSQLFilePath,
      removeRecentSQLFilesByPath,
      moveRecentSQLFilesByDirectory,
      removeRecentSQLFilesByDirectory,
      refreshGlobalExternalSQLRootNode,
      setExpandedKeys,
      setAutoExpandParent,
      getActiveContext: () => useStore.getState().activeContext,
  });

  useEffect(() => {
    const handleWorkbenchAddExternalSQLDirectory = () => {
      void handleAddExternalSQLDirectory({ type: 'external-sql-root' });
    };
    window.addEventListener('gonavi:add-external-sql-directory', handleWorkbenchAddExternalSQLDirectory);
    return () => {
      window.removeEventListener('gonavi:add-external-sql-directory', handleWorkbenchAddExternalSQLDirectory);
    };
  }, [handleAddExternalSQLDirectory]);

  const getNodeDatabaseContext = (node: any): { connectionId: string; dbName: string; dbNodeKey: string } | null => {
    if (!node) return null;
    if (node.type === 'database') {
      return {
        connectionId: String(node?.dataRef?.id || '').trim(),
        dbName: String(node?.dataRef?.dbName || '').trim(),
        dbNodeKey: String(node.key || '').trim(),
      };
    }

    if (
      node.type === 'external-sql-root'
      || node.type === 'external-sql-directory'
      || node.type === 'external-sql-folder'
      || node.type === 'external-sql-file'
    ) {
      return {
        connectionId: String(node?.dataRef?.connectionId || '').trim(),
        dbName: String(node?.dataRef?.dbName || '').trim(),
        dbNodeKey: String(node?.dataRef?.dbNodeKey || '').trim(),
      };
    }

    return null;
  };

  const locateObjectInSidebarRef = useRef<(detail: unknown) => Promise<void>>(async () => {});

  const waitForSidebarLoadKey = async (loadKey: string): Promise<boolean> => {
      for (let attempt = 0; attempt < SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS && loadingNodesRef.current.has(loadKey); attempt += 1) {
          await new Promise(resolve => window.setTimeout(resolve, SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS));
      }
      return !loadingNodesRef.current.has(loadKey);
  };

  const locateObjectInSidebar = async (detail: unknown) => {
      const request = normalizeSidebarLocateObjectRequest(detail);
      if (!request) {
          message.warning(t('sidebar.message.locate_current_table_unavailable'));
          return;
      }

      if (request.objectGroup === 'externalSqlFiles') {
          await refreshGlobalExternalSQLRootNode(false);
          const target = resolveSidebarLocateTarget(request, { groupBySchema: false });
          const path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
          if (!path) {
              message.warning(t('sidebar.message.locate_external_sql_file_not_found', { path: request.filePath }));
              return;
          }
          const targetKey = path[path.length - 1];
          const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey);
          setSearchValue('');
          mergeExpandedTreeKeys(path.slice(0, -1));
          setSelectedKeys([targetKey]);
          selectedNodesRef.current = targetNode ? [targetNode] : [];
          const connectionId = String(request.connectionId || activeContext?.connectionId || activeTab?.connectionId || '').trim();
          const dbName = String(request.dbName || activeContext?.dbName || activeTab?.dbName || '').trim();
          if (connectionId) {
              setActiveContext({ connectionId, dbName });
          }
          scrollSidebarTreeToKey(targetKey);
          return;
      }

      const conn = connections.find(item => item.id === request.connectionId);
      if (!conn) {
          message.warning(t('sidebar.message.locate_connection_not_found_for_object'));
          return;
      }

      const target = resolveSidebarLocateTarget(request, {
          groupBySchema: shouldHideSchemaPrefix(conn),
      });
      const objectLabel = request.objectGroup === 'materializedViews'
          ? t('sidebar.locate.object.materialized_view')
          : request.objectGroup === 'views'
              ? t('sidebar.locate.object.view')
              : request.objectGroup === 'triggers'
                  ? t('sidebar.locate.object.trigger')
                  : request.objectGroup === 'routines'
                      ? t('sidebar.locate.object.routine')
                      : t('sidebar.locate.object.table');

      let path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
      const dbLoadKey = `dbs-${request.connectionId}`;
      const tableLoadKey = `tables-${request.connectionId}-${request.dbName}`;

      if (!path && !findSidebarNodePathByKey(treeDataRef.current as SidebarLocateTreeNodeLike[], target.databaseKey)) {
          const connectionNode = findTreeNodeByKey(treeDataRef.current, target.connectionKey);
          if (!connectionNode) {
              message.warning(t('sidebar.message.locate_connection_not_in_tree'));
              return;
          }
          if (loadingNodesRef.current.has(dbLoadKey)) {
              const loaded = await waitForSidebarLoadKey(dbLoadKey);
              if (!loaded) {
                  message.info(t('sidebar.message.locate_database_loading', { database: request.dbName }));
                  return;
              }
          } else {
              await loadDatabases(connectionNode);
          }
      }

      const dbNode = findTreeNodeByKey(treeDataRef.current, target.databaseKey);
      if (!dbNode) {
          message.warning(t('sidebar.message.locate_database_not_found', { database: request.dbName }));
          return;
      }

      path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
      if (!path) {
          if (loadingNodesRef.current.has(tableLoadKey)) {
              const loaded = await waitForSidebarLoadKey(tableLoadKey);
              if (!loaded) {
                  message.info(t('sidebar.message.locate_object_loading', {
                      object: objectLabel,
                      database: request.dbName,
                  }));
                  return;
              }
          } else {
              await loadTables(dbNode);
          }
          path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
      }

      if (!path) {
          message.warning(t('sidebar.message.locate_object_not_found', {
              object: objectLabel,
              name: request.tableName,
          }));
          return;
      }

      const targetKey = path[path.length - 1];
      const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey);
      setSearchValue('');
      mergeExpandedTreeKeys(path.slice(0, -1));
      setSelectedKeys([targetKey]);
      selectedNodesRef.current = targetNode ? [targetNode] : [];
      setActiveContext({ connectionId: request.connectionId, dbName: request.dbName });
      scrollSidebarTreeToKey(targetKey);
  };

  const handleLocateActiveTabInSidebar = () => {
      if (!activeTabLocateRequest) {
          message.warning(t('sidebar.message.locate_current_table_unavailable'));
          return;
      }
      void locateObjectInSidebar(activeTabLocateRequest);
  };

  useEffect(() => {
      locateObjectInSidebarRef.current = locateObjectInSidebar;
  });

  useEffect(() => {
      const handleLocateSidebarObject = (event: Event) => {
          void locateObjectInSidebarRef.current((event as CustomEvent).detail);
      };
      window.addEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener);
      return () => {
          window.removeEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener);
      };
  }, []);

  useEffect(() => {
      const handleSidebarTablePinChanged = (event: Event) => {
          const detail = (event as CustomEvent).detail || {};
          const connectionId = String(detail.connectionId || '').trim();
          const dbName = String(detail.dbName || '').trim();
          if (!connectionId || !dbName) return;
          const dbNode = findTreeNodeByKeyRef.current(treeDataRef.current, `${connectionId}-${dbName}`);
          if (dbNode) {
              void loadTables(dbNode);
          }
      };
      window.addEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener);
      return () => {
          window.removeEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener);
      };
  }, []);

  const onLoadData = async ({ key, children, dataRef, type }: any) => {
    if (type === 'tag' || type === 'all-saved-queries' || type === 'saved-query-group' || type === 'saved-query-manual-group' || type === 'unmatched-saved-queries') return;
    if (hasSidebarLazyChildren(children)) return;

    if (type === 'connection') {
        await loadDatabases({ key, dataRef });
    } else if (type === 'jvm-mode' || type === 'jvm-resource') {
        await loadJVMResources({ key, dataRef });
    } else if (type === 'database') {
        await loadTables({ key, dataRef });
    } else if (type === 'external-sql-root') {
        await refreshGlobalExternalSQLRootNode(false);
    } else if (type === 'table') {
        // Expand table to show object categories
        const conn = dataRef; 

        const folders: TreeNode[] = [
            {
                title: t('sidebar.table_folder.columns'),
                key: `${key}-columns`,
                icon: <UnorderedListOutlined />,
                type: 'folder-columns',
                isLeaf: true,
                dataRef: conn
            },
            {
                title: t('sidebar.table_folder.indexes'),
                key: `${key}-indexes`,
                icon: <KeyOutlined style={{ transform: 'rotate(45deg)' }} />,
                type: 'folder-indexes',
                isLeaf: true,
                dataRef: conn
            },
            {
                title: t('sidebar.table_folder.foreign_keys'),
                key: `${key}-fks`,
                icon: <LinkOutlined />,
                type: 'folder-fks',
                isLeaf: true,
                dataRef: conn
            },
            {
                title: t('sidebar.table_folder.triggers'),
                key: `${key}-triggers`,
                icon: <ThunderboltOutlined />,
                type: 'folder-triggers',
                isLeaf: true,
                dataRef: conn
            }
        ];
        
        replaceTreeNodeChildren(key, folders);
    }
  };

  const isStructureOnlyDbType = (connectionId: string): boolean => {
      const conn = connections.find(c => c.id === connectionId);
      if (!conn) return false;
      const dbType = resolveDataSourceType(conn.config);
      return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis' || dbType === 'iotdb';
  };

  const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => {
      const { tableName, dbName, id } = node.dataRef;
      const conn = connections.find(c => c.id === id);
      const forceReadOnly = readOnly
          || isStructureOnlyDbType(id)
          || isConnectionStructureEditRestricted(conn?.config);
      addTab({
          id: `design-${id}-${dbName}-${tableName}`,
          title: forceReadOnly
              ? t('sidebar.tab.table_structure', { table: tableName })
              : t('sidebar.tab.design_table', { table: tableName }),
          type: 'design',
          connectionId: id,
          dbName: dbName,
          tableName: tableName,
          initialTab: initialTab,
          readOnly: forceReadOnly
      });
  };

  const openNewTableDesign = (node: any) => {
      const { dbName, id } = node.dataRef;
      const conn = connections.find(c => c.id === id);
      if (isStructureOnlyDbType(id) || isConnectionStructureEditRestricted(conn?.config)) {
          message.warning(t('sidebar.message.visual_new_table_unsupported'));
          return;
      }
      addTab({
          id: `new-table-${id}-${dbName}-${Date.now()}`,
          title: t('sidebar.tab.new_table', { database: dbName }),
          type: 'design',
          connectionId: id,
          dbName: dbName,
          tableName: '', // Empty tableName signals creation mode
          initialTab: 'columns',
          readOnly: false
      });
  };

  const openSidebarObjectNode = (node: any): boolean => {
      if (node.type === 'view' || node.type === 'materialized-view') {
          const { viewName, dbName, id, schemaName } = node.dataRef;
          addTab({
              id: node.key,
              title: viewName,
              type: 'table',
              connectionId: id,
              dbName,
              tableName: viewName,
              objectType: node.type === 'materialized-view' ? 'materialized-view' : 'view',
              schemaName,
              sidebarLocateKey: String(node.key || ''),
          });
          return true;
      }
      if (node.type === 'db-trigger') {
          const { triggerName, triggerTableName, schemaName, dbName, id } = node.dataRef;
          addTab({
              id: `trigger-${node.key}`,
              title: t('sidebar.tab.trigger', { name: triggerName }),
              type: 'trigger',
              connectionId: id,
              dbName,
              triggerName,
              triggerTableName,
              schemaName,
              sidebarLocateKey: String(node.key || ''),
          });
          return true;
      }
      if (node.type === 'db-event') {
          openEventDefinition(node);
          return true;
      }
      if (node.type === 'routine') {
          const { routineName, routineType, dbName, id } = node.dataRef;
          const typeLabel = t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function');
          addTab({
              id: `routine-def-${node.key}`,
              title: t('sidebar.tab.routine_definition', { type: typeLabel, name: routineName }),
              type: 'routine-def',
              connectionId: id,
              dbName,
              routineName,
              routineType
          });
          return true;
      }
      if (node.type === 'sequence') {
          openSequenceDefinition(node);
          return true;
      }
      if (node.type === 'package') {
          openPackageDefinition(node);
          return true;
      }
      return false;
  };

  const onSelect = (keys: React.Key[], info: any) => {
      if (isV2Ui && info?.node?.type === 'v2-table-section') {
          return;
      }
      if (Date.now() < treeDragSelectSuppressUntilRef.current) {
          return;
      }
      if (isTreeDragging) {
          return;
      }
      setSelectedKeys(keys);
      selectedNodesRef.current = info.selectedNodes || [];

      if (keys.length === 0) {
          if (shouldClearSidebarActiveContextOnEmptySelect(isV2Ui)) {
              setActiveContext(null);
          }
          return;
      }
      if (shouldSkipSidebarSelectWhileDragging(isTreeDragging, info)) return;

      const { type, dataRef, key, title } = info.node;
      const nodeConnectionId = resolveSidebarNodeConnectionId(info.node, connectionIds);

      // Update active context
      if (type === 'connection') {
          setActiveContext({ connectionId: key, dbName: '' });
      } else if (type === 'database') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
      } else if (type === 'table') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
      } else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
      } else if (type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
      } else if (type === 'saved-query') {
          setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
      } else if (type === 'redis-db') {
          setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` });
      }

      if (type === 'folder-columns') openDesign(info.node, 'columns', false);
      else if (type === 'folder-indexes') openDesign(info.node, 'indexes', false);
      else if (type === 'folder-fks') openDesign(info.node, 'foreignKeys', false);
      else if (type === 'folder-triggers') openDesign(info.node, 'triggers', false);
      else if (type === 'object-group' && dataRef?.groupKey === 'tables') {
          // 单击延迟打开表概览，双击时会取消此定时器
          if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
          const { id, dbName: gDbName, schemaName } = dataRef;
          clickTimerRef.current = setTimeout(() => {
              clickTimerRef.current = null;
              addTab({
                  id: `table-overview-${id}-${gDbName}${schemaName ? `-${schemaName}` : ''}`,
                  title: t('sidebar.tab.table_overview', {
                      database: gDbName,
                      schema: schemaName ? ` (${schemaName})` : '',
                  }),
                  type: 'table-overview' as any,
                  connectionId: id,
                  dbName: gDbName,
                  schemaName,
              } as any);
          }, 250);
      } else if (openSidebarObjectNode(info.node)) {
          return;
      }
  };

  const onExpand = (newExpandedKeys: React.Key[], info?: any) => {
    if (!info?.expanded && shouldClearSidebarNodeChildrenOnCollapse(info?.node)) {
        const collapsedKey = String(info.node?.key || '').trim();
        const keysToClear = [
            collapsedKey,
            ...collectSidebarSubtreeKeys(info.node),
        ].filter(Boolean);
        const keysToClearSet = new Set(keysToClear);
        setExpandedKeys(newExpandedKeys.filter((key) => !keysToClearSet.has(String(key))));
        setAutoExpandParent(false);
        clearTreeNodeChildrenByKeys(keysToClear);
        return;
    }
    setExpandedKeys(newExpandedKeys);
    setAutoExpandParent(false);
    if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info)) {
        void onLoadData(info.node);
    }
  };

  const onDoubleClick = (e: any, node: any) => {
      // 双击时取消单击延迟动作（如表概览打开），让双击只触发展开/折叠
      if (clickTimerRef.current) {
          clearTimeout(clickTimerRef.current);
          clickTimerRef.current = null;
      }
      const { type, dataRef, key: nodeKey } = node;
      if (isV2Ui && type === 'v2-table-section') {
          return;
      }
      const nodeConnectionId = resolveSidebarNodeConnectionId(node, connectionIds);
      if (type === 'connection') {
          setSelectedKeys([nodeKey]);
          selectedNodesRef.current = [node];
          setActiveContext({ connectionId: nodeKey, dbName: '' });
      } else if (type === 'database') {
          setSelectedKeys([nodeKey]);
          selectedNodesRef.current = [node];
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
      } else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' });
      } else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'sequence' || type === 'package' || type === 'db-trigger' || type === 'db-event' || type === 'routine') {
          setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName });
      } else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
      else if (type === 'redis-db') setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` });

      if (node.type === 'table') {
          const { tableName, dbName, id } = node.dataRef;
          // 记录表访问
          recordTableAccess(id, dbName, tableName);
          addTab({
              id: node.key,
              title: tableName,
              type: 'table',
              connectionId: id,
              dbName,
              tableName,
              initialViewMode: tableDoubleClickAction === 'open-design' ? 'fields' : undefined,
              initialViewModeRequestId: tableDoubleClickAction === 'open-design' ? String(Date.now()) : undefined,
              objectType: 'table',
          });
          return;
      } else if (node.type === 'view' || node.type === 'materialized-view') {
          const { viewName, dbName, id, schemaName } = node.dataRef;
          addTab({
              id: node.key,
              title: viewName,
              type: 'table',
              connectionId: id,
              dbName,
              tableName: viewName,
              objectType: node.type === 'materialized-view' ? 'materialized-view' : 'view',
              schemaName,
              sidebarLocateKey: String(node.key || ''),
          });
          return;
      } else if (node.type === 'saved-query') {
          const q = node.dataRef;
          addTab({
              id: q.id,
              title: resolveSavedQueryDisplayName(q.name),
              type: 'query',
              connectionId: q.connectionId,
              dbName: q.dbName,
              query: q.sql,
              savedQueryId: q.id,
          });
          return;
      } else if (node.type === 'external-sql-file') {
          void openExternalSQLFile(node);
          return;
      } else if (node.type === 'redis-db') {
          const { id, redisDB } = node.dataRef;
          addTab({
              id: `redis-keys-${id}-db${redisDB}`,
              title: `db${redisDB}`,
              type: 'redis-keys',
              connectionId: id,
              redisDB: redisDB
          });
          return;
      } else if (node.type === 'db-trigger') {
          const { triggerName, triggerTableName, schemaName, dbName, id } = node.dataRef;
          addTab({
              id: `trigger-${node.key}`,
              title: t('sidebar.tab.trigger', { name: triggerName }),
              type: 'trigger',
              connectionId: id,
              dbName,
              triggerName,
              triggerTableName,
              schemaName,
              sidebarLocateKey: String(node.key || ''),
          });
          return;
      } else if (node.type === 'db-event') {
          openEventDefinition(node);
          return;
      } else if (node.type === 'routine') {
          const { routineName, routineType, dbName, id } = node.dataRef;
          const typeLabel = t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function');
          addTab({
              id: `routine-def-${node.key}`,
              title: t('sidebar.tab.routine_definition', { type: typeLabel, name: routineName }),
              type: 'routine-def',
              connectionId: id,
              dbName,
              routineName,
              routineType
          });
          return;
      } else if (node.type === 'sequence') {
          openSequenceDefinition(node);
          return;
      } else if (node.type === 'package') {
          openPackageDefinition(node);
          return;
      } else if (node.type === 'jvm-mode') {
          const { providerMode, id } = node.dataRef;
          const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;
          openJVMOverviewTab(conn, providerMode);
          return;
      } else if (node.type === 'jvm-resource') {
          const { providerMode, resourcePath, resourceKind, id } = node.dataRef;
          const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;
          openJVMResourceTab(conn, providerMode, resourcePath, resourceKind);
          return;
      } else if (node.type === 'jvm-monitoring') {
          const { providerMode, id } = node.dataRef;
          const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;
          openJVMMonitoringTab(conn, providerMode);
          return;
      } else if (node.type === 'jvm-diagnostic') {
          const conn = (connections.find((item) => item.id === node.dataRef.id) || node.dataRef) as SavedConnection;
          openJVMDiagnosticTab(conn);
          return;
      }

      const key = node.key;
      const isExpanded = expandedKeys.includes(key);
      const newExpandedKeys = isExpanded
          ? expandedKeys.filter(k => k !== key)
          : [...expandedKeys, key];

      setExpandedKeys(newExpandedKeys);
      if (!isExpanded) {
          setAutoExpandParent(false);
          if (shouldLoadSidebarNodeOnExpand(node)) {
              void onLoadData(node);
          }
      }
  };

  const renderSidebarSwitcherIcon = useCallback((node: SidebarTreeSwitcherNodeLike) => {
      if (node.isLeaf) {
          return null;
      }
      const keepCollapsed = shouldKeepSidebarSwitcherCollapsedWhileLoading(node, loadingNodesRef.current);
      return <CaretDownFilled rotate={keepCollapsed ? -90 : undefined} />;
  }, []);
  

  const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {
      return buildRpcConnectionConfig(conn.config, {
          database: resolveSidebarRuntimeDatabase(
              conn?.config?.type,
              conn?.config?.driver,
              conn?.config?.database,
              overrideDatabase,
              clearDatabase,
              conn?.config?.oceanBaseProtocol,
          ),
      });
  };

  const buildJVMRuntimeConfig = (conn: SavedConnection & { dbName?: string }, providerMode: string) => {
      const sourceJVM = conn.config.jvm || {};
      return buildRpcConnectionConfig(conn.config, {
          database: '',
          jvm: {
              ...sourceJVM,
              preferredMode: providerMode as 'jmx' | 'endpoint' | 'agent',
              allowedModes: [providerMode as 'jmx' | 'endpoint' | 'agent'],
          },
      });
  };

  const openJVMOverviewTab = (conn: SavedConnection, providerMode: string) => {
      addTab({
          id: `jvm-overview-${conn.id}-${providerMode}`,
          title: buildJVMTabTitle(conn.name, 'overview', providerMode),
          type: 'jvm-overview',
          connectionId: conn.id,
          providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
      });
  };

  const openJVMMonitoringTab = (conn: SavedConnection, providerMode: string) => {
      addTab({
          id: `jvm-monitoring-${conn.id}-${providerMode}`,
          title: buildJVMTabTitle(conn.name, 'monitoring', providerMode),
          type: 'jvm-monitoring',
          connectionId: conn.id,
          providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
      });
  };

  const buildJVMDiagnosticTreeNodes = (conn: SavedConnection): TreeNode[] => {
      const descriptor = buildJVMDiagnosticActionDescriptor(conn.id, conn.config.jvm?.diagnostic, t);
      if (!descriptor) {
          return [];
      }
      return [{
          title: descriptor.title,
          key: descriptor.key,
          icon: <DashboardOutlined />,
          type: 'jvm-diagnostic',
          dataRef: {
              ...conn,
              diagnosticTransport: descriptor.transport,
          },
          isLeaf: true,
      }];
  };

  const openJVMResourceTab = (conn: SavedConnection, providerMode: string, resourcePath: string, resourceKind?: string) => {
      const trimmedResourcePath = String(resourcePath || '').trim();
      addTab({
          id: `jvm-resource-${conn.id}-${providerMode}-${encodeURIComponent(trimmedResourcePath)}`,
          title: trimmedResourcePath
              ? `${buildJVMTabTitle(conn.name, 'resource', providerMode)} · ${trimmedResourcePath}`
              : buildJVMTabTitle(conn.name, 'resource', providerMode),
          type: 'jvm-resource',
          connectionId: conn.id,
          providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
          resourcePath: trimmedResourcePath,
          resourceKind,
      });
  };

  const openJVMDiagnosticTab = (conn: SavedConnection) => {
      const transport = conn.config.jvm?.diagnostic?.transport || 'agent-bridge';
      addTab({
          id: `jvm-diagnostic-${conn.id}`,
          title: buildJVMTabTitle(conn.name, 'diagnostic', transport),
          type: 'jvm-diagnostic',
          connectionId: conn.id,
      });
  };

  const getConnectionNodeRef = (connRef: any) => {
      const latestConn = connections.find(c => c.id === connRef.id);
      return { key: connRef.id, dataRef: latestConn || connRef };
  };

  const getDatabaseNodeRef = (connRef: any, dbName: string) => {
      const latestConn = connections.find(c => c.id === connRef.id);
      return {
          title: dbName,
          key: `${connRef.id}-${dbName}`,
          dataRef: { ...(latestConn || connRef), dbName }
      };
  };

  const extractObjectName = (fullName: string) => {
      return splitQualifiedName(String(fullName || '').trim()).objectName || String(fullName || '').trim();
  };


  const resolveSavedQueryDisplayName = (name: string | null | undefined) => {
      const rawName = String(name || '').trim();
      return rawName || t('query_editor.save_modal.unnamed');
  };

  const openSavedQueryGroupModal = useCallback(async (
      target?: SavedQueryGroup | null,
      initialParentGroupId?: string | null,
  ) => {
      try {
          const groups = await reloadSavedQueryGroups();
          const targetId = String(target?.id || '').trim();
          if (targetId && !groups.some((group) => group.id === targetId)) {
              message.warning(t('sidebar.message.saved_query_group_not_found'));
              return;
          }
          const parentId = String(initialParentGroupId || '').trim();
          setSavedQueryGroupTargetId(targetId || null);
          setSavedQueryGroupInitialParentId(
              parentId && groups.some((group) => group.id === parentId) ? parentId : null,
          );
          setIsSavedQueryGroupModalOpen(true);
      } catch (error) {
          message.error(t('sidebar.message.saved_query_group_load_failed', {
              error: error instanceof Error ? error.message : String(error),
          }));
      }
  }, [reloadSavedQueryGroups]);

  const closeSavedQueryGroupModal = useCallback(() => {
      setIsSavedQueryGroupModalOpen(false);
      setSavedQueryGroupTargetId(null);
      setSavedQueryGroupInitialParentId(null);
  }, []);

  const handleSaveSavedQueryGroup = useCallback(async (group: SavedQueryGroup) => {
      const isEditing = Boolean(group.id);
      await saveSavedQueryGroup(group);
      message.success(t(
          isEditing
              ? 'sidebar.message.saved_query_group_updated'
              : 'sidebar.message.saved_query_group_created',
      ));
  }, [saveSavedQueryGroup]);

  const savedQueryGroupTarget = useMemo(
      () => savedQueryGroups.find((group) => group.id === savedQueryGroupTargetId) || null,
      [savedQueryGroupTargetId, savedQueryGroups],
  );

  const {
      loadDatabases,
      loadJVMResources,
      loadTables,
  } = useSidebarTreeLoaders({
      savedQueries,
      tableSortPreference,
      tableAccessCount,
      pinnedSidebarTables,
      isV2Ui,
      loadingNodesRef,
      setConnectionStates,
      setLoadedKeys,
      replaceTreeNodeChildren,
      buildRuntimeConfig,
      buildJVMRuntimeConfig,
      buildJVMDiagnosticTreeNodes,
      resolveSavedQueryDisplayName,
      onDatabaseTreeLoaded: (databaseKey: string) => {
          databaseTreeTouchedAtRef.current[databaseKey] = Date.now();
          pruneLoadedDatabaseTrees();
      },
  });

  const openSchemaVisibilitySettings = useCallback((node: any) => {
      const dbName = String(node?.dataRef?.dbName || node?.title || '').trim();
      const connectionId = String(node?.dataRef?.id || '').trim();
      const connection = connections.find((item) => item.id === connectionId) || node?.dataRef;
      if (!connection || !dbName || !shouldHideSchemaPrefix(connection as SavedConnection)) {
          return;
      }

      const databaseNode = node?.type === 'database'
          ? node
          : getDatabaseNodeRef(connection, dbName);
      const currentRule = getSchemaVisibilityRule(connection as SavedConnection, dbName);
      const availableSchemas = Array.from(new Set([
          ...(Array.isArray(databaseNode?.children)
              ? databaseNode.children
                  .filter((item: any) => item?.dataRef?.groupKey === 'schema')
                  .map((item: any) => String(item?.dataRef?.schemaName || item?.title || '').trim())
              : []),
          ...(currentRule?.schemas || []),
      ].filter(Boolean))).sort((a, b) => a.localeCompare(b));

      schemaVisibilityForm.setFieldsValue({
          mode: currentRule?.mode || 'include',
          schemas: currentRule?.schemas || [],
      });
      setSchemaVisibilityTarget({
          connection: connection as SavedConnection,
          dbName,
          databaseNodeKey: databaseNode?.key || `${connectionId}-${dbName}`,
          availableSchemas,
      });
      void loadSchemas(connection as SavedConnection, dbName).then((result) => {
          const loadedSchemas = Array.isArray(result?.schemas)
              ? result.schemas.map((schema) => String(schema || '').trim()).filter(Boolean)
              : [];
          if (loadedSchemas.length === 0) return;
          setSchemaVisibilityTarget((current) => {
              if (!current || current.connection.id !== connectionId || current.dbName !== dbName) {
                  return current;
              }
              return {
                  ...current,
                  availableSchemas: Array.from(new Set([
                      ...current.availableSchemas,
                      ...loadedSchemas,
                  ])).sort((left, right) => left.localeCompare(right)),
              };
          });
      }).catch(() => undefined);
  }, [connections, getDatabaseNodeRef, schemaVisibilityForm]);

  const handleSaveSchemaVisibility = useCallback(async () => {
      if (!schemaVisibilityTarget) return;
      setIsSavingSchemaVisibility(true);
      try {
          const values = await schemaVisibilityForm.validateFields();
          const mode = values.mode === 'exclude' ? 'exclude' : 'include';
          const seenSchemas = new Set<string>();
          const schemas = (Array.isArray(values.schemas) ? values.schemas : [])
              .map((schema) => String(schema || '').trim())
              .filter((schema) => {
                  const normalized = schema.toLocaleLowerCase();
                  if (!normalized || seenSchemas.has(normalized)) return false;
                  seenSchemas.add(normalized);
                  return true;
              });
          const nextRule: SchemaVisibilityRule | undefined = schemas.length > 0
              ? { mode, schemas }
              : undefined;
          const nextConnection = updateSchemaVisibilityRule(
              schemaVisibilityTarget.connection,
              schemaVisibilityTarget.dbName,
              nextRule,
          );
          const backendApp = (window as any).go?.app?.App;
          if (typeof backendApp?.SaveConnection !== 'function') {
              throw new Error(t('connection_modal.message.save_failed'));
          }
          const saved = await backendApp.SaveConnection({
              id: nextConnection.id,
              name: nextConnection.name,
              config: nextConnection.config,
              includeDatabases: nextConnection.includeDatabases,
              includeRedisDatabases: nextConnection.includeRedisDatabases,
              schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase,
              iconType: nextConnection.iconType,
              iconColor: nextConnection.iconColor,
          });
          const persistedConnection: SavedConnection = {
              ...nextConnection,
              ...(saved || {}),
              schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase,
          };
          connectionReloadSignaturesRef.current[persistedConnection.id] =
              buildConnectionReloadSignature(persistedConnection);
          updateConnection(persistedConnection);
          await loadTables({
              key: schemaVisibilityTarget.databaseNodeKey,
              type: 'database',
              dataRef: {
                  ...persistedConnection,
                  dbName: schemaVisibilityTarget.dbName,
              },
          });
          setExpandedKeys((previous) => previous.includes(schemaVisibilityTarget.databaseNodeKey)
              ? previous
              : [...previous, schemaVisibilityTarget.databaseNodeKey]);
          setSchemaVisibilityTarget(null);
          message.success(t('sidebar.schema_visibility.message.saved'));
      } catch (error: any) {
          message.error(t('sidebar.schema_visibility.message.save_failed', {
              error: error?.message || String(error),
          }));
      } finally {
          setIsSavingSchemaVisibility(false);
      }
  }, [loadTables, schemaVisibilityForm, schemaVisibilityTarget, updateConnection]);

  const migrateSchemaVisibilityForRenamedDatabase = useCallback(async (
      connection: SavedConnection,
      oldDbName: string,
      newDbName: string,
  ): Promise<SavedConnection> => {
      const currentConnection = connections.find((item) => item.id === connection.id) || connection;
      const nextConnection = moveSchemaVisibilityRule(currentConnection, oldDbName, newDbName);
      if (nextConnection === currentConnection) {
          return currentConnection;
      }

      const backendApp = (window as any).go?.app?.App;
      if (typeof backendApp?.SaveConnection !== 'function') {
          message.warning(t('sidebar.schema_visibility.message.save_failed', {
              error: t('connection_modal.message.save_failed'),
          }));
          return currentConnection;
      }

      try {
          const saved = await backendApp.SaveConnection({
              id: nextConnection.id,
              name: nextConnection.name,
              config: nextConnection.config,
              includeDatabases: nextConnection.includeDatabases,
              includeRedisDatabases: nextConnection.includeRedisDatabases,
              schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase,
              iconType: nextConnection.iconType,
              iconColor: nextConnection.iconColor,
          });
          const persistedConnection: SavedConnection = {
              ...nextConnection,
              ...(saved || {}),
              schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase,
          };
          connectionReloadSignaturesRef.current[persistedConnection.id] =
              buildConnectionReloadSignature(persistedConnection);
          updateConnection(persistedConnection);
          return persistedConnection;
      } catch (error: any) {
          message.warning(t('sidebar.schema_visibility.message.save_failed', {
              error: error?.message || String(error),
          }));
          return currentConnection;
      }
  }, [connections, updateConnection]);

  const {
      handleCopyStructure,
      handleCopyTable,
      handleCopyTableName,
      handleCopyDatabaseName,
      handleExport,
      openExportDialog,
      handleCopyTableAsInsert,
      openTableDdlInDesigner,
      openTableInERView,
      injectTablePromptToAI,
      handleCreateDatabase,
      openCreateSchemaModal,
      handleCreateSchema,
      openRenameSchemaModal,
      handleRenameSchema,
      handleDeleteSchema,
      handleRenameDatabase,
      handleDeleteDatabase,
      handleRenameTable,
      handleDeleteTable,
      handleTableDataDangerAction,
      openViewDefinition,
      openEditView,
      openCreateView,
      openCreateStarRocksMaterializedView,
      openCreateStarRocksExternalCatalog,
      openCreateStarRocksRollup,
      handleDropView,
      handleRenameView,
      openRenameSavedQueryModal,
      handleRenameSavedQuery,
      handleRevealSavedQueryInFolder,
      isSavedQueryUnmatched,
      handleRebindSavedQuery,
      openRoutineDefinition,
      openEventDefinition,
      openEditEvent,
      openSequenceDefinition,
      openPackageDefinition,
      openEditRoutine,
      openCreateRoutine,
      handleDropRoutine,
      resolveMessagePublishTarget,
      openMessagePublishModal,
      handleMessagePublishSuccess,
  } = useSidebarObjectActions({
      connections,
      connectionIds,
      connectionIdSet,
      tabs,
      treeDataRef,
      setTreeData,
      setExpandedKeys,
      setLoadedKeys,
      addTab,
      updateQueryTabDraft,
      saveQuery,
      addSqlLog,
      closeTabsByDatabase,
      createDbForm,
      targetConnection,
      setIsCreateDbModalOpen,
      createSchemaForm,
      createSchemaTarget,
      setCreateSchemaTarget,
      setIsCreateSchemaModalOpen,
      renameSchemaForm,
      renameSchemaTarget,
      setRenameSchemaTarget,
      setIsRenameSchemaModalOpen,
      renameDbForm,
      renameDbTarget,
      setRenameDbTarget,
      setIsRenameDbModalOpen,
      renameTableForm,
      renameTableTarget,
      setRenameTableTarget,
      setIsRenameTableModalOpen,
      renameViewForm,
      renameViewTarget,
      setRenameViewTarget,
      setIsRenameViewModalOpen,
      renameSavedQueryForm,
      renameSavedQueryTarget,
      setRenameSavedQueryTarget,
      setIsRenameSavedQueryModalOpen,
      setMessagePublishTarget,
      buildRuntimeConfig,
      getConnectionNodeRef,
      getDatabaseNodeRef,
      extractObjectName,
      isPostgresSchemaDialect,
      loadDatabases,
      loadTables,
      openDesign,
      onDoubleClick,
      runExportWithProgress,
      setAIPanelVisible,
      addAIContext,
      migrateSchemaVisibilityForRenamedDatabase,
  });



  const refreshV2TableContextMenuStatsRef = useRef<(node: any) => void>(() => {});

  const {
      getConnectionNodeForAction,
      toggleSidebarTablePinned,
      handleV2TableContextMenuAction,
      handleTableGroupSortAction,
      handleV2TableGroupContextMenuAction,
      handleV2DatabaseContextMenuAction,
      disconnectConnectionNode,
      deleteConnectionNode,
      handleV2ConnectionContextMenuAction,
      handleV2ConnectionGroupContextMenuAction,
  } = useSidebarV2ActionHandlers({
      connections,
      connectionTags,
      pinnedSidebarTables,
      loadingNodesRef,
      treeDataRef,
      findTreeNodeByKeyRef,
      refreshV2TableContextMenuStatsRef,
      setConnectionStates,
      setExpandedKeys,
      setLoadedKeys,
      setTargetConnection,
      setIsCreateDbModalOpen,
      setRenameDbTarget,
      setIsRenameDbModalOpen,
      setRenameTableTarget,
      setIsRenameTableModalOpen,
      setRenameViewTarget,
      setIsCreateTagModalOpen,
      renameDbForm,
      renameTableForm,
      createTagForm,
      addTab,
      closeTabsByDatabase,
      closeTabsByConnection,
      removeConnection,
      removeConnectionTag,
      moveConnectionToTag,
      setSidebarTablePinned,
      setTableSortPreference,
      replaceTreeNodeChildren,
      loadDatabases,
      loadTables,
      getDatabaseNodeRef,
      extractObjectName,
      openDesign,
      openNewTableDesign,
      onDoubleClick,
      openMessagePublishModal,
      openTableDdlInDesigner,
      openTableInERView,
      handleCopyTableName,
      handleCopyTable,
      handleCopyDatabaseName,
      handleCopyStructure,
      handleCopyTableAsInsert,
      openCreateStarRocksRollup,
      handleExport,
      openExportDialog,
      injectTablePromptToAI,
      handleTableDataDangerAction,
      handleDeleteTable,
      openCreateSchemaModal,
      openCreateStarRocksMaterializedView,
      openCreateStarRocksExternalCatalog,
      handleExportDatabaseSQL,
      handleRunSQLFile,
      handleDeleteDatabase,
      onEditConnection,
      handleDuplicateConnection,
      buildConnectionRootQueryTabTitle,
      buildConnectionRootRedisCommandTabTitle,
      buildConnectionRootRedisMonitorTabTitle,
  });
  const {
      onSearch,
      searchScopeSummary,
      searchScopePopoverContent,
      displayTreeData,
      v2CommandSearchObjectMode,
      v2CommandSearchAiMode,
      filteredCommandSearchTreeItems,
      filteredCommandSearchActionItems,
      filteredCommandSearchRecentItems,
      commandSearchAiItem,
      commandSearchFlatItems,
      flattenConnectionNodes,
      activeConnection,
      activeConnectionDisplayName,
      activeDatabaseDisplayName,
      v2VisibleTreeData,
      v2TreeHorizontalScrollWidth,
      effectiveTreeHeight,
      v2TreeMetrics,
      activeConnectionObjectCount,
  } = useSidebarSearchModel({
      searchScopes,
      setSearchScopes,
      setSearchValue,
      deferredSearchValue,
      deferredV2CommandSearchValue,
      v2CommandSearchValue,
      setV2CommandActiveIndex,
      v2ExplorerFilter,
      sidebarTableMetadataFields,
      treeData: visibleSidebarTreeData,
      treeViewportWidth,
      treeHeight,
      isV2Ui,
      isV2CommandSearchOpen,
      connections,
      connectionIds,
      selectedKeys,
      selectedNodesRef,
      activeContext,
      activeTab,
      recentSqlLogs,
      shortcutOptions,
      activeShortcutPlatform,
      overlayTheme,
      darkMode,
      onCreateConnection,
      onToggleAI,
      onToggleLogPanel,
      setAIPanelVisible,
      extractObjectName,
  });
  const legacyToolbarButtonColor = darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)';
  const legacyToolbarStyle: React.CSSProperties = {
      padding: '6px 16px',
      display: 'grid',
      gridTemplateColumns: 'repeat(6, minmax(0, 1fr))',
      gap: 8,
      alignItems: 'center',
      justifyItems: 'center',
      borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
      borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
      background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)',
  };
  const legacyToolbarItemStyle: React.CSSProperties = {
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      minWidth: 0,
  };
  const legacyToolbarDisabledWrapStyle: React.CSSProperties = {
      display: 'inline-flex',
      alignItems: 'center',
      justifyContent: 'center',
  };

  const {
      contextMenu,
      setContextMenu,
      contextMenuPortalRef,
      buildRailConnectionStatus,
      openV2ConnectionContextMenu,
      getV2TreeMetaText,
      renderV2SidebarContextMenuContent,
      fetchV2TableContextMenuStats,
      refreshV2TableContextMenuStats,
  } = useSidebarV2ContextMenu({
      connections,
      connectionStates,
      connectionTags,
      activeShortcutPlatform,
      flattenConnectionNodes,
      v2TreeMetrics,
      tableSortPreference,
      pinnedSidebarTables,
      getConnectionNodeForAction,
      buildRuntimeConfig,
      extractObjectName,
      isPostgresSchemaDialect,
      loadTables,
      getDatabaseNodeRef,
      handleExportSchemaSQL,
      handleDeleteSchema,
      openRenameSchemaModal,
      openSchemaVisibilitySettings,
      resolveMessagePublishTarget,
      addSqlLog,
      handleV2TableContextMenuAction,
      handleV2TableGroupContextMenuAction,
      handleV2DatabaseContextMenuAction,
      handleV2ConnectionContextMenuAction,
      handleV2ConnectionGroupContextMenuAction,
  });
  refreshV2TableContextMenuStatsRef.current = refreshV2TableContextMenuStats;
  const getV2TreeMetaTextRef = useRef(getV2TreeMetaText);
  getV2TreeMetaTextRef.current = getV2TreeMetaText;
  const toggleSidebarTablePinnedRef = useRef(toggleSidebarTablePinned);
  toggleSidebarTablePinnedRef.current = toggleSidebarTablePinned;

  const renderV2TreeTitle = useCallback((node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({
      node,
      hoverTitle,
      statusBadge,
      getV2TreeMetaText: getV2TreeMetaTextRef.current,
      sidebarTableMetadataFields,
      toggleSidebarTablePinned: toggleSidebarTablePinnedRef.current,
      snapshotTreeSelectionBeforeDrag,
      restoreTreeSelectionAfterDrag,
      treeDragSelectSuppressUntilRef,
      setIsTreeDragging,
  }), [
      restoreTreeSelectionAfterDrag,
      setIsTreeDragging,
      sidebarTableMetadataFields,
      snapshotTreeSelectionBeforeDrag,
      treeDragSelectSuppressUntilRef,
  ]);

  const {
      selectConnectionFromRail,
      runCommandSearchItem,
      handleV2CommandSearchKeyDown,
  } = useSidebarCommandSearchRunner({
      activeContext,
      activeTab,
      addTab,
      closeV2CommandSearch,
      commandSearchFlatItems,
      connectionIds,
      findTreeNodeByKeyRef,
      locateObjectInSidebar,
      loadDatabases,
      mergeExpandedTreeKeys,
      onDoubleClick,
      scrollSidebarTreeToKey,
      selectedNodesRef,
      setActiveContext,
      setSelectedKeys,
      setV2CommandActiveIndex,
      treeDataRef,
      v2CommandActiveIndex,
  });
  expandConnectionFromRailRef.current = (connectionId: string) => {
      const conn = connections.find((item) => item.id === connectionId);
      if (conn) {
          selectConnectionFromRail(conn);
      }
  };

  const getNodeMenuItems = (node: any): MenuProps['items'] => buildSidebarLegacyNodeMenuItems(node, {
    addTab,
    getMetadataDialect,
    shouldHideSchemaPrefix,
    openSchemaVisibilitySettings,
    handleV2DatabaseContextMenuAction,
    isPostgresSchemaDialect,
    handleExportSchemaSQL,
    openRenameSchemaModal,
    loadTables,
    getDatabaseNodeRef,
    handleDeleteSchema,
    tableSortPreference,
    isStructureOnlyDbType,
    openNewTableDesign,
    handleTableGroupSortAction,
    openCreateView,
    openCreateStarRocksMaterializedView,
    openCreateRoutine,
    createTagForm,
    setRenameViewTarget,
    setIsCreateTagModalOpen,
    removeConnectionTag,
    setExpandedKeys,
    setLoadedKeys,
    loadingNodesRef,
    loadDatabases,
    buildConnectionRootRedisCommandTabTitle,
    buildConnectionRootRedisMonitorTabTitle,
    onEditConnection,
    handleDuplicateConnection,
    disconnectConnectionNode,
    deleteConnectionNode,
    connectionTags,
    moveConnectionToTag,
    setTargetConnection,
    setIsCreateDbModalOpen,
    buildConnectionRootQueryTabTitle,
    handleRunSQLFile,
    openCreateStarRocksExternalCatalog,
    openEditView,
    renameViewForm,
    setIsRenameViewModalOpen,
    handleDropView,
    onDoubleClick,
    openViewDefinition,
    openRoutineDefinition,
    openEditRoutine,
    handleDropRoutine,
    openEventDefinition,
    openEditEvent,
    openSequenceDefinition,
    openPackageDefinition,
    resolveMessagePublishTarget,
    openMessagePublishModal,
    openDesign,
    openCreateStarRocksRollup,
    handleCopyTableName,
    handleCopyTable,
    handleCopyStructure,
    handleExport,
    setRenameTableTarget,
    renameTableForm,
    setIsRenameTableModalOpen,
    handleTableDataDangerAction,
    handleDeleteTable,
    openExportDialog,
    isSavedQueryUnmatched,
    connections,
    handleRebindSavedQuery,
    openRenameSavedQueryModal,
    handleRevealSavedQueryInFolder,
    resolveSavedQueryDisplayName,
    deleteQuery,
    savedQueryGroups,
    openSavedQueryGroupModal,
    deleteSavedQueryGroup,
    moveSavedQueryToGroup,
    treeDataRef,
    setTreeData,
    handleAddExternalSQLDirectory,
    openCreateExternalSQLFileModal,
    openCreateExternalSQLDirectoryModal,
    openRenameExternalSQLDirectoryModal,
    handleRefreshExternalSQLDirectory,
    handleDeleteExternalSQLDirectory,
    handleRemoveExternalSQLDirectory,
    openExternalSQLFile,
    openRenameExternalSQLFileModal,
    handleDeleteExternalSQLFile,
    extractObjectName,
  });

  const titleRender = useSidebarTitleRender({
      connectionStates,
      isV2Ui,
      renderV2TreeTitle,
      handleAddExternalSQLDirectory,
      snapshotTreeSelectionBeforeDrag,
      restoreTreeSelectionAfterDrag,
      treeDragSelectSuppressUntilRef,
      setIsTreeDragging,
  });
  const getTagParentId = (tagId: unknown): string | null => {
      const tag = connectionTags.find((candidate) => candidate.id === String(tagId || '').trim());
      const parentTagId = String(tag?.parentTagId || '').trim();
      return parentTagId || null;
  };

  const getConnectionParentTagId = (connectionId: unknown): string | null => (
      connectionTags.find((tag) => tag.connectionIds.includes(String(connectionId || '').trim()))?.id || null
  );

  const getNodeParentTagId = (node: any): string | null => {
      if (node?.type === 'tag') return getTagParentId(node?.dataRef?.id);
      if (node?.type === 'connection') return getConnectionParentTagId(node?.key);
      return null;
  };

  const getNodeOrderToken = (node: any): string | null => {
      if (node?.type === 'tag') {
          const tagId = String(node?.dataRef?.id || '').trim();
          return tagId ? buildSidebarRootTagToken(tagId) : null;
      }
      if (node?.type === 'connection') {
          const connectionId = String(node?.key || '').trim();
          return connectionId ? buildSidebarRootConnectionToken(connectionId) : null;
      }
      return null;
  };

  const allowSidebarTreeDrop = ({ dragNode, dropNode, dropPosition }: any): boolean => {
      if (!dragNode || !dropNode) return false;
      if ((dragNode.type !== 'tag' && dragNode.type !== 'connection') || (dropNode.type !== 'tag' && dropNode.type !== 'connection')) {
          return false;
      }
      // Connections cannot contain tree items. A group can contain a group only
      // when the pointer lands on its content, not on its before/after gap.
      const droppingIntoTag = dropNode.type === 'tag' && Number(dropPosition) === 0;
      if (dropNode.type === 'connection' && Number(dropPosition) === 0) return false;
      if (dragNode.type !== 'tag') return String(dragNode.key) !== String(dropNode.key);

      const dragTagId = String(dragNode?.dataRef?.id || '').trim();
      const targetParentTagId = droppingIntoTag
          ? String(dropNode?.dataRef?.id || '').trim() || null
          : getNodeParentTagId(dropNode);
      return !!dragTagId && !isConnectionTagDescendant(dragTagId, targetParentTagId, connectionTags);
  };

  const handleDrop = (info: any) => {
      setIsTreeDragging(false);
      const dropPosition = normalizeSidebarTreeRelativeDropPosition(
          Number(info.dropPosition || 0),
          info?.node?.pos,
      );
      const domDropNode = resolveSidebarDropNodeFromDomEvent(info?.event);
      const dropTargetMetrics = resolveSidebarDropTargetMetricsFromDomEvent(info?.event);
      const insertBefore = resolveSidebarDropInsertBefore(dropPosition, dropTargetMetrics ? {
          clientY: info?.event?.clientY,
          top: dropTargetMetrics.top,
          height: dropTargetMetrics.height,
      } : null);
      const dragNode = info.dragNode;
      const dropNode = domDropNode && domDropNode.key === String(info?.node?.key || '')
          ? info.node
          : (domDropNode
              ? findTreeNodeByKeyRef.current(treeDataRef.current, domDropNode.key) || info.node
              : info.node);
      if (!dragNode || !dropNode) return;

      const droppingIntoTag = dropNode.type === 'tag' && (
          info?.dropToGap === false || (info?.dropToGap === undefined && dropPosition === 0)
      );
      const targetParentTagId = droppingIntoTag
          ? String(dropNode?.dataRef?.id || '').trim() || null
          : getNodeParentTagId(dropNode);
      const targetToken = droppingIntoTag ? null : getNodeOrderToken(dropNode);
      const targetInsertBefore = droppingIntoTag ? false : insertBefore;

      if (dragNode.type === 'tag') {
          const dragTagId = String(dragNode?.dataRef?.id || '').trim();
          if (!dragTagId || isConnectionTagDescendant(dragTagId, targetParentTagId, connectionTags)) return;
          moveConnectionTag(dragTagId, targetParentTagId, targetToken, targetInsertBefore);
          return;
      }

      if (dragNode.type === 'connection') {
          const connectionId = String(dragNode.key || '').trim();
          if (!connectionId || connectionId === String(dropNode.key || '')) return;
          moveConnectionToTag(connectionId, targetParentTagId, targetToken, targetInsertBefore);
      }
  };

  const onRightClick = ({ event, node }: any) => {
      if (isV2Ui && node?.type === 'v2-table-section') {
          event.preventDefault();
          event.stopPropagation();
          return;
      }
      if (isV2Ui && node?.type === 'connection') {
          openV2ConnectionContextMenu(event, node);
          return;
      }
      if (isV2Ui && node?.type === 'database') {
          const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
          setContextMenu({
              x: position.x,
              y: position.y,
              sourceX: event.clientX,
              sourceY: event.clientY,
              items: [],
              kind: 'v2-database',
              node,
              rootClassName: 'gn-v2-table-context-menu-popup',
              overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
              maxHeight: position.maxHeight,
          });
          return;
      }
      if (
          isV2Ui
          && node?.type === 'object-group'
          && node?.dataRef?.groupKey === 'schema'
          && isPostgresSchemaDialect(getMetadataDialect(node.dataRef as SavedConnection))
          && String(node?.dataRef?.schemaName || '').trim()
      ) {
          const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
          setContextMenu({
              x: position.x,
              y: position.y,
              sourceX: event.clientX,
              sourceY: event.clientY,
              items: [],
              kind: 'v2-schema',
              node,
              rootClassName: 'gn-v2-table-context-menu-popup',
              overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
              maxHeight: position.maxHeight,
          });
          return;
      }
      if (isV2Ui && node?.type === 'object-group' && node?.dataRef?.groupKey === 'tables') {
          const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
          setContextMenu({
              x: position.x,
              y: position.y,
              sourceX: event.clientX,
              sourceY: event.clientY,
              items: [],
              kind: 'v2-table-group',
              node,
              rootClassName: 'gn-v2-table-context-menu-popup',
              overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
              maxHeight: position.maxHeight,
          });
          return;
      }
      if (isV2Ui && node?.type === 'table') {
          const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
          setContextMenu({
              x: position.x,
              y: position.y,
              sourceX: event.clientX,
              sourceY: event.clientY,
              items: [],
              kind: 'v2-table',
              node,
              rootClassName: 'gn-v2-table-context-menu-popup',
              overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
              maxHeight: position.maxHeight,
          });
          return;
      }
      const items = getNodeMenuItems(node);
      if (items && items.length > 0) {
          setContextMenu({
              x: event.clientX,
              y: event.clientY,
              items
          });
      }
  };

  const v2RailObjectActionsLabel = t('sidebar.rail.object_actions');
  const v2RailSystemActionsLabel = t('sidebar.rail.system_actions');
  const v2NewGroupLabel = t('sidebar.action.new_group');
  const v2BatchTablesLabel = t('sidebar.action.batch_tables');
  const v2BatchDatabasesLabel = t('sidebar.action.batch_databases');
  const v2DataImportLabel = t('sidebar.action.data_import');
  const v2OpenExternalSqlFileLabel = t('sidebar.sql_file_exec.title');
  const v2LocateCurrentTableLabel = t('sidebar.action.locate_current_table');
  const v2LocateCurrentTableUnavailableLabel = t('sidebar.message.locate_current_table_unavailable');
  const v2AiAssistantLabel = t('app.sidebar.ai_assistant');
  const v2SettingsLabel = t('app.sidebar.settings');
  const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database');
  const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected');
  const v2ConnectionActionsLabel = t('sidebar.active_connection.actions');
  const v2CommandSearchLabel = t('sidebar.command_search.label');
  const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder');

  const handleOpenDataImportWorkbench = useCallback(() => {
    const node = selectedNodesRef.current[0];
    const activeTab = tabs.find((tab) => tab.id === activeTabId);
    const nodeConnectionId = String(
      node?.dataRef?.id || (node?.type === 'connection' ? node?.key : '') || '',
    ).trim();
    const connectionId = nodeConnectionId || String(activeContext?.connectionId || '').trim();
    const dbName = String(
      node?.type === 'database'
        ? (node?.dataRef?.dbName || node?.title || '')
        : node?.dataRef?.dbName || activeContext?.dbName || '',
    ).trim();
    const tableName = String(
      node?.type === 'table'
        ? (node?.dataRef?.tableName || node?.title || '')
        : !node && activeTab?.type === 'table'
          ? activeTab.tableName || ''
          : '',
    ).trim();
    const mode = node?.type === 'database' ? 'database' : 'table';

    const existingImportTab = tabs.find((tab) => tab.id === DATA_IMPORT_WORKBENCH_TAB_ID);
    addTab(resolveDataImportWorkbenchLaunchTab(
      existingImportTab,
      { connectionId, dbName, tableName, mode },
    ));
  }, [activeContext?.connectionId, activeContext?.dbName, activeTabId, addTab, tabs]);

  const v2CommandSearchPanelProps: SidebarSearchPanelProps<V2CommandSearchItem> = {
    isOpen: isV2CommandSearchOpen,
    searchValue: v2CommandSearchValue,
    activeIndex: v2CommandActiveIndex,
    label: v2CommandSearchLabel,
    placeholder: v2CommandSearchPlaceholder,
    persistedFilter: v2PersistedSidebarFilter,
    persistentFilterEnabled: v2CommandSearchPersistentFilterEnabled,
    aiMode: v2CommandSearchAiMode,
    objectMode: v2CommandSearchObjectMode,
    flatItems: commandSearchFlatItems,
    sections: {
      goTo: filteredCommandSearchTreeItems,
      ai: commandSearchAiItem,
      actions: filteredCommandSearchActionItems,
      recent: filteredCommandSearchRecentItems,
    },
    inputRef: commandSearchInputRef,
    handlers: {
      onSearchValueChange: handleV2CommandSearchValueChange,
      onKeyDown: handleV2CommandSearchKeyDown,
      onClose: closeV2CommandSearch,
      onItemSelect: (item: V2CommandSearchItem) => runCommandSearchItem(item),
      onItemHover: (key: string) => setV2CommandActiveIndex(commandSearchFlatItems.findIndex((entry) => entry.key === key)),
      onRemoveRecentItem: (item: V2CommandSearchItem) => {
        if (item.kind === 'recent') hideSqlLogFromRecent(item.logId);
      },
      onClearRecentItems: clearRecentSqlLogs,
      onTogglePersistentFilter: toggleV2CommandSearchPersistentFilter,
      onResetFilter: resetV2SidebarFilter,
    },
  };

  // V2 Connection Rail 子组件 props（从原 renderV2ConnectionRail 抽出，保留所有原行为）
  const v2ConnectionRailProps = {
    labels: {
      railSystemActions: v2RailSystemActionsLabel,
      railObjectActions: v2RailObjectActionsLabel,
      newGroup: v2NewGroupLabel,
      batchTables: v2BatchTablesLabel,
      batchDatabases: v2BatchDatabasesLabel,
      dataImport: v2DataImportLabel,
      openExternalSqlFile: v2OpenExternalSqlFileLabel,
      locateCurrentTable: v2LocateCurrentTableLabel,
      locateCurrentTableUnavailable: v2LocateCurrentTableUnavailableLabel,
      aiAssistant: v2AiAssistantLabel,
      settings: v2SettingsLabel,
    },
    handlers: {
      openCreateTagModal: () => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); },
      openBatchTableExport: openBatchTableWorkbench,
      openBatchDatabaseExport: openBatchDatabaseWorkbench,
      openDataImport: handleOpenDataImportWorkbench,
      openExternalSqlFile: handleOpenSQLFileFromToolbar,
      locateActiveTab: handleLocateActiveTabInSidebar,
      toggleAI: onToggleAI ?? (() => {}),
      openSettings: onOpenSettings ?? (() => {}),
    },
    canLocateActiveTab,
    sidebarExpandAction: onExpandSidebar && expandSidebarLabel ? {
      label: expandSidebarLabel,
      onClick: onExpandSidebar,
      buttonRef: expandSidebarButtonRef,
    } : undefined,
    workbenchActions: (
      <>
        <SlowQueryRailButton
          className="gn-v2-rail-tool gn-v2-rail-sql-analysis-button"
          tooltipPlacement="right"
        />
        <SqlAuditRailButton
          className="gn-v2-rail-tool gn-v2-rail-sql-audit-button"
          tooltipPlacement="right"
        />
      </>
    ),
  };

  return (
    <div className={isV2Ui ? 'gn-v2-sidebar-redesign' : undefined} style={{ display: 'flex', height: '100%', minHeight: 0 }}>
        {exportProgressModal}
        {isV2Ui && <SidebarConnectionRail {...v2ConnectionRailProps} />}
        <div
            id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}
            className={isV2Ui ? 'gn-v2-object-explorer' : undefined}
            data-sidebar-tree-panel={isV2Ui ? 'true' : undefined}
            aria-hidden={isV2Ui ? isTreePanelCollapsed : undefined}
            style={{ display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex', flexDirection: 'column', height: '100%', minWidth: 0, flex: 1 }}
        >
        {isV2Ui && (
            <div className="gn-v2-active-connection-header" data-object-count={activeConnectionObjectCount}>
                <div className="gn-v2-active-connection-trigger" aria-label={v2ActiveConnectionHeaderLabel}>
                    <span className={`gn-v2-live-dot is-${activeConnection ? buildRailConnectionStatus(activeConnection.id) : 'idle'}`} />
                    <div className="gn-v2-active-connection-copy">
                        <strong>{activeConnectionDisplayName}</strong>
                        <span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
                    </div>
                </div>
                <div className="gn-v2-active-connection-actions">
                    <Tooltip title={t('sidebar.menu.new_query')}>
                        <Button
                            size="small"
                            type="text"
                            className="gn-v2-active-connection-query-action"
                            icon={<FileTextOutlined />}
                            aria-label={t('sidebar.menu.new_query')}
                            data-gonavi-new-query-action="true"
                            disabled={!activeConnection}
                            onClick={() => {
                                if (!activeConnection) {
                                    return;
                                }
                                const selectedDatabase = resolveV2SelectedDatabaseName({
                                    activeConnectionId: activeConnection.id,
                                    activeContextConnectionId: activeContext?.connectionId,
                                    activeContextDbName: activeContext?.dbName,
                                });
                                if (selectedDatabase) {
                                    handleV2DatabaseContextMenuAction(getDatabaseNodeRef(activeConnection, selectedDatabase), 'new-query');
                                    return;
                                }
                                handleV2ConnectionContextMenuAction(getConnectionNodeForAction(activeConnection), 'new-query');
                            }}
                        >
                            {t('sidebar.menu.new_query')}
                        </Button>
                    </Tooltip>
                    {onCreateConnection && (
                        <Tooltip title={t('connection.new')}>
                            <Button
                                size="small"
                                type="text"
                                icon={<PlusOutlined />}
                                aria-label={t('connection.new')}
                                data-gonavi-create-connection-action="true"
                                onClick={onCreateConnection}
                            />
                        </Tooltip>
                    )}
                    <Tooltip title={v2ConnectionActionsLabel}>
                        <Button
                            size="small"
                            type="text"
                            icon={<MoreOutlined />}
                            aria-label={v2ConnectionActionsLabel}
                            disabled={!activeConnection}
                            onClick={(event) => {
                                if (activeConnection) {
                                    openV2ConnectionContextMenu(event, activeConnection);
                                }
                            }}
                        />
                    </Tooltip>
                    {onCollapseSidebar && collapseSidebarLabel && (
                        <Tooltip title={collapseSidebarLabel} placement="bottom" mouseEnterDelay={0.35}>
                            <Button
                                ref={collapseSidebarButtonRef}
                                size="small"
                                type="text"
                                className="gonavi-sidebar-collapse-trigger"
                                data-sidebar-collapse-trigger="true"
                                data-sidebar-toggle-placement="explorer-header"
                                aria-label={collapseSidebarLabel}
                                aria-controls="gonavi-sidebar-tree-panel"
                                aria-expanded={true}
                                icon={<MenuFoldOutlined />}
                                onClick={onCollapseSidebar}
                            />
                        </Tooltip>
                    )}
                </div>
            </div>
        )}
        <div className={isV2Ui ? 'gn-v2-explorer-search' : undefined} style={{ padding: '8px 14px', borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>
            {isV2Ui && !v2UseLegacySidebarFilter ? (
                <div className="gn-v2-explorer-command-row" data-v2-sidebar-search-mode="command">
                    <button
                        type="button"
                        className="gn-v2-explorer-command-trigger"
                        onClick={() => {
                            openV2CommandSearch();
                            onFocusCommandSearch?.();
                        }}
                        aria-label={v2CommandSearchLabel}
                    >
                        <SearchOutlined />
                        <span>{v2PersistedSidebarFilter || v2CommandSearchPlaceholder}</span>
                        {focusSidebarSearchShortcutTokens.length > 0 ? (
                            <span className="gn-v2-search-shortcut" aria-hidden="true">
                                {focusSidebarSearchShortcutTokens.map((token, index) => (
                                    <kbd key={`${token}-${index}`}>{token}</kbd>
                                ))}
                            </span>
                        ) : null}
                    </button>
                    <Tooltip title={v2PersistedSidebarFilter ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_synced_filter')}>
                        <button
                            type="button"
                            className="gn-v2-explorer-filter-action"
                            aria-label={t('sidebar.command_search.reset_filter')}
                            disabled={!v2PersistedSidebarFilter}
                            onClick={resetV2SidebarFilter}
                        >
                            <ReloadOutlined />
                        </button>
                    </Tooltip>
                </div>
            ) : isV2Ui ? (
                <div className="gn-v2-explorer-legacy-filter-row" data-v2-sidebar-search-mode="filter">
                    <Input
                        {...noAutoCapInputProps}
                        ref={searchInputRef}
                        value={searchValue}
                        placeholder={t('sidebar.search.placeholder')}
                        onChange={onSearch}
                        size="small"
                        prefix={<SearchOutlined />}
                    />
                    <Tooltip title={searchValue ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_filter_content')}>
                        <button
                            type="button"
                            className="gn-v2-explorer-filter-action"
                            aria-label={t('sidebar.command_search.reset_filter')}
                            disabled={!searchValue}
                            onClick={resetV2SidebarFilter}
                        >
                            <ReloadOutlined />
                        </button>
                    </Tooltip>
                </div>
            ) : (
                <Input
                    {...noAutoCapInputProps}
                    ref={searchInputRef}
                    placeholder={t('sidebar.search.placeholder')}
                    onChange={onSearch}
                    size="small"
                    prefix={<SearchOutlined style={{ color: darkMode ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.35)' }} />}
                    style={{
                        borderRadius: 6,
                        border: 'none',
                        background: darkMode ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.03)',
                        boxShadow: 'none',
                        padding: '4px 8px',
                        color: darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)',
                    }}
                    suffix={(
                        <Popover
                            content={searchScopePopoverContent}
                            trigger="click"
                            placement="bottomRight"
                            open={isSearchScopePopoverOpen}
                            onOpenChange={setIsSearchScopePopoverOpen}
                            styles={{ body: { padding: 0, borderRadius: 16, overflow: 'hidden' } }}
                        >
                            <Tooltip title={t('sidebar.command_search.scope.tooltip', { scope: searchScopeSummary })}>
                                <div
                                    style={{
                                        display: 'flex',
                                        alignItems: 'center',
                                        gap: 4,
                                        cursor: 'pointer',
                                        padding: '2px 6px',
                                        borderRadius: 4,
                                        background: isSearchScopePopoverOpen
                                            ? (darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.06)')
                                            : 'transparent',
                                        transition: 'background 0.2s',
                                        color: searchScopes.includes('smart')
                                            ? (darkMode ? '#ffd666' : '#1677ff')
                                            : (darkMode ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)'),
                                    }}
                                    onMouseEnter={(e) => {
                                        if (!isSearchScopePopoverOpen) {
                                          e.currentTarget.style.background = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)';
                                          e.currentTarget.style.color = darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)';
                                        }
                                    }}
                                    onMouseLeave={(e) => {
                                        if (!isSearchScopePopoverOpen) {
                                          e.currentTarget.style.background = 'transparent';
                                          e.currentTarget.style.color = searchScopes.includes('smart')
                                              ? (darkMode ? '#ffd666' : '#1677ff')
                                              : (darkMode ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)');
                                        }
                                    }}
                                >
                                    <FilterOutlined style={{ fontSize: 13 }} />
                                    <span style={{ fontSize: 12, fontWeight: 500 }}>
                                        {searchScopes.includes('smart') ? t('sidebar.command_search.scope.compact_smart') : searchScopes.length}
                                    </span>
                                </div>
                            </Tooltip>
                        </Popover>
                    )}
                />
            )}
        </div>

        {isV2Ui && (
            <div className="gn-v2-explorer-filter-tabs" aria-label={t('sidebar.command_search.object_kind.filter_aria')}>
                {V2_EXPLORER_FILTER_OPTIONS.map((item) => (
                    <button
                        key={item.key}
                        type="button"
                        className={v2ExplorerFilter === item.key ? 'is-active' : undefined}
                        aria-pressed={v2ExplorerFilter === item.key}
                        onClick={() => setV2ExplorerFilter(item.key)}
                    >
                        {t(item.labelKey)}
                    </button>
                ))}
            </div>
        )}

        {/* Toolbar */}
        {!isV2Ui && (
        <div data-sidebar-legacy-toolbar="true" style={legacyToolbarStyle}>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={t('sidebar.action.new_group')}>
                    <Button
                        size="small"
                        type="text"
                        icon={<FolderOpenOutlined />}
                        aria-label={t('sidebar.action.new_group')}
                        data-sidebar-create-group-action="true"
                        onClick={() => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }}
                        style={{ color: legacyToolbarButtonColor }}
                    />
                </Tooltip>
            </div>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={t('sidebar.action.batch_tables')}>
                    <Button
                        size="small"
                        type="text"
                        icon={<TableOutlined />}
                        aria-label={t('sidebar.action.batch_tables')}
                        data-sidebar-batch-table-action="true"
                        onClick={openBatchTableWorkbench}
                        style={{ color: legacyToolbarButtonColor }}
                    />
                </Tooltip>
            </div>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={t('sidebar.action.batch_databases')}>
                    <Button
                        size="small"
                        type="text"
                        icon={<DatabaseOutlined />}
                        aria-label={t('sidebar.action.batch_databases')}
                        data-sidebar-batch-database-action="true"
                        onClick={openBatchDatabaseWorkbench}
                        style={{ color: legacyToolbarButtonColor }}
                    />
                </Tooltip>
            </div>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={v2DataImportLabel}>
                    <Button
                        size="small"
                        type="text"
                        icon={<ImportOutlined />}
                        aria-label={v2DataImportLabel}
                        data-sidebar-data-import-action="true"
                        onClick={handleOpenDataImportWorkbench}
                        style={{ color: legacyToolbarButtonColor }}
                    />
                </Tooltip>
            </div>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={v2OpenExternalSqlFileLabel}>
                    <Button
                        size="small"
                        type="text"
                        icon={<FileAddOutlined />}
                        aria-label={v2OpenExternalSqlFileLabel}
                        data-sidebar-open-external-sql-file-action="true"
                        onClick={handleOpenSQLFileFromToolbar}
                        style={{ color: legacyToolbarButtonColor }}
                    />
                </Tooltip>
            </div>
            <div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
                <Tooltip title={canLocateActiveTab ? t('sidebar.action.locate_current_tab') : t('sidebar.message.locate_current_tab_unavailable')}>
                    <span style={legacyToolbarDisabledWrapStyle}>
                        <Button
                            size="small"
                            type="text"
                            icon={<AimOutlined />}
                            aria-label={t('sidebar.action.locate_current_tab')}
                            data-sidebar-locate-current-tab-action="true"
                            disabled={!canLocateActiveTab}
                            onClick={handleLocateActiveTabInSidebar}
                            style={{ color: legacyToolbarButtonColor }}
                        />
                    </span>
                </Tooltip>
            </div>
        </div>
        )}

        <div
            ref={treeContainerRef}
            className={`sidebar-tree-scroll-shell${isV2Ui ? ' gn-v2-explorer-tree-shell' : ''}`}
            style={{
                flex: 1,
                overflow: 'hidden',
                minHeight: 0,
            }}
        >
            <div className="sidebar-tree-scroll-content">
                <Tree
                    key={`${isV2Ui ? `v2-tree-${v2ExplorerFilter}` : 'legacy-tree'}-${sidebarObjectVisibilitySignature}`}
                    ref={treeRef}
                    showIcon
                    draggable={{
                        icon: false,
                        nodeDraggable: (node: any) => node.type === 'connection' || node.type === 'tag'
                    }}
                    allowDrop={allowSidebarTreeDrop}
                    onDragStart={() => {
                        snapshotTreeSelectionBeforeDrag();
                        treeDragSelectSuppressUntilRef.current = Date.now() + 600;
                        setIsTreeDragging(true);
                    }}
                    onDragEnter={() => {
                        treeDragSelectSuppressUntilRef.current = Date.now() + 600;
                        setIsTreeDragging(true);
                    }}
                    onDragEnd={() => {
                        restoreTreeSelectionAfterDrag();
                        setIsTreeDragging(false);
                    }}
                    onDrop={handleDrop}
                    loadData={onLoadData}
                    treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}
                    onDoubleClick={onDoubleClick}
                    onSelect={onSelect}
                    titleRender={titleRender}
                    switcherIcon={renderSidebarSwitcherIcon}
                    expandedKeys={expandedKeys}
                    onExpand={onExpand}
                    loadedKeys={loadedKeys}
                    onLoad={setLoadedKeys}
                    autoExpandParent={autoExpandParent}
                    selectedKeys={selectedKeys}
                    blockNode
                    height={effectiveTreeHeight}
                    scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined}
                    onRightClick={onRightClick}
                />
            </div>
        </div>

        </div>
        <SidebarSearchPanel {...v2CommandSearchPanelProps} />

        {contextMenu?.kind && typeof document !== 'undefined' && createPortal(
            <div
                ref={contextMenuPortalRef}
                className={`gn-v2-sidebar-context-menu-portal ${contextMenu.rootClassName || ''}`}
                data-gonavi-close-shortcut-guard="true"
                data-gonavi-close-shortcut-blocks-background="true"
                style={{
                    position: 'fixed',
                    left: contextMenu.x,
                    top: contextMenu.y,
                    zIndex: APP_POPUP_Z_INDEX,
                    width: contextMenu.overlayStyle?.width ?? SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
                    maxWidth: contextMenu.overlayStyle?.maxWidth ?? 'calc(100vw - 24px)',
                    ['--gn-v2-context-menu-max-height' as any]: `${contextMenu.maxHeight ?? SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT}px`,
                }}
                onMouseDown={(event) => event.stopPropagation()}
                onClick={(event) => event.stopPropagation()}
                onContextMenu={(event) => event.preventDefault()}
            >
                {renderV2SidebarContextMenuContent(contextMenu)}
            </div>,
            document.body,
        )}

        {contextMenu && !contextMenu.kind && (
            <Dropdown
                menu={{ items: contextMenu.items }}
                open={true}
                onOpenChange={(open) => { if (!open) setContextMenu(null); }}
                trigger={['contextMenu']}
                rootClassName={contextMenu.rootClassName}
                overlayStyle={contextMenu.overlayStyle}
            >
                <div style={{ position: 'fixed', left: contextMenu.x, top: contextMenu.y, width: 1, height: 1 }} />
            </Dropdown>
        )}

        <SidebarEntityModals
            connections={connections}
            connectionTags={connectionTags}
            modalPanelStyle={modalPanelStyle}
            modalSectionStyle={modalSectionStyle}
            modalScrollSectionStyle={modalScrollSectionStyle}
            renderSidebarModalTitle={renderSidebarModalTitle}
            isCreateTagModalOpen={isCreateTagModalOpen}
            setIsCreateTagModalOpen={setIsCreateTagModalOpen}
            createTagForm={createTagForm}
            renameViewTarget={renameViewTarget}
            updateConnectionTag={updateConnectionTag}
            addConnectionTag={addConnectionTag}
            isCreateDbModalOpen={isCreateDbModalOpen}
            setIsCreateDbModalOpen={setIsCreateDbModalOpen}
            createDbForm={createDbForm}
            handleCreateDatabase={handleCreateDatabase}
            isCreateSchemaModalOpen={isCreateSchemaModalOpen}
            setIsCreateSchemaModalOpen={setIsCreateSchemaModalOpen}
            createSchemaForm={createSchemaForm}
            createSchemaTarget={createSchemaTarget}
            setCreateSchemaTarget={setCreateSchemaTarget}
            handleCreateSchema={handleCreateSchema}
            isRenameSchemaModalOpen={isRenameSchemaModalOpen}
            setIsRenameSchemaModalOpen={setIsRenameSchemaModalOpen}
            renameSchemaForm={renameSchemaForm}
            renameSchemaTarget={renameSchemaTarget}
            setRenameSchemaTarget={setRenameSchemaTarget}
            handleRenameSchema={handleRenameSchema}
            isRenameDbModalOpen={isRenameDbModalOpen}
            setIsRenameDbModalOpen={setIsRenameDbModalOpen}
            renameDbForm={renameDbForm}
            renameDbTarget={renameDbTarget}
            setRenameDbTarget={setRenameDbTarget}
            handleRenameDatabase={handleRenameDatabase}
            isRenameTableModalOpen={isRenameTableModalOpen}
            setIsRenameTableModalOpen={setIsRenameTableModalOpen}
            renameTableForm={renameTableForm}
            renameTableTarget={renameTableTarget}
            setRenameTableTarget={setRenameTableTarget}
            handleRenameTable={handleRenameTable}
            isRenameViewModalOpen={isRenameViewModalOpen}
            setIsRenameViewModalOpen={setIsRenameViewModalOpen}
            renameViewForm={renameViewForm}
            setRenameViewTarget={setRenameViewTarget}
            handleRenameView={handleRenameView}
            isRenameSavedQueryModalOpen={isRenameSavedQueryModalOpen}
            setIsRenameSavedQueryModalOpen={setIsRenameSavedQueryModalOpen}
            renameSavedQueryForm={renameSavedQueryForm}
            renameSavedQueryTarget={renameSavedQueryTarget}
            setRenameSavedQueryTarget={setRenameSavedQueryTarget}
            handleRenameSavedQuery={handleRenameSavedQuery}
        />

        <SavedQueryGroupModal
            open={isSavedQueryGroupModalOpen}
            groups={savedQueryGroups}
            savedQueries={savedQueries}
            target={savedQueryGroupTarget}
            initialParentGroupId={savedQueryGroupInitialParentId}
            modalPanelStyle={modalPanelStyle}
            modalSectionStyle={modalSectionStyle}
            modalScrollSectionStyle={modalScrollSectionStyle}
            renderModalTitle={renderSidebarModalTitle}
            onClose={closeSavedQueryGroupModal}
            onSave={handleSaveSavedQueryGroup}
        />

        <Modal
            title={renderSidebarModalTitle(
                <FolderOpenOutlined />,
                t('sidebar.schema_visibility.title', { database: schemaVisibilityTarget?.dbName || '' }),
                t('sidebar.schema_visibility.description'),
            )}
            open={Boolean(schemaVisibilityTarget)}
            centered
            width={560}
            okText={t('common.save')}
            confirmLoading={isSavingSchemaVisibility}
            styles={{
                content: modalPanelStyle,
                header: { background: 'transparent', borderBottom: 'none', paddingBottom: 10 },
                body: { paddingTop: 8 },
                footer: { background: 'transparent', borderTop: 'none', paddingTop: 12 },
            }}
            onOk={() => void handleSaveSchemaVisibility()}
            onCancel={() => {
                setSchemaVisibilityTarget(null);
                schemaVisibilityForm.resetFields();
            }}
        >
            <Form form={schemaVisibilityForm} layout="vertical">
                <div style={modalSectionStyle}>
                    <Form.Item
                        name="mode"
                        label={t('sidebar.schema_visibility.field.mode')}
                        style={{ marginBottom: 14 }}
                    >
                        <Radio.Group optionType="button" buttonStyle="solid">
                            <Radio.Button value="include">
                                {t('sidebar.schema_visibility.mode.include')}
                            </Radio.Button>
                            <Radio.Button value="exclude">
                                {t('sidebar.schema_visibility.mode.exclude')}
                            </Radio.Button>
                        </Radio.Group>
                    </Form.Item>
                    <Form.Item
                        name="schemas"
                        label={t('sidebar.schema_visibility.field.schemas')}
                        help={t('sidebar.schema_visibility.field.schemas_help')}
                        style={{ marginBottom: 12 }}
                    >
                        <Select
                            mode="tags"
                            allowClear
                            tokenSeparators={[',', ';', '，', '；']}
                            placeholder={t('sidebar.schema_visibility.field.schemas_placeholder')}
                            options={(schemaVisibilityTarget?.availableSchemas || []).map((schema) => ({
                                label: schema,
                                value: schema,
                            }))}
                        />
                    </Form.Item>
                    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
                        <span style={modalHintTextStyle}>
                            {t('sidebar.schema_visibility.notice')}
                        </span>
                        <Button
                            type="link"
                            size="small"
                            onClick={() => schemaVisibilityForm.setFieldsValue({ schemas: [] })}
                        >
                            {t('sidebar.schema_visibility.action.show_all')}
                        </Button>
                    </div>
                </div>
            </Form>
        </Modal>

        <ExternalSQLFileModal {...externalSQLFileModalProps} />

        <FindInDatabaseModal
            open={findInDbContext.open}
            onClose={() => setFindInDbContext({ open: false, connectionId: '', dbName: '' })}
            connectionId={findInDbContext.connectionId}
            dbName={findInDbContext.dbName}
        />
        <MessagePublishModal
            open={Boolean(messagePublishTarget)}
            connection={messagePublishTarget?.connection || null}
            executionDbName={messagePublishTarget?.executionDbName || ''}
            defaultDestination={messagePublishTarget?.destination || ''}
            onCancel={() => setMessagePublishTarget(null)}
            onSuccess={handleMessagePublishSuccess}
        />
    </div>
  );
});

export default Sidebar;
