import Modal from './components/common/ResizableDraggableModal'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Layout, Button, ConfigProvider, theme, message, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip, Alert } from 'antd'; import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined, TableOutlined, MenuOutlined, PoweroffOutlined, TagOutlined, UserOutlined, UpCircleOutlined, MessageOutlined, FileTextOutlined, SyncOutlined, SendOutlined } from '@ant-design/icons'; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetDarkTheme, WindowSetLightTheme, WindowSetPosition, WindowSetSize, WindowSetSystemDefaultTheme, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import FloatingWorkbenchWindows from './components/FloatingWorkbenchWindows'; import FloatingAIChatWindow from './components/FloatingAIChatWindow'; import FloatingQueryResultWindows from './components/FloatingQueryResultWindows'; import ConnectionModal from './components/ConnectionModal'; import SnippetSettingsModal from './components/SnippetSettingsModal'; import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswordModal'; import DataSyncModal from './components/DataSyncModal'; import { type DataSyncEntryMode } from './components/dataSyncEntryMode'; import DriverManagerModal from './components/DriverManagerModal'; import LinuxCJKFontBanner from './components/LinuxCJKFontBanner'; import LogPanel from './components/LogPanel'; import { AISettingsContent } from './components/AISettingsModal'; import AIChatPanel from './components/AIChatPanel'; import AIPanelErrorBoundary from './components/ai/AIPanelErrorBoundary'; import SecurityUpdateBanner from './components/SecurityUpdateBanner'; import SecurityUpdateIntroModal from './components/SecurityUpdateIntroModal'; import SecurityUpdateProgressModal from './components/SecurityUpdateProgressModal'; import SecurityUpdateSettingsModal from './components/SecurityUpdateSettingsModal'; import LanguageSettingsPanel from './components/LanguageSettingsPanel'; import WebAuthSettingsPanel from './components/WebAuthSettingsPanel'; import { DEFAULT_APPEARANCE, MAX_V2_SIDEBAR_RAIL_SCALE, MIN_V2_SIDEBAR_RAIL_SCALE, sanitizeV2SidebarRailScale, useStore, } from './store'; import { GlobalProxyConfig, SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, isMacLikePlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, getLinuxCJKFontInstallHint, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies'; import { DENSITY_OPTIONS, sanitizeDataTableDensity, sanitizeDataTableFontSize, sanitizeSidebarTreeFontSize, } from './utils/dataGridDisplay'; import { TAB_DISPLAY_SECONDARY_DEFAULT_KEYS, TAB_DISPLAY_ELEMENT_META, applyTabDisplaySettingsPatch, resolveTabDisplayElementOrder, sanitizeTabDisplaySettings, switchTabDisplayLayout, type TabDisplayElementKey, type TabDisplayLayout, type TabDisplaySettings, } from './utils/tabDisplay'; import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow'; import { shouldEnableMacWindowDiagnostics } from './utils/macWindowDiagnostics'; import { getConnectionWorkbenchState } from './utils/startupReadiness'; import { createGlobalProxyDraft, toSaveGlobalProxyInput } from './utils/globalProxyDraft'; import { detectConnectionImportKind, isConnectionPackagePasswordRequiredError, resolveConnectionPackageExportResult, normalizeConnectionPackagePassword, } from './utils/connectionExport'; import { bootstrapSecureConfig, finalizeSecurityUpdateStatus, mergeSecurityUpdateStatusWithLegacySource, prepareSecureConfigForExternalMCP, startSecurityUpdateFromBootstrap, } from './utils/secureConfigBootstrap'; import { bootstrapSavedQueries } from './utils/savedQueryPersistence'; import { LEGACY_PERSIST_KEY, hasLegacyMigratableSensitiveItems, stripLegacyPersistedConnectionById, } from './utils/legacyConnectionStorage'; import { DEFAULT_QUERY_TEMPLATE } from './components/queryEditor/QueryEditorHelpers'; import { DEFAULT_SIDEBAR_TABLE_METADATA_FIELDS, SIDEBAR_TABLE_METADATA_FIELDS, applySidebarTableMetadataFieldOrder, resolveSidebarTableMetadataFieldOrder, resolveSidebarTableMetadataFields, setSidebarTableMetadataFieldSelected, type SidebarTableMetadataField, } from './utils/sidebarTableMetadata'; import { getSecurityUpdateStatusMeta, resolveSecurityUpdateEntryVisibility, } from './utils/securityUpdatePresentation'; import { hasSecurityUpdateRecentResult, resolveSecurityUpdateRepairEntry, resolveSecurityUpdateSettingsFocusTarget, shouldRefreshSecurityUpdateDetailsFocus, shouldReopenSecurityUpdateDetails, shouldRetrySecurityUpdateAfterRepairSave, type SecurityUpdateRepairSource, type SecurityUpdateSettingsFocusTarget, } from './utils/securityUpdateRepairFlow'; import { getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix'; import { clearStartupWindowRestorePending, isStartupWindowRestorePending, markStartupWindowRestorePending, resolveDefaultStartupWindowBounds, } from './utils/windowStartupLayout'; import { SHORTCUT_ACTION_META, SHORTCUT_ACTION_ORDER, ShortcutAction, canRecordShortcutForAction, eventToShortcut, findReservedConflictsForAction, getShortcutDisplay, getShortcutDisplayLabel, getShortcutPlatform, installGlobalImeCompositionTracking, isEditableElement, isShortcutMatch, normalizeShortcutCombo, resolveShortcutBinding, splitConflictsByContext, type ConflictInfo, } from './utils/shortcuts'; import { resolveTitleBarToggleIconKey, resolveWindowsScaleCheckDelayMs, shouldApplyWindowsScaleFix, shouldResetWebViewZoomForScaleFix, shouldToggleMaximisedWindowForScaleFix, type WindowScaleFixReason, type WindowsScaleCheckTrigger } from './utils/windowStateUi'; import { resolveVisibleStartupWindowBounds } from './utils/windowRestoreBounds'; import { resolveWailsWindowVisibleViewport } from './utils/wailsWindowViewport'; import { SIDEBAR_UTILITY_ITEM_KEYS, resolveAIEntryPlacement, resolveLegacyAIEdgeHandleAttachment, resolveLegacyAIEdgeHandleDockStyle, resolveLegacyAIEdgeHandleStyle, } from './utils/aiEntryLayout'; import { DEFAULT_AI_PANEL_WIDTH, resolveOverlayAIPanelWidth, shouldOverlayAIPanel } from './utils/aiPanelLayout'; import { safeWindowRuntimeCall } from './utils/wailsRuntime'; import { buildApplicationQuitUnsavedSQLLabel, collectApplicationQuitUnsavedSQLTargets, saveApplicationQuitUnsavedSQLTargets, } from './utils/sqlEditorApplicationQuit'; import { useAppUpdateManager } from './hooks/useAppUpdateManager'; import { useAppLogPanelResize } from './hooks/useAppLogPanelResize'; import { useAppSidebarResize } from './hooks/useAppSidebarResize'; import { useAppUtilityStyles } from './hooks/useAppUtilityStyles'; import { ApplyDataRootDirectory, CancelApplicationQuit, ForceQuitApplication, GetDataRootDirectoryInfo, GetSavedConnections, ListInstalledFontFamilies, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App'; import { getAntdLocale } from './i18n/frameworkLocale'; import { useI18n } from './i18n/provider'; import './App.css'; import './v2-theme.css'; import './styles/v2-theme-workbench.css'; const { Sider, Content } = Layout; const MIN_UI_SCALE = 0.8; const MAX_UI_SCALE = 1.25; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; /** 设置页 Slider 底部预设刻度 */ const UI_SCALE_SLIDER_MARKS: Record = { 0.8: '80%', 0.9: '90%', 1: '100%', 1.1: '110%', 1.25: '125%', }; const FONT_SIZE_SLIDER_MARKS: Record = { 12: '12', 14: '14', 16: '16', 18: '18', 20: '20', }; const SIDEBAR_RAIL_SCALE_SLIDER_MARKS: Record = { 1: '100%', 1.25: '125%', 1.5: '150%', 1.8: '180%', }; const OPACITY_SLIDER_MARKS: Record = { 0.1: '10%', 0.5: '50%', 1: '100%', }; const BLUR_SLIDER_MARKS: Record = { 0: '0', 6: '6', 12: '12', 20: '20', }; const DATA_TABLE_FONT_SLIDER_MARKS: Record = { 10: '10', 12: '12', 14: '14', 16: '16', 18: '18', }; const DEFAULT_UI_SCALE = 1.0; const DEFAULT_FONT_SIZE = 14; const EMPTY_INSTALLED_FONT_FAMILIES: InstalledFontFamily[] = []; type ThemeSettingsSliderUnit = 'percent' | 'px' | 'none'; type ThemeSettingsSliderProps = { min: number; max: number; step?: number; value: number; onChange: (value: number) => void; disabled?: boolean; marks?: Record; /** percent:右侧按百分比输入(内部仍用 0~1 / 0.8~1.25 比例) */ unit?: ThemeSettingsSliderUnit; }; const clampThemeSliderValue = (value: number, min: number, max: number, step?: number): number => { let next = Math.min(max, Math.max(min, value)); if (step && step > 0) { const steps = Math.round((next - min) / step); next = min + steps * step; // 消除浮点误差 const decimals = String(step).includes('.') ? (String(step).split('.')[1]?.length ?? 0) : 0; if (decimals > 0) { next = Number(next.toFixed(decimals)); } next = Math.min(max, Math.max(min, next)); } return next; }; /** 主题设置页:滑条 + 底部预设 + 可编辑数值 */ const ThemeSettingsSlider: React.FC = ({ min, max, step, value, onChange, disabled, marks, unit = 'none', }) => { const isPercent = unit === 'percent'; const minN = Number(min); const maxN = Number(max); const span = maxN - minN || 1; const stepN = step === undefined || step === null ? undefined : Number(step); const current = Number(value); const displayMin = isPercent ? minN * 100 : minN; const displayMax = isPercent ? maxN * 100 : maxN; const displayStep = isPercent ? (stepN !== undefined ? stepN * 100 : 1) : (stepN !== undefined ? stepN : 1); const displayValue = Number.isFinite(current) ? (isPercent ? Number((current * 100).toFixed(4)) : current) : displayMin; const commitDisplayValue = (raw: number | string | null) => { if (raw === null || raw === undefined || raw === '') { return; } const parsed = typeof raw === 'number' ? raw : Number(String(raw).trim().replace(/%/g, '')); if (!Number.isFinite(parsed)) { return; } const modelValue = isPercent ? parsed / 100 : parsed; onChange(clampThemeSliderValue(modelValue, minN, maxN, stepN)); }; const markEntries = marks ? Object.entries(marks).map(([raw, label]) => ({ value: Number(raw), label, })) : []; return (
{ const n = Array.isArray(next) ? next[0] : next; if (typeof n === 'number' && Number.isFinite(n)) { onChange(clampThemeSliderValue(n, minN, maxN, stepN)); } }} disabled={disabled} tooltip={{ open: false }} />
{markEntries.length > 0 ? (
{markEntries.map((mark) => { const pct = ((mark.value - minN) / span) * 100; const active = Number.isFinite(current) ? (stepN && stepN > 0 ? Math.abs(current - mark.value) <= stepN / 2 + 1e-9 : Math.abs(current - mark.value) < 1e-6) : false; return ( ); })}
) : null}
{ if (typeof next === 'number') { commitDisplayValue(next); } }} onBlur={(event) => { commitDisplayValue(event.target.value); }} onPressEnter={(event) => { commitDisplayValue((event.target as HTMLInputElement).value); (event.target as HTMLInputElement).blur(); }} />
); }; const createEmptySecurityUpdateStatus = (): SecurityUpdateStatus => ({ overallStatus: 'not_detected', summary: { total: 0, updated: 0, pending: 0, skipped: 0, failed: 0, }, issues: [], }); const detectNavigatorPlatform = (): string => { if (typeof navigator === 'undefined') { return ''; } const uaDataPlatform = (navigator as Navigator & { userAgentData?: { platform?: string }; }).userAgentData?.platform; if (uaDataPlatform) { return uaDataPlatform; } return navigator.userAgent || ''; }; const readCurrentVisibleViewport = () => resolveWailsWindowVisibleViewport( window.screen as Screen & { availLeft?: number; availTop?: number }, { innerWidth: window.innerWidth, innerHeight: window.innerHeight }, { useMonitorLocalOrigin: isMacLikePlatform() }, ); const getSystemThemeMode = (): 'light' | 'dark' => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return 'light'; } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }; const mergeSavedConnections = (current: SavedConnection[], imported: SavedConnection[]): SavedConnection[] => { const merged = new Map(); current.forEach((conn) => merged.set(conn.id, conn)); imported.forEach((conn) => merged.set(conn.id, conn)); return Array.from(merged.values()); }; type ConnectionPackageDialogMode = 'import' | 'export'; type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace'; type ToolCenterPaneKey = | 'connection-package' | 'data-root' | 'security-update' | 'schema-compare' | 'data-compare' | 'sync' | 'drivers' | 'snippet-settings' | 'shortcut-settings'; type ToolCenterPaneState = { key: ToolCenterPaneKey; group: ToolCenterGroupKey; }; type SettingsCenterGroupKey = 'preferences' | 'services' | 'about'; type SettingsCenterPaneKey = 'language' | 'theme' | 'sidebar-metadata' | 'proxy' | 'web-auth' | 'ai' | 'about-go-navi'; type SettingsCenterPaneState = { key: SettingsCenterPaneKey; group: SettingsCenterGroupKey; }; const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => ( group === 'about' ? { key: 'about-go-navi', group: 'about' } : null ); const DEFAULT_GLOBAL_PROXY_TEST_URL = 'https://api.github.com/'; type GlobalProxyTestResultState = { success: boolean; message: string; url?: string; finalUrl?: string; statusCode?: number; durationMs?: number; viaProxy?: boolean; }; const getGlobalProxyDefaultPort = (type: GlobalProxyConfig['type']): number => ( type === 'http' ? 8080 : 1080 ); const createGlobalProxyComparableDraft = ( value: Partial = {}, ): GlobalProxyConfig => ({ ...createGlobalProxyDraft(value), password: typeof value.password === 'string' ? value.password : '', }); const areGlobalProxyDraftsEqual = ( left: Partial, right: Partial, ): boolean => { const normalizedLeft = createGlobalProxyComparableDraft(left); const normalizedRight = createGlobalProxyComparableDraft(right); return ( normalizedLeft.enabled === normalizedRight.enabled && normalizedLeft.type === normalizedRight.type && normalizedLeft.host === normalizedRight.host && normalizedLeft.port === normalizedRight.port && normalizedLeft.user === normalizedRight.user && normalizedLeft.password === normalizedRight.password && normalizedLeft.hasPassword === normalizedRight.hasPassword ); }; const formatAboutCheckedAt = (value: Date): string => { const pad = (input: number) => String(input).padStart(2, '0'); return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}`; }; const formatAboutReleaseTime = (value: string | undefined): string => { const text = String(value || '').trim(); if (!text) { return '-'; } const date = new Date(text); if (Number.isNaN(date.getTime())) { return '-'; } return formatAboutCheckedAt(date); }; type ConnectionPackageDialogState = { open: boolean; mode: ConnectionPackageDialogMode; includeSecrets: boolean; useFilePassword: boolean; password: string; error: string; confirmLoading: boolean; }; const createClosedConnectionPackageDialogState = (): ConnectionPackageDialogState => ({ open: false, mode: 'export', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); type SidebarMetadataSortableRowProps = { field: SidebarTableMetadataField; label: string; checked: boolean; dividerColor: string; titleColor: string; mutedColor: string; onToggle: (selected: boolean) => void; }; const SidebarMetadataSortableRow: React.FC = ({ field, label, checked, dividerColor, titleColor, mutedColor, onToggle, }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: field }); return (
{label}
onToggle(selected)} />
); }; function App() { const { language, t } = useI18n(); const [isModalOpen, setIsModalOpen] = useState(false); const [isConnectionModalMounted, setIsConnectionModalMounted] = useState(false); const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); const [syncModalEntryMode, setSyncModalEntryMode] = useState('sync'); const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); const connectionModalWarmupDoneRef = useRef(false); const windowState = useStore(state => state.windowState); const themeMode = useStore(state => state.theme); const themePreference = useStore(state => state.themePreference); const setTheme = useStore(state => state.setTheme); const setThemePreference = useStore(state => state.setThemePreference); const appearance = useStore(state => state.appearance); const setAppearance = useStore(state => state.setAppearance); const uiScale = useStore(state => state.uiScale); const setUiScale = useStore(state => state.setUiScale); const fontSize = useStore(state => state.fontSize); const setFontSize = useStore(state => state.setFontSize); const startupFullscreen = useStore(state => state.startupFullscreen); const setStartupFullscreen = useStore(state => state.setStartupFullscreen); const globalProxy = useStore(state => state.globalProxy); const replaceConnections = useStore(state => state.replaceConnections); const replaceGlobalProxy = useStore(state => state.replaceGlobalProxy); const replaceSavedQueries = useStore(state => state.replaceSavedQueries); const queryOptions = useStore(state => state.queryOptions); const setQueryOptions = useStore(state => state.setQueryOptions); const shortcutOptions = useStore(state => state.shortcutOptions); const updateShortcut = useStore(state => state.updateShortcut); const resetShortcutOptions = useStore(state => state.resetShortcutOptions); const [systemThemeMode, setSystemThemeMode] = useState<'light' | 'dark'>(() => getSystemThemeMode()); const darkMode = themeMode === 'dark'; const isV2Ui = appearance.uiVersion === 'v2'; const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE)); const effectiveFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(fontSize) || DEFAULT_FONT_SIZE))); const tokenFontSize = Math.round(effectiveFontSize * effectiveUiScale); const titleBarToggleIconKey = resolveTitleBarToggleIconKey( windowState === 'fullscreen' ? 'fullscreen' : (windowState === 'maximized' ? 'maximized' : 'normal') ); const tokenFontSizeSM = Math.max(10, Math.round(tokenFontSize * 0.86)); const tokenFontSizeLG = Math.max(tokenFontSize + 1, Math.round(tokenFontSize * 1.14)); const tokenControlHeight = Math.max(24, Math.round(32 * effectiveUiScale)); const tokenControlHeightSM = Math.max(20, Math.round(24 * effectiveUiScale)); const tokenControlHeightLG = Math.max(30, Math.round(40 * effectiveUiScale)); const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false; const sidebarTreeFontSizeFollowsGlobal = appearance.sidebarTreeFontSizeFollowGlobal !== false; const effectiveDataTableFontSize = dataTableFontSizeFollowsGlobal ? effectiveFontSize : (sanitizeDataTableFontSize(appearance.dataTableFontSize) ?? effectiveFontSize); const effectiveSidebarTreeFontSize = sidebarTreeFontSizeFollowsGlobal ? effectiveFontSize : (sanitizeSidebarTreeFontSize(appearance.sidebarTreeFontSize) ?? effectiveFontSize); const effectiveSidebarRailScale = sanitizeV2SidebarRailScale(appearance.v2SidebarRailScale); const tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; const newQuerySqlTemplate = appearance.newQuerySqlTemplate ?? DEFAULT_QUERY_TEMPLATE; const sidebarTableMetadataFieldOrder = useMemo( () => resolveSidebarTableMetadataFieldOrder(queryOptions?.sidebarTableMetadataFieldOrder), [queryOptions?.sidebarTableMetadataFieldOrder], ); const sidebarTableMetadataFields = useMemo( () => resolveSidebarTableMetadataFields( queryOptions?.sidebarTableMetadataFields, queryOptions?.showSidebarTableComment === true, sidebarTableMetadataFieldOrder, ), [queryOptions?.showSidebarTableComment, queryOptions?.sidebarTableMetadataFields, sidebarTableMetadataFieldOrder], ); const sidebarMetadataDragSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 }, }), ); const tabDisplaySettings = useMemo( () => sanitizeTabDisplaySettings(appearance.tabDisplay), [appearance.tabDisplay], ); const tabDisplayElementOrder = useMemo( () => resolveTabDisplayElementOrder(tabDisplaySettings), [tabDisplaySettings], ); const visibleTabDisplayElementKeys = useMemo( () => new Set([ ...tabDisplaySettings.primaryElements, ...tabDisplaySettings.secondaryElements, ]), [tabDisplaySettings], ); const getTabDisplayElementLabel = useCallback( (key: TabDisplayElementKey) => t(TAB_DISPLAY_ELEMENT_META[key].labelKey), [t], ); const getTabDisplayElementDescription = useCallback( (key: TabDisplayElementKey) => t(TAB_DISPLAY_ELEMENT_META[key].descriptionKey), [t], ); useEffect(() => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return; } const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); const applySystemTheme = (matches: boolean) => { setSystemThemeMode(matches ? 'dark' : 'light'); }; applySystemTheme(mediaQueryList.matches); const handleChange = (event: MediaQueryListEvent) => { applySystemTheme(event.matches); }; if (typeof mediaQueryList.addEventListener === 'function') { mediaQueryList.addEventListener('change', handleChange); return () => { mediaQueryList.removeEventListener('change', handleChange); }; } mediaQueryList.addListener(handleChange); return () => { mediaQueryList.removeListener(handleChange); }; }, []); useEffect(() => { const resolvedTheme = themePreference === 'system' ? systemThemeMode : themePreference; if (themeMode !== resolvedTheme) { setTheme(resolvedTheme); } if (themePreference === 'system') { void safeWindowRuntimeCall(() => WindowSetSystemDefaultTheme(), undefined); return; } if (resolvedTheme === 'dark') { void safeWindowRuntimeCall(() => WindowSetDarkTheme(), undefined); return; } void safeWindowRuntimeCall(() => WindowSetLightTheme(), undefined); }, [setTheme, systemThemeMode, themeMode, themePreference]); const setTabDisplaySettings = useCallback((settings: Partial) => { setAppearance({ tabDisplay: applyTabDisplaySettingsPatch(tabDisplaySettings, settings), }); }, [setAppearance, tabDisplaySettings]); const setTabDisplayLayout = useCallback((layout: TabDisplayLayout) => { if (layout === tabDisplaySettings.layout) return; setAppearance({ tabDisplay: switchTabDisplayLayout(tabDisplaySettings, layout), }); }, [setAppearance, tabDisplaySettings]); const updateTabDisplayElementVisibility = useCallback((key: TabDisplayElementKey, checked: boolean) => { setFocusedTabDisplayElementKey(key); const removeKey = (keys: TabDisplayElementKey[]) => keys.filter((item) => item !== key); if (!checked) { setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements: removeKey(tabDisplaySettings.primaryElements), secondaryElements: removeKey(tabDisplaySettings.secondaryElements), }); return; } const primaryElements = removeKey(tabDisplaySettings.primaryElements); const secondaryElements = removeKey(tabDisplaySettings.secondaryElements); if (tabDisplaySettings.layout === 'double' && TAB_DISPLAY_SECONDARY_DEFAULT_KEYS.includes(key)) { secondaryElements.push(key); } else { primaryElements.push(key); } setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements, secondaryElements, }); }, [setTabDisplaySettings, tabDisplaySettings]); const moveTabDisplayElement = useCallback((key: TabDisplayElementKey, offset: -1 | 1) => { setFocusedTabDisplayElementKey(key); const moveWithin = (keys: TabDisplayElementKey[]) => { const index = keys.indexOf(key); if (index < 0) return keys; const nextIndex = index + offset; if (nextIndex < 0 || nextIndex >= keys.length) return keys; const next = [...keys]; [next[index], next[nextIndex]] = [next[nextIndex], next[index]]; return next; }; setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements: moveWithin(tabDisplaySettings.primaryElements), secondaryElements: moveWithin(tabDisplaySettings.secondaryElements), }); }, [setTabDisplaySettings, tabDisplaySettings]); const setTabDisplayElementRow = useCallback((key: TabDisplayElementKey, row: 'primary' | 'secondary') => { setFocusedTabDisplayElementKey(key); const primaryElements = tabDisplaySettings.primaryElements.filter((item) => item !== key); const secondaryElements = tabDisplaySettings.secondaryElements.filter((item) => item !== key); if (row === 'primary') { primaryElements.push(key); } else { secondaryElements.push(key); } setTabDisplaySettings({ layout: tabDisplaySettings.layout, primaryElements, secondaryElements, }); }, [setTabDisplaySettings, tabDisplaySettings]); const resolvedUiFontFamily = resolveUIFontFamily(appearance.customUIFontFamily); const resolvedMonoFontFamily = resolveMonoFontFamily(appearance.customMonoFontFamily); const appComponentSize: 'small' | 'middle' | 'large' = effectiveUiScale <= 0.92 ? 'small' : (effectiveUiScale >= 1.12 ? 'large' : 'middle'); const titleBarHeight = Math.max(28, Math.round(32 * effectiveUiScale)); const titleBarButtonWidth = Math.max(40, Math.round(46 * effectiveUiScale)); const floatingLogButtonHeight = Math.max(30, Math.round(34 * effectiveUiScale)); const resolvedAppearance = resolveAppearanceValues(appearance); const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); const effectiveBlur = normalizeBlurForPlatform(resolvedAppearance.blur); const blurFilter = blurToFilter(effectiveBlur); const [runtimePlatform, setRuntimePlatform] = useState(''); const [runtimeBuildType, setRuntimeBuildType] = useState(''); const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); const isWebRuntime = runtimeBuildType === 'web'; const [installedFontFamilies, setInstalledFontFamilies] = useState(EMPTY_INSTALLED_FONT_FAMILIES); const [isFontFamiliesLoading, setIsFontFamiliesLoading] = useState(false); const [fontFamiliesLoadError, setFontFamiliesLoadError] = useState(null); const hasLoadedInstalledFontsRef = useRef(false); const uiFontOptions = useMemo( () => buildFontFamilyOptions(runtimePlatform, 'ui', installedFontFamilies, t), [installedFontFamilies, runtimePlatform, t], ); const monoFontOptions = useMemo( () => buildFontFamilyOptions(runtimePlatform, 'mono', installedFontFamilies, t), [installedFontFamilies, runtimePlatform, t], ); const linuxCJKFontInstallHint = getLinuxCJKFontInstallHint(runtimePlatform, installedFontFamilies); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false); const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? 1280 : window.innerWidth || 1280)); const [securityUpdateStatus, setSecurityUpdateStatus] = useState(() => createEmptySecurityUpdateStatus()); const [securityUpdateRawPayload, setSecurityUpdateRawPayload] = useState(null); const [securityUpdateHasLegacySensitiveItems, setSecurityUpdateHasLegacySensitiveItems] = useState(false); const [isSecurityUpdateIntroOpen, setIsSecurityUpdateIntroOpen] = useState(false); const [isSecurityUpdateBannerDismissed, setIsSecurityUpdateBannerDismissed] = useState(false); const [isSecurityUpdateSettingsOpen, setIsSecurityUpdateSettingsOpen] = useState(false); const [securityUpdateSettingsFocusTarget, setSecurityUpdateSettingsFocusTarget] = useState(null); const [securityUpdateSettingsFocusRequest, setSecurityUpdateSettingsFocusRequest] = useState(0); const [isSecurityUpdateProgressOpen, setIsSecurityUpdateProgressOpen] = useState(false); const [securityUpdateProgressStage, setSecurityUpdateProgressStage] = useState(() => t('app.security_update.stage.checking_saved_config')); const [securityUpdateRepairSource, setSecurityUpdateRepairSource] = useState(null); const [focusedTabDisplayElementKey, setFocusedTabDisplayElementKey] = useState(null); const [focusedAIProviderId, setFocusedAIProviderId] = useState(undefined); const [connectionPackageDialog, setConnectionPackageDialog] = useState(() => createClosedConnectionPackageDialogState()); const [pendingConnectionImportPayload, setPendingConnectionImportPayload] = useState(null); const [aiPanelRenderNonce, setAiPanelRenderNonce] = useState(0); const sidebarWidth = useStore(state => state.sidebarWidth); const setSidebarWidth = useStore(state => state.setSidebarWidth); const aiPanelVisible = useStore(state => state.aiPanelVisible); const detachedAIChatWindow = useStore(state => state.detachedAIChatWindow); const detachAIChatPanel = useStore(state => state.detachAIChatPanel); const aiChatDetached = Boolean(detachedAIChatWindow); const toggleAIPanel = useStore(state => state.toggleAIPanel); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); const windowDiagSequenceRef = React.useRef(0); const windowDiagLastSignatureRef = React.useRef(''); const windowDiagLastAtRef = React.useRef(0); const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig); const securityUpdateStatusMeta = useMemo( () => getSecurityUpdateStatusMeta(securityUpdateStatus, t), [securityUpdateStatus, t], ); const securityUpdateEntryVisibility = useMemo( () => resolveSecurityUpdateEntryVisibility(securityUpdateStatus), [securityUpdateStatus], ); const windowCornerRadius = 14; useEffect(() => { if (typeof window === 'undefined') { return; } const syncViewportWidth = () => { setViewportWidth(window.innerWidth || document.documentElement?.clientWidth || 1280); }; syncViewportWidth(); window.addEventListener('resize', syncViewportWidth); return () => window.removeEventListener('resize', syncViewportWidth); }, []); useEffect(()=>{ if (typeof document === 'undefined' || !document.body) { return; } switch(windowState){ case 'fullscreen': case 'maximized': document.body.style.setProperty('--gonavi-border-radius', '0px'); break; default: document.body.style.setProperty('--gonavi-border-radius', `${windowCornerRadius}px`); break; } }, [windowState]); // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, // 避免 GPU 持续计算窗口背后的模糊合成 useEffect(() => { try { void SetWindowTranslucency(resolvedAppearance.opacity, resolvedAppearance.blur).catch(() => undefined); } catch(e) { /* ignore */ } }, [resolvedAppearance.blur, resolvedAppearance.opacity]); useEffect(() => { let cancelled = false; try { Environment() .then((env) => { if (cancelled) return; const platform = String(env?.platform || '').toLowerCase(); setRuntimePlatform(platform); setRuntimeBuildType(String(env?.buildType || '').toLowerCase()); setIsLinuxRuntime(platform === 'linux'); }) .catch(() => { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); }); } catch(e) { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); } return () => { cancelled = true; }; }, []); useEffect(() => { if (isStoreHydrated) { return; } const unsubscribe = useStore.persist.onFinishHydration(() => { setIsStoreHydrated(true); }); return () => { unsubscribe(); }; }, [isStoreHydrated]); useEffect(() => { if (!isStoreHydrated) { return; } let cancelled = false; const loadSavedQueries = async () => { try { await bootstrapSavedQueries({ backend: (window as any).go?.app?.App, replaceSavedQueries: (queries) => { if (!cancelled) { replaceSavedQueries(queries); } }, }); } catch (err) { console.warn('Failed to bootstrap saved queries', err); } }; void loadSavedQueries(); return () => { cancelled = true; }; }, [isStoreHydrated, replaceSavedQueries]); const normalizeSecurityUpdateStatus = useCallback((status?: Partial | null): SecurityUpdateStatus => { const fallback = createEmptySecurityUpdateStatus(); return { ...fallback, ...(status ?? {}), summary: { ...fallback.summary, ...(status?.summary ?? {}), }, issues: Array.isArray(status?.issues) ? status.issues : [], }; }, []); const applySecurityUpdateStatus = useCallback(( status?: Partial | null, options?: { openSettings?: boolean; refreshFocus?: boolean; resetBannerDismissed?: boolean; }, ) => { const nextStatus = normalizeSecurityUpdateStatus(status); const visibility = resolveSecurityUpdateEntryVisibility(nextStatus); setSecurityUpdateStatus(nextStatus); setIsSecurityUpdateIntroOpen(visibility.showIntro); if (options?.resetBannerDismissed !== false) { setIsSecurityUpdateBannerDismissed(false); } if (options?.openSettings) { if (options.refreshFocus !== false) { setSecurityUpdateSettingsFocusTarget(resolveSecurityUpdateSettingsFocusTarget(nextStatus)); setSecurityUpdateSettingsFocusRequest((current) => current + 1); } setIsSecurityUpdateSettingsOpen(true); } return nextStatus; }, [normalizeSecurityUpdateStatus]); useEffect(() => { if (!isStoreHydrated) { return; } let cancelled = false; const loadSecureConfig = async () => { try { const result = await bootstrapSecureConfig({ backend: (window as any).go?.app?.App, autoStartLegacySecurityUpdate: true, replaceConnections, replaceGlobalProxy, t, }); if (cancelled) { return; } setSecurityUpdateRawPayload(result.rawPayload); setSecurityUpdateHasLegacySensitiveItems(result.hasLegacySensitiveItems); applySecurityUpdateStatus(result.status); } catch (err) { console.warn('Failed to bootstrap secure config', err); } finally { if (!cancelled) { setHasLoadedSecureConfig(true); } } }; void loadSecureConfig(); return () => { cancelled = true; }; }, [applySecurityUpdateStatus, isStoreHydrated, replaceConnections, replaceGlobalProxy, t]); useEffect(() => { let cancelled = false; let startupWindowTimer: number | null = null; let restoredOnce = false; const maxApplyAttempts = 8; const applyRetryDelayMs = 350; const settleDelayMs = 180; const useMaximiseForStartup = isWindowsPlatform(); const checkStartupPreferenceApplied = async (): Promise => { try { if (await WindowIsFullscreen()) { return true; } } catch (_) { // ignore } try { if (await WindowIsMaximised()) { return true; } } catch (_) { // ignore } return false; }; const markAppliedMaximisedOrFullscreen = (mode: 'maximised' | 'fullscreen') => { // 启动偏好成功后立刻固化 windowState,避免宽限期内被写成 normal 导致下次半窗 if (mode === 'maximised' || useMaximiseForStartup) { useStore.getState().setWindowState('maximized'); } else { useStore.getState().setWindowState('fullscreen'); } clearStartupWindowRestorePending(); }; // mode: // - maximised: 始终最大化(Windows 启动偏好 / 记忆的 maximized / Windows 上的 fullscreen 记忆) // - fullscreen: 非 Windows 优先真全屏,失败再最大化 // 第 1 次立即执行(delay=0),避免 Windows 先闪 1024×768 半窗再最大化 const applyStartupWindowChrome = (attempt: number, mode: 'maximised' | 'fullscreen') => { if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } const delayMs = attempt <= 1 ? 0 : applyRetryDelayMs; startupWindowTimer = window.setTimeout(() => { if (cancelled) { return; } void Promise.resolve() .then(async () => { if (await checkStartupPreferenceApplied()) { markAppliedMaximisedOrFullscreen(mode); return; } try { if (mode === 'maximised') { await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } else { await WindowFullscreen(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); if (await checkStartupPreferenceApplied()) { markAppliedMaximisedOrFullscreen(mode); return; } await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } } catch (e) { console.warn("Wails Window APIs unavailable", e); } if (await checkStartupPreferenceApplied()) { markAppliedMaximisedOrFullscreen(mode); return; } if (attempt < maxApplyAttempts) { applyStartupWindowChrome(attempt + 1, mode); } else { // 最终仍失败:结束宽限,避免长期阻断 bounds 记忆;下次启动会再试 clearStartupWindowRestorePending(); void emitWindowDiagnostic('warn:startup-maximise-failed', { mode, attempts: attempt, }); } }); }, delayMs); }; const restoreWindowState = async () => { if (cancelled) return; // 仅在 hydration 完成后跑一次(或显式重入);避免未水合默认态先写半窗 bounds if (!useStore.persist.hasHydrated()) { return; } if (restoredOnce) { return; } restoredOnce = true; const state = useStore.getState(); // 1) 「启动时最大化」开关优先(Windows 按 Maximize 处理) if (state.startupFullscreen) { markStartupWindowRestorePending(3200); applyStartupWindowChrome(1, useMaximiseForStartup ? 'maximised' : 'fullscreen'); return; } // 2) 记忆用户上次窗口态:最大化/全屏 const savedState = state.windowState; if (savedState === 'fullscreen') { // Windows 上记忆的 fullscreen 也走最大化,避免真全屏后标题栏交互困难 markStartupWindowRestorePending(3200); applyStartupWindowChrome(1, useMaximiseForStartup ? 'maximised' : 'fullscreen'); return; } if (savedState === 'maximized') { // 必须重试:Windows 冷启动 HWND/WebView2 未就绪时单次 Maximise 经常失败, // 会残留 main.go 默认 1024x768 贴左上角;任务栏恢复后才“突然正常”。 markStartupWindowRestorePending(3200); applyStartupWindowChrome(1, 'maximised'); return; } // 3) 普通窗口:恢复用户调整过的尺寸和位置 const bounds = state.windowBounds; if (!bounds || bounds.width < 400 || bounds.height < 300) { // 无记忆尺寸时,Windows 默认左上角小窗看起来像“只开了一半”,改为居中可用尺寸 if (isWindowsPlatform()) { try { const nextBounds = resolveDefaultStartupWindowBounds(readCurrentVisibleViewport()); WindowSetSize(nextBounds.width, nextBounds.height); WindowSetPosition(nextBounds.x, nextBounds.y); state.setWindowBounds(nextBounds); state.setWindowState('normal'); void emitWindowDiagnostic('adjust:startup-default-window-bounds', { to: nextBounds, }); } catch (e) { console.warn('Failed to apply default Windows startup bounds', e); } } return; } try { const nextBounds = resolveVisibleStartupWindowBounds(bounds, readCurrentVisibleViewport()); if ( nextBounds.x !== bounds.x || nextBounds.y !== bounds.y || nextBounds.width !== bounds.width || nextBounds.height !== bounds.height ) { void emitWindowDiagnostic('adjust:startup-window-bounds', { from: bounds, to: nextBounds, }); state.setWindowBounds(nextBounds); } WindowSetSize(nextBounds.width, nextBounds.height); WindowSetPosition(nextBounds.x, nextBounds.y); state.setWindowState('normal'); } catch (e) { console.warn('Failed to restore window bounds', e); } }; if (useStore.persist.hasHydrated()) { void restoreWindowState(); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled) { return; } // hydration 完成后再恢复,确保读到 startupFullscreen / windowState / windowBounds restoredOnce = false; void restoreWindowState(); }); return () => { cancelled = true; if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } unsubscribeHydration(); }; }, []); // 定时保存窗口状态、尺寸与位置 useEffect(() => { const SAVE_INTERVAL_MS = 2000; let cancelled = false; let hydrated = useStore.persist.hasHydrated(); let eventSaveTimer: number | null = null; let boundsRepairTimer: number | null = null; let lastSaved = ''; const saveWindowState = async () => { if (cancelled || !hydrated) { return; } try { const [isFs, isMax] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); // 启动最大化/全屏尚未 settle 时,禁止把状态写回 normal, // 否则下次冷启动会落到默认 1024x768 左上角(Windows 首次打开“只显示一半”)。 if (isStartupWindowRestorePending()) { if (!isFs && !isMax) { return; } clearStartupWindowRestorePending(); } // 保存窗口状态 const store = useStore.getState(); const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal'); if (store.windowState !== newState) { void emitWindowDiagnostic('transition:windowState', { from: store.windowState, to: newState, }); store.setWindowState(newState); } // 只在普通窗口模式下保存尺寸和位置 if (isFs || isMax) return; const [size, pos] = await Promise.all([ safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); if (!size || !pos) return; const w = Math.trunc(Number(size.w || 0)); const h = Math.trunc(Number(size.h || 0)); const x = Math.trunc(Number(pos.x || 0)); const y = Math.trunc(Number(pos.y || 0)); if (w < 400 || h < 300) return; const key = `${w},${h},${x},${y}`; if (key === lastSaved) return; lastSaved = key; if (Math.abs(x) > 5000 || Math.abs(y) > 5000) { void emitWindowDiagnostic('anomaly:windowBounds', { width: w, height: h, x, y }); } store.setWindowBounds({ width: w, height: h, x, y }); } catch (e) { // 静默忽略 } }; const scheduleWindowStateSave = (delayMs = 120) => { if (cancelled || !hydrated) { return; } if (eventSaveTimer !== null) { window.clearTimeout(eventSaveTimer); } eventSaveTimer = window.setTimeout(() => { eventSaveTimer = null; void saveWindowState(); }, delayMs); }; const repairRuntimeWindowBounds = async () => { if (cancelled || !hydrated) { return; } // 启动最大化 settle 期间不要抢跑普通 bounds 校正 if (isStartupWindowRestorePending()) { return; } try { const [isFs, isMax] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); if (isFs || isMax) { return; } const [size, pos] = await Promise.all([ safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); if (!size || !pos) { return; } const currentBounds = { width: Math.trunc(Number(size.w || 0)), height: Math.trunc(Number(size.h || 0)), x: Math.trunc(Number(pos.x || 0)), y: Math.trunc(Number(pos.y || 0)), }; if (currentBounds.width <= 0 || currentBounds.height <= 0) { return; } const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport()); if ( nextBounds.x === currentBounds.x && nextBounds.y === currentBounds.y && nextBounds.width === currentBounds.width && nextBounds.height === currentBounds.height ) { return; } void emitWindowDiagnostic('adjust:runtime-window-bounds', { from: currentBounds, to: nextBounds, }); WindowSetSize(nextBounds.width, nextBounds.height); WindowSetPosition(nextBounds.x, nextBounds.y); lastSaved = `${nextBounds.width},${nextBounds.height},${nextBounds.x},${nextBounds.y}`; useStore.getState().setWindowBounds(nextBounds); window.dispatchEvent(new Event('resize')); } catch { // Wails runtime window APIs are best-effort here. } }; const scheduleWindowBoundsRepair = (delayMs = 80) => { if (cancelled || !hydrated) { return; } if (boundsRepairTimer !== null) { window.clearTimeout(boundsRepairTimer); } boundsRepairTimer = window.setTimeout(() => { boundsRepairTimer = null; void repairRuntimeWindowBounds(); }, delayMs); }; const handleWindowRuntimeChange = () => { scheduleWindowBoundsRepair(); scheduleWindowStateSave(260); }; const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { scheduleWindowBoundsRepair(); scheduleWindowStateSave(260); } }; const handleWindowLifecycleFlush = () => { void saveWindowState(); }; if (hydrated) { scheduleWindowBoundsRepair(360); scheduleWindowStateSave(320); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled || hydrated) { return; } hydrated = true; scheduleWindowBoundsRepair(360); scheduleWindowStateSave(320); }); const timer = window.setInterval(() => { void saveWindowState(); }, SAVE_INTERVAL_MS); window.addEventListener('resize', handleWindowRuntimeChange); window.addEventListener('focus', handleWindowRuntimeChange); window.addEventListener('pageshow', handleWindowRuntimeChange); window.addEventListener('pagehide', handleWindowLifecycleFlush, { capture: true }); window.addEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true }); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (eventSaveTimer !== null) { window.clearTimeout(eventSaveTimer); } if (boundsRepairTimer !== null) { window.clearTimeout(boundsRepairTimer); } window.clearInterval(timer); window.removeEventListener('resize', handleWindowRuntimeChange); window.removeEventListener('focus', handleWindowRuntimeChange); window.removeEventListener('pageshow', handleWindowRuntimeChange); window.removeEventListener('pagehide', handleWindowLifecycleFlush, { capture: true }); window.removeEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true }); document.removeEventListener('visibilitychange', handleVisibilityChange); unsubscribeHydration(); }; }, []); useEffect(() => { if (!isWindowsPlatform()) { return; } let cancelled = false; let inFlight = false; let lastRatio = Number(window.devicePixelRatio) || 1; let lastFixAt = 0; let activationTimer: number | null = null; let resizeTimer: number | null = null; let minimisedSeen = false; let hiddenSeen = document.visibilityState === 'hidden'; const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); const fixWindowScaleIfNeeded = async (reason: WindowScaleFixReason) => { if (cancelled || inFlight) return; const now = Date.now(); if (now - lastFixAt < 700) return; inFlight = true; try { const [isFullscreen, isMaximised] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); // 全屏状态下只广播 resize,避免破坏用户的全屏上下文。 if (isFullscreen) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const size = await safeWindowRuntimeCall(() => WindowGetSize(), null); const width = Math.trunc(Number(size?.w || 0)); const height = Math.trunc(Number(size?.h || 0)); const hasViewportScaleDrift = hasWindowsViewportScaleDrift({ windowWidth: width, innerWidth: window.innerWidth, devicePixelRatio: Number(window.devicePixelRatio) || 1, visualViewportScale: window.visualViewport?.scale, }); const shouldResetWebViewZoom = shouldResetWebViewZoomForScaleFix(reason, hasViewportScaleDrift); if (shouldResetWebViewZoom && !isMaximised) { try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (!res?.success) { console.warn('ResetWebViewZoom unavailable in fixWindowScaleIfNeeded:', res?.message); } } catch (e) { console.warn('ResetWebViewZoom call failed in fixWindowScaleIfNeeded', e); } } if (isMaximised) { if (!shouldToggleMaximisedWindowForScaleFix(reason, hasViewportScaleDrift)) { // restore(任务栏点击恢复后字体异常变大/变糊)的零感知修复路径: // 调 backend App.ResetWebViewZoom 触发 WebView2 ICoreWebView2Controller::put_ZoomFactor(1.0), // 让 WebView2 重算 D2D/DirectWrite 字体度量。该异常不一定表现为 viewport ratio drift, // 所以 restore 场景不能依赖 hasViewportScaleDrift。完全不动窗口、零动画。 // backend 失败(wails 升级破坏反射 / 非 Windows)时回退到 dispatch resize 兜底; // 用户仍可按 Ctrl+Shift+0 手动 toggle 修复。 if (shouldResetWebViewZoom) { try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (!res?.success) { console.warn('ResetWebViewZoom unavailable in fixWindowScaleIfNeeded:', res?.message); } } catch (e) { console.warn('ResetWebViewZoom call failed in fixWindowScaleIfNeeded', e); } } window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } try { WindowUnmaximise(); await wait(96); WindowMaximise(); await wait(96); } catch (e) { console.warn("Wails Window maximise restore unavailable in fixWindowScaleIfNeeded", e); } window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } if (width <= 0 || height <= 0) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } if (!shouldApplyWindowsScaleFix(reason, hasViewportScaleDrift)) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const nudgedWidth = getWindowsScaleFixNudgedWidth(width); try { WindowSetSize(nudgedWidth, height); await wait(28); WindowSetSize(width, height); } catch(e) {} window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); } catch(e) { console.warn("Wails Window APIs unavailable in fixWindowScaleIfNeeded", e); } finally { inFlight = false; } }; const rememberMinimisedState = async (): Promise => { if (cancelled) return false; const isMinimised = await safeWindowRuntimeCall(() => WindowIsMinimised(), false); if (isMinimised) { minimisedSeen = true; } return isMinimised; }; const rememberMinimisedStateSoon = () => { window.setTimeout(() => { if (cancelled) return; void rememberMinimisedState(); }, 120); }; const checkDevicePixelRatio = () => { if (cancelled) return; const currentRatio = Number(window.devicePixelRatio) || 1; if (Math.abs(currentRatio - lastRatio) < 0.02) { return; } lastRatio = currentRatio; if (minimisedSeen || hiddenSeen) { scheduleActivationFix(); return; } void fixWindowScaleIfNeeded('ratio-change'); }; const scheduleDevicePixelRatioCheck = (trigger: WindowsScaleCheckTrigger) => { if (cancelled) return; const delayMs = resolveWindowsScaleCheckDelayMs(trigger); if (delayMs <= 0) { checkDevicePixelRatio(); return; } if (resizeTimer !== null) { window.clearTimeout(resizeTimer); } resizeTimer = window.setTimeout(() => { resizeTimer = null; if (cancelled) return; checkDevicePixelRatio(); }, delayMs); }; const scheduleActivationFix = () => { if (cancelled) return; if (activationTimer !== null) { window.clearTimeout(activationTimer); } const delayMs = (minimisedSeen || hiddenSeen) ? 260 : 80; activationTimer = window.setTimeout(async () => { activationTimer = null; if (cancelled) return; if (await rememberMinimisedState()) { return; } const reason: WindowScaleFixReason = (minimisedSeen || hiddenSeen) ? 'restore' : 'activation'; minimisedSeen = false; hiddenSeen = false; void fixWindowScaleIfNeeded(reason); }, delayMs); }; const handleWindowFocus = () => { if (cancelled) return; scheduleDevicePixelRatioCheck('focus'); scheduleActivationFix(); }; const handleWindowBlur = () => { if (cancelled) return; if (document.visibilityState === 'hidden') { hiddenSeen = true; } rememberMinimisedStateSoon(); }; const handleVisibilityChange = () => { if (cancelled) return; if (document.visibilityState !== 'visible') { hiddenSeen = true; rememberMinimisedStateSoon(); return; } scheduleDevicePixelRatioCheck('visibilitychange'); scheduleActivationFix(); }; const handlePageShow = () => { if (cancelled) return; scheduleDevicePixelRatioCheck('pageshow'); scheduleActivationFix(); }; const handleWindowResize = () => { rememberMinimisedStateSoon(); scheduleDevicePixelRatioCheck('resize'); }; const pollTimer = window.setInterval(() => { void rememberMinimisedState(); checkDevicePixelRatio(); }, 900); // Windows 冷启动:WebView2 首次布局常只铺满左上角一部分,任务栏恢复才会走 restore 修复。 // 这里在启动后主动按 startup 原因做几次轻量 settle,避免用户必须双击任务栏。 // 间隔需大于 fixWindowScaleIfNeeded 的 700ms 节流,确保多次都能真正执行。 const startupLayoutFixTimers = [220, 1000, 1900].map((delayMs) => ( window.setTimeout(() => { if (cancelled) return; void fixWindowScaleIfNeeded('startup'); }, delayMs) )); window.addEventListener('resize', handleWindowResize); window.addEventListener('focus', handleWindowFocus); window.addEventListener('blur', handleWindowBlur); window.addEventListener('pageshow', handlePageShow); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (activationTimer !== null) { window.clearTimeout(activationTimer); } if (resizeTimer !== null) { window.clearTimeout(resizeTimer); } for (const timer of startupLayoutFixTimers) { window.clearTimeout(timer); } window.clearInterval(pollTimer); window.removeEventListener('resize', handleWindowResize); window.removeEventListener('focus', handleWindowFocus); window.removeEventListener('blur', handleWindowBlur); window.removeEventListener('pageshow', handlePageShow); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); const { bgContent, bgMain, floatingLogButtonBgColor, floatingLogButtonBorderColor, floatingLogButtonShadow, floatingLogButtonTextColor, isSidebarCompact, isSidebarNarrow, isSidebarUltraCompact, overlayTheme, renderUtilityModalTitle, sidebarCreateConnectionActionStyle, sidebarHorizontalPadding, sidebarQueryActionStyle, toolCenterContentPanelStyle, toolCenterDetailBodyStyle, toolCenterDetailPanelStyle, toolCenterModalContentStyle, toolCenterModalSplitStyle, toolCenterModalWorkspaceStyle, toolCenterNavPanelStyle, toolCenterNavScrollStyle, toolCenterRowDescriptionStyle, toolCenterRowStyle, toolCenterScrollableListStyle, utilityButtonStyle, utilityModalShellStyle, utilityMutedTextStyle, utilityPanelStyle, } = useAppUtilityStyles({ blurFilter, darkMode, effectiveOpacity, effectiveUiScale, isV2Ui, resolvedAppearance, sidebarWidth, }); const addTab = useStore(state => state.addTab); const activeContext = useStore(state => state.activeContext); const connections = useStore(state => state.connections); const tabs = useStore(state => state.tabs); const activeTabId = useStore(state => state.activeTabId); const setActiveTab = useStore(state => state.setActiveTab); const savedQueries = useStore(state => state.savedQueries); const saveQuery = useStore(state => state.saveQuery); const applicationQuitConfirmRef = useRef<{ destroy: () => void } | null>(null); const applicationQuitHandlingRef = useRef(false); const openSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { setIsSecurityUpdateIntroOpen(false); setSecurityUpdateSettingsFocusTarget(focusTarget); setSecurityUpdateSettingsFocusRequest((current) => current + 1); setIsSecurityUpdateSettingsOpen(true); }, []); const handleOpenSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { openSecurityUpdateSettings(focusTarget); }, [openSecurityUpdateSettings]); const runSecurityUpdateRound = useCallback(async (mode: 'start' | 'retry' | 'restart') => { const backendApp = (window as any).go?.app?.App; const stageText = mode === 'start' ? t('app.security_update.stage.checking_saved_config') : (mode === 'retry' ? t('app.security_update.stage.verifying_result') : t('app.security_update.stage.updating_secure_storage')); const detailsWereOpen = isSecurityUpdateSettingsOpen; setSecurityUpdateProgressStage(stageText); setIsSecurityUpdateProgressOpen(true); setIsSecurityUpdateIntroOpen(false); setIsSecurityUpdateSettingsOpen(false); let nextStatus: SecurityUpdateStatus | null = null; let shouldOpenSettings = false; let refreshSettingsFocus = false; try { if (mode === 'start') { const result = await startSecurityUpdateFromBootstrap({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }); if (result.error) { throw result.error; } nextStatus = normalizeSecurityUpdateStatus(result.status); } else if (mode === 'retry') { if (typeof backendApp?.RetrySecurityUpdateCurrentRound !== 'function') { throw new Error(t('app.security_update.error.capability_unavailable')); } nextStatus = normalizeSecurityUpdateStatus(await backendApp.RetrySecurityUpdateCurrentRound({ migrationId: securityUpdateStatus.migrationId, })); } else { if (typeof backendApp?.RestartSecurityUpdate !== 'function') { throw new Error(t('app.security_update.error.capability_unavailable')); } nextStatus = normalizeSecurityUpdateStatus(await backendApp.RestartSecurityUpdate({ migrationId: securityUpdateStatus.migrationId, sourceType: 'current_app_saved_config', rawPayload: securityUpdateRawPayload ?? '', options: { allowPartial: true, writeBackup: true, }, })); } if (mode !== 'start') { nextStatus = await finalizeSecurityUpdateStatus({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }, nextStatus); } shouldOpenSettings = nextStatus.overallStatus === 'needs_attention' || nextStatus.overallStatus === 'rolled_back'; refreshSettingsFocus = shouldRefreshSecurityUpdateDetailsFocus({ requestedOpen: shouldOpenSettings, wasOpen: detailsWereOpen, }); } catch (err: any) { console.warn('Failed to execute security update round', err); setIsSecurityUpdateProgressOpen(false); if (detailsWereOpen) { setIsSecurityUpdateSettingsOpen(true); } void message.error(err?.message || t('app.security_update.message.not_finished_retry_later')); return; } if (!nextStatus) { setIsSecurityUpdateProgressOpen(false); return; } setIsSecurityUpdateProgressOpen(false); applySecurityUpdateStatus(nextStatus, { openSettings: shouldOpenSettings, refreshFocus: refreshSettingsFocus, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); setIsSecurityUpdateSettingsOpen(false); void message.success(t('app.security_update.message.completed')); } else if (nextStatus.overallStatus === 'needs_attention') { void message.warning(t('app.security_update.message.needs_attention')); } else if (nextStatus.overallStatus === 'rolled_back') { void message.warning(t('app.security_update.message.rolled_back')); } }, [ applySecurityUpdateStatus, isSecurityUpdateSettingsOpen, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, securityUpdateRawPayload, securityUpdateStatus.migrationId, t, ]); const handleStartSecurityUpdate = useCallback(() => { void runSecurityUpdateRound('start'); }, [runSecurityUpdateRound]); const handlePrepareExternalMCPUse = useCallback(async () => { const backendApp = (window as any).go?.app?.App; const result = await prepareSecureConfigForExternalMCP({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }); if (result.error) { throw result.error; } if (!result.status) { return; } const nextStatus = normalizeSecurityUpdateStatus(result.status); const shouldOpenSettings = nextStatus.overallStatus === 'needs_attention' || nextStatus.overallStatus === 'rolled_back'; applySecurityUpdateStatus(nextStatus, { openSettings: shouldOpenSettings, refreshFocus: shouldOpenSettings, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); setIsSecurityUpdateSettingsOpen(false); return; } const hasConnectionIssue = nextStatus.issues.some((issue) => issue.scope === 'connection' && issue.status !== 'updated', ); if (nextStatus.overallStatus === 'rolled_back' || hasConnectionIssue) { throw new Error(t('app.security_update.message.needs_attention')); } if (nextStatus.overallStatus === 'needs_attention') { void message.warning(t('app.security_update.message.needs_attention')); } }, [ applySecurityUpdateStatus, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, t, ]); const handleRetrySecurityUpdate = useCallback(() => { void runSecurityUpdateRound('retry'); }, [runSecurityUpdateRound]); const handleRestartSecurityUpdate = useCallback(() => { void runSecurityUpdateRound('restart'); }, [runSecurityUpdateRound]); const handlePostponeSecurityUpdate = useCallback(async () => { const backendApp = (window as any).go?.app?.App; setIsSecurityUpdateIntroOpen(false); try { if (typeof backendApp?.DismissSecurityUpdateReminder === 'function') { const nextStatus = mergeSecurityUpdateStatusWithLegacySource( await backendApp.DismissSecurityUpdateReminder(), securityUpdateRawPayload, { t }, ); applySecurityUpdateStatus(nextStatus); return; } applySecurityUpdateStatus({ overallStatus: 'postponed', canStart: true, canPostpone: true, summary: securityUpdateStatus.summary, issues: securityUpdateStatus.issues, }); } catch (err: any) { console.warn('Failed to dismiss security update reminder', err); void message.error(err?.message || t('app.security_update.message.postpone_failed')); } }, [ applySecurityUpdateStatus, securityUpdateRawPayload, securityUpdateStatus.issues, securityUpdateStatus.summary, t, ]); const handleSecurityUpdateIssueAction = useCallback((issue: SecurityUpdateIssue) => { const repairEntry = resolveSecurityUpdateRepairEntry(issue, connections, securityUpdateStatus, t); if (repairEntry.type === 'warning') { void message.warning(repairEntry.message); return; } if (repairEntry.type === 'connection') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setEditingConnection(repairEntry.connection); setIsModalOpen(true); return; } if (repairEntry.type === 'proxy') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setIsProxyModalOpen(true); return; } if (repairEntry.type === 'ai') { setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setFocusedAIProviderId(repairEntry.providerId); setActiveSettingsCenterGroupKey('services'); setActiveSettingsCenterPane({ key: 'ai', group: 'services' }); setIsSettingsModalOpen(true); return; } if (repairEntry.type === 'retry') { void runSecurityUpdateRound('retry'); return; } setSecurityUpdateRepairSource(null); openSecurityUpdateSettings(repairEntry.focusTarget); }, [connections, openSecurityUpdateSettings, runSecurityUpdateRound, securityUpdateStatus, t]); const isMacRuntime = runtimePlatform === 'darwin' || (runtimePlatform === '' && /mac/i.test(detectNavigatorPlatform())); const isWindowsRuntime = runtimePlatform === 'windows' || (runtimePlatform === '' && isWindowsPlatform()); const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true; const activeShortcutPlatform = getShortcutPlatform(isMacRuntime); const macWindowDiagnosticsEnabled = shouldEnableMacWindowDiagnostics( isMacRuntime, import.meta.env.DEV, import.meta.env.VITE_GONAVI_ENABLE_MAC_WINDOW_DIAGNOSTICS, ); useEffect(() => { return installGlobalImeCompositionTracking(window, document); }, []); // 启动发现更新时打开设置中心「关于」页(由 useAppUpdateManager 通过 bridge 调用) const updateCenterBridgeRef = useRef<{ open: () => void; close: () => void; isOpen: () => boolean; } | null>(null); const { aboutDisplayVersion, aboutInfo, aboutLoading, aboutUpdateStatus, canShowProgressEntry, changeUpdateChannel, checkForUpdates, downloadUpdate, formatBytes, handleInstallFromProgress, hideUpdateDownloadProgress, isAboutOpen, isBackgroundProgressForLatestUpdate, isLatestUpdateDownloaded, isUpdateChannelLoading, isUpdateChannelSaving, lastUpdateInfo, markUpdateProgressDismissed, muteLatestUpdate, openDownloadedUpdateDirectory, prepareAboutSurface, setIsAboutOpen, showUpdateDownloadProgress, updateChannel, updateDownloadProgress, } = useAppUpdateManager({ runtimeBuildType, t, updateCenterBridgeRef, }); const [aboutLastCheckedAt, setAboutLastCheckedAt] = useState(''); useEffect(() => { if (!lastUpdateInfo) { return; } setAboutLastCheckedAt(formatAboutCheckedAt(new Date())); }, [ lastUpdateInfo?.channel, lastUpdateInfo?.currentVersion, lastUpdateInfo?.hasUpdate, lastUpdateInfo?.latestVersion, ]); const emitWindowDiagnostic = useCallback(async (stage: string, extra: Record = {}) => { if (!macWindowDiagnosticsEnabled) { return; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.LogWindowDiagnostic !== 'function') { return; } try { const [isFullscreen, isMaximised, isMinimised, isNormal, size, position] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), safeWindowRuntimeCall(() => WindowIsMinimised(), false), safeWindowRuntimeCall(() => WindowIsNormal(), false), safeWindowRuntimeCall(() => WindowGetSize(), null), safeWindowRuntimeCall(() => WindowGetPosition(), null), ]); const payload = { seq: ++windowDiagSequenceRef.current, ts: new Date().toISOString(), stage, nativeControls: useNativeMacWindowControls, documentVisible: document.visibilityState, documentHasFocus: document.hasFocus(), devicePixelRatio: Number(window.devicePixelRatio) || 1, windowState: { isFullscreen, isMaximised, isMinimised, isNormal, }, size: size ? { w: Math.trunc(Number(size.w || 0)), h: Math.trunc(Number(size.h || 0)) } : null, position: position ? { x: Math.trunc(Number(position.x || 0)), y: Math.trunc(Number(position.y || 0)) } : null, extra, }; const signature = JSON.stringify({ stage, nativeControls: payload.nativeControls, visible: payload.documentVisible, focus: payload.documentHasFocus, state: payload.windowState, size: payload.size, position: payload.position, extra, }); const now = Date.now(); if (signature === windowDiagLastSignatureRef.current && now-windowDiagLastAtRef.current < 250) { return; } windowDiagLastSignatureRef.current = signature; windowDiagLastAtRef.current = now; await backendApp.LogWindowDiagnostic(stage, JSON.stringify(payload)); } catch (error) { console.warn('Failed to emit window diagnostic', error); } }, [macWindowDiagnosticsEnabled, useNativeMacWindowControls]); useEffect(() => { if (!isStoreHydrated || !isMacRuntime) { return; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.SetMacNativeWindowControls !== 'function') { return; } void safeWindowRuntimeCall(() => SetMacNativeWindowControls(useNativeMacWindowControls), undefined); }, [isMacRuntime, isStoreHydrated, useNativeMacWindowControls]); useEffect(() => { if (!macWindowDiagnosticsEnabled) { return; } let cancelled = false; let pollTimer: number | null = null; let burstTimer: number | null = null; const stopBurst = () => { if (pollTimer !== null) { window.clearInterval(pollTimer); pollTimer = null; } if (burstTimer !== null) { window.clearTimeout(burstTimer); burstTimer = null; } }; const startBurst = (reason: string, extra: Record = {}) => { if (cancelled) { return; } void emitWindowDiagnostic(`burst:start:${reason}`, extra); if (pollTimer === null) { pollTimer = window.setInterval(() => { void emitWindowDiagnostic(`burst:tick:${reason}`); }, 250); } if (burstTimer !== null) { window.clearTimeout(burstTimer); } burstTimer = window.setTimeout(() => { stopBurst(); void emitWindowDiagnostic(`burst:stop:${reason}`); }, 6000); }; const handleFocus = () => { void emitWindowDiagnostic('event:focus'); }; const handleBlur = () => { void emitWindowDiagnostic('event:blur'); }; const handleResize = () => { void emitWindowDiagnostic('event:resize'); }; const handleVisibilityChange = () => { void emitWindowDiagnostic('event:visibilitychange', { visibility: document.visibilityState }); }; const handleEditableKeydown = (event: KeyboardEvent) => { if (!isEditableElement(event.target)) { return; } const key = String(event.key || ''); const maybeFullscreenKey = key === 'Escape' || key.toLowerCase() === 'f' || key === 'Process'; const hasModifier = event.ctrlKey || event.metaKey || event.altKey; startBurst('editable-keydown', { key, code: String(event.code || ''), ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, shiftKey: event.shiftKey, maybeFullscreenKey, hasModifier, }); }; const handleCompositionStart = () => { startBurst('compositionstart'); }; const handleCompositionEnd = () => { startBurst('compositionend'); }; void emitWindowDiagnostic('session:start'); window.addEventListener('focus', handleFocus); window.addEventListener('blur', handleBlur); window.addEventListener('resize', handleResize); window.addEventListener('keydown', handleEditableKeydown, true); window.addEventListener('compositionstart', handleCompositionStart, true); window.addEventListener('compositionend', handleCompositionEnd, true); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; stopBurst(); window.removeEventListener('focus', handleFocus); window.removeEventListener('blur', handleBlur); window.removeEventListener('resize', handleResize); window.removeEventListener('keydown', handleEditableKeydown, true); window.removeEventListener('compositionstart', handleCompositionStart, true); window.removeEventListener('compositionend', handleCompositionEnd, true); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [emitWindowDiagnostic, macWindowDiagnosticsEnabled]); const handleNewQuery = useCallback(() => { let connId = ''; let db = ''; // Priority: Active Tab Context (if connection still valid) > Sidebar Selection (activeContext) if (activeTabId) { const currentTab = tabs.find(t => t.id === activeTabId); if (currentTab && currentTab.connectionId && connections.some(c => c.id === currentTab.connectionId)) { connId = currentTab.connectionId; db = currentTab.dbName || ''; } } // Fallback: Sidebar selection context (only if connection still valid) if (!connId && activeContext?.connectionId && connections.some(c => c.id === activeContext.connectionId)) { connId = activeContext.connectionId; db = activeContext.dbName || ''; } addTab({ id: `query-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, title: t('query.new'), type: 'query', connectionId: connId, dbName: db, query: '' }); }, [activeTabId, tabs, connections, activeContext, addTab, t]); const switchActiveTabByOffset = useCallback((offset: 1 | -1) => { if (tabs.length < 2) return; const activeIndex = tabs.findIndex(tab => tab.id === activeTabId); const baseIndex = activeIndex >= 0 ? activeIndex : 0; const nextIndex = (baseIndex + offset + tabs.length) % tabs.length; setActiveTab(tabs[nextIndex].id); }, [activeTabId, setActiveTab, tabs]); const resetApplicationQuitRequest = useCallback(() => { applicationQuitHandlingRef.current = false; applicationQuitConfirmRef.current = null; void CancelApplicationQuit(); }, []); const forceQuitApplication = useCallback(async () => { const res = await ForceQuitApplication(); if (res && res.success === false) { throw new Error(res.message || t('common.unknown')); } }, [t]); const handleApplicationQuitRequest = useCallback(async () => { if (applicationQuitHandlingRef.current) { return; } applicationQuitHandlingRef.current = true; let targets; try { targets = await collectApplicationQuitUnsavedSQLTargets(tabs, savedQueries); } catch (error) { resetApplicationQuitRequest(); message.error(t('app.quit.unsaved_sql.inspect_failed', { detail: error instanceof Error ? error.message : String(error), })); return; } if (targets.length === 0) { try { await forceQuitApplication(); } catch (error) { resetApplicationQuitRequest(); message.error(t('app.quit.message.quit_failed', { detail: error instanceof Error ? error.message : String(error), })); } return; } const label = buildApplicationQuitUnsavedSQLLabel(targets); let destroyConfirm: (() => void) | null = null; const confirmRef = Modal.confirm({ title: t('app.quit.unsaved_sql.title'), content: t(targets.length === 1 ? 'app.quit.unsaved_sql.content_single' : 'app.quit.unsaved_sql.content_multiple', { label }), okText: t('app.quit.unsaved_sql.save_exit'), cancelText: t('app.quit.unsaved_sql.cancel'), closable: true, maskClosable: false, okButtonProps: { danger: true, type: 'primary' }, footer: (_, { OkBtn, CancelBtn }) => ( <> ), onCancel: () => { resetApplicationQuitRequest(); }, onOk: async () => { try { await saveApplicationQuitUnsavedSQLTargets(targets, saveQuery); message.success(t('app.quit.unsaved_sql.saved')); await forceQuitApplication(); } catch (error) { resetApplicationQuitRequest(); message.error(t('app.quit.unsaved_sql.save_failed_cancel_exit', { detail: error instanceof Error ? error.message : String(error), })); throw error; } }, }); destroyConfirm = confirmRef.destroy; applicationQuitConfirmRef.current = confirmRef; }, [forceQuitApplication, resetApplicationQuitRequest, saveQuery, savedQueries, t, tabs]); useEffect(() => { const offBeforeClose = EventsOn('app:before-close-request', () => { void handleApplicationQuitRequest(); }); return () => { offBeforeClose(); }; }, [handleApplicationQuitRequest]); const closeConnectionPackageDialog = useCallback(() => { setConnectionPackageDialog(createClosedConnectionPackageDialogState()); setPendingConnectionImportPayload(null); setToolCenterBackGroupKey(null); setActiveToolCenterPane((current) => (current?.key === 'connection-package' ? null : current)); }, []); const refreshConnectionsAfterImport = useCallback(async (importedViews: SavedConnection[]) => { const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.GetSavedConnections === 'function') { let latestConnections: unknown; try { latestConnections = await GetSavedConnections(); } catch (error) { const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? t('app.connection_package.message.import_failed_with_error', { error: detail }) : t('app.connection_package.message.import_failed'), ); } if (!Array.isArray(latestConnections)) { throw new Error(t('app.connection_package.error.refresh_failed_no_connections')); } replaceConnections(latestConnections as SavedConnection[]); return; } const latestConnections = useStore.getState().connections; replaceConnections(mergeSavedConnections(latestConnections, importedViews)); }, [replaceConnections]); const importConnectionsPayload = useCallback(async (raw: string, password: string) => { const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.ImportConnectionsPayload !== 'function') { throw new Error(t('app.connection_package.error.import_capability_unavailable')); } let importedViews: unknown; try { importedViews = await backendApp.ImportConnectionsPayload(raw, password); } catch (error) { if (isConnectionPackagePasswordRequiredError(error)) { throw error; } const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? t('app.connection_package.message.import_failed_with_error', { error: detail }) : t('app.connection_package.message.import_failed'), ); } if (!Array.isArray(importedViews)) { throw new Error(t('app.connection_package.error.import_no_connections')); } await refreshConnectionsAfterImport(importedViews as SavedConnection[]); return importedViews as SavedConnection[]; }, [refreshConnectionsAfterImport, t]); const handleImportConnections = async (sourceGroup?: ToolCenterGroupKey) => { setToolCenterBackGroupKey(sourceGroup ?? null); const res = await (window as any).go.app.App.ImportConfigFile(); if (!res.success) { if (res.message !== "已取消") { void message.error(t('app.connection_package.message.import_failed_with_error', { error: res.message })); } return; } const raw = typeof res.data === 'string' ? res.data : String(res.data ?? ''); const importKind = detectConnectionImportKind(raw); if (importKind === 'invalid') { void message.error(t('app.connection_package.message.unsupported_file_format')); return; } try { setPendingConnectionImportPayload(null); const importedViews = await importConnectionsPayload(raw, ''); if ((importKind === 'mysql-workbench-xml' || importKind === 'navicat-ncx') && importedViews.some(v => !v.hasPrimaryPassword)) { void message.warning(t('app.connection_package.message.imported_with_missing_passwords', { count: importedViews.length })); } else { void message.success(t('app.connection_package.message.imported_connections', { count: importedViews.length })); } } catch (e: any) { if (isConnectionPackagePasswordRequiredError(e)) { if (sourceGroup) { setToolCenterBackGroupKey(sourceGroup); setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); } setPendingConnectionImportPayload(raw); setConnectionPackageDialog({ open: true, mode: 'import', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); return; } void message.error(e?.message || t('app.connection_package.message.import_failed')); } }; const handleExportConnections = async (sourceGroup?: ToolCenterGroupKey) => { if (connections.length === 0) { void message.warning(t('app.connection_package.message.no_connections_to_export')); return; } setToolCenterBackGroupKey(sourceGroup ?? null); if (sourceGroup) { setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); } setConnectionPackageDialog({ open: true, mode: 'export', includeSecrets: true, useFilePassword: false, password: '', error: '', confirmLoading: false, }); }; const handleConfirmConnectionPackageDialog = async () => { const backendApp = (window as any).go?.app?.App; const password = normalizeConnectionPackagePassword(connectionPackageDialog.password); if (connectionPackageDialog.mode === 'import' && !password) { setConnectionPackageDialog((current) => ({ ...current, error: t('app.connection_package.error.restore_password_required'), })); return; } if ( connectionPackageDialog.mode === 'export' && connectionPackageDialog.includeSecrets && connectionPackageDialog.useFilePassword && !password ) { setConnectionPackageDialog((current) => ({ ...current, error: t('app.connection_package.error.file_password_required'), })); return; } setConnectionPackageDialog((current) => ({ ...current, password: ( current.mode === 'export' && (!current.includeSecrets || !current.useFilePassword) ) ? '' : password, error: '', confirmLoading: true, })); try { if (connectionPackageDialog.mode === 'export') { if (typeof backendApp?.ExportConnectionsPackage !== 'function') { throw new Error(t('app.connection_package.error.export_capability_unavailable')); } let res: unknown; try { res = await backendApp.ExportConnectionsPackage({ includeSecrets: connectionPackageDialog.includeSecrets, filePassword: ( connectionPackageDialog.includeSecrets && connectionPackageDialog.useFilePassword ) ? password : '', }); } catch (error) { const detail = error instanceof Error ? error.message : String(error ?? '').trim(); throw new Error( detail ? `${t('app.connection_package.message.export_failed')}: ${detail}` : t('app.connection_package.message.export_failed'), ); } const exportResult = resolveConnectionPackageExportResult(connectionPackageDialog, res); if (exportResult.kind === 'canceled') { setConnectionPackageDialog(exportResult.nextDialog); return; } if (exportResult.kind === 'failed') { throw new Error(exportResult.error); } closeConnectionPackageDialog(); void message.success(t('app.connection_package.message.export_succeeded')); return; } if (!pendingConnectionImportPayload) { throw new Error(t('app.connection_package.error.missing_import_payload')); } const importedViews = await importConnectionsPayload(pendingConnectionImportPayload, password); closeConnectionPackageDialog(); void message.success(t('app.connection_package.message.imported_connections', { count: importedViews.length })); } catch (e: any) { setConnectionPackageDialog((current) => ({ ...current, confirmLoading: false, error: e?.message || t( current.mode === 'export' ? 'app.connection_package.message.export_failed' : 'app.connection_package.message.import_failed', ), })); } }; const [isToolsModalOpen, setIsToolsModalOpen] = useState(false); const [activeToolCenterGroupKey, setActiveToolCenterGroupKey] = useState('config'); const [toolCenterBackGroupKey, setToolCenterBackGroupKey] = useState(null); const [activeToolCenterPane, setActiveToolCenterPane] = useState(null); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences'); const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); type ThemeSettingsSection = 'theme' | 'appearance' | 'workspace'; const THEME_SETTINGS_SECTION_STORAGE_KEY = 'gonavi.themeSettingsSection'; const sanitizeThemeSettingsSection = useCallback((value: unknown): ThemeSettingsSection => { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'appearance' || normalized === 'workspace') { return normalized; } return 'theme'; }, []); const [themeModalSection, setThemeModalSection] = useState(() => { try { return sanitizeThemeSettingsSection(window.localStorage.getItem(THEME_SETTINGS_SECTION_STORAGE_KEY)); } catch { return 'theme'; } }); useEffect(() => { try { window.localStorage.setItem(THEME_SETTINGS_SECTION_STORAGE_KEY, themeModalSection); } catch { // ignore persistence failures } }, [themeModalSection]); const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false); const [capturingShortcutAction, setCapturingShortcutAction] = useState(null); const tabDisplaySettingsPanelRef = useRef(null); const [tabDisplaySettingsFocusRequest, setTabDisplaySettingsFocusRequest] = useState(0); const isThemeSettingsPaneOpen = activeSettingsCenterPane?.key === 'theme'; useEffect(() => { const shouldLoadInstalledFonts = runtimePlatform === 'linux' || ((isThemeModalOpen || isThemeSettingsPaneOpen) && themeModalSection === 'appearance'); if (!shouldLoadInstalledFonts) { return; } if (hasLoadedInstalledFontsRef.current || isFontFamiliesLoading) { return; } let cancelled = false; hasLoadedInstalledFontsRef.current = true; setIsFontFamiliesLoading(true); setFontFamiliesLoadError(null); ListInstalledFontFamilies() .then((result) => { if (cancelled) { return; } if (!result?.success) { throw new Error(String(result?.message || t('app.theme.font_family.load_failed'))); } const nextFonts = Array.isArray(result?.data) ? result.data .map((item) => ({ family: sanitizeFontFamilyInput((item as InstalledFontFamily | Record)?.family) || '', path: typeof (item as InstalledFontFamily | Record)?.path === 'string' ? String((item as InstalledFontFamily | Record).path) : undefined, })) .filter((item) => item.family) : EMPTY_INSTALLED_FONT_FAMILIES; setInstalledFontFamilies(nextFonts); }) .catch((error) => { if (cancelled) { return; } hasLoadedInstalledFontsRef.current = false; setFontFamiliesLoadError(String(error instanceof Error ? error.message : error || t('app.theme.font_family.load_failed'))); }) .finally(() => { if (!cancelled) { setIsFontFamiliesLoading(false); } }); return () => { cancelled = true; }; }, [isThemeModalOpen, isThemeSettingsPaneOpen, runtimePlatform, t, themeModalSection]); useEffect(() => { if ((!isThemeModalOpen && !isThemeSettingsPaneOpen) || themeModalSection !== 'workspace' || tabDisplaySettingsFocusRequest === 0) { return; } const timer = window.setTimeout(() => { tabDisplaySettingsPanelRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' }); }, 80); return () => window.clearTimeout(timer); }, [isThemeModalOpen, isThemeSettingsPaneOpen, themeModalSection, tabDisplaySettingsFocusRequest]); const shortcutConflictMap = useMemo(() => { const map: Partial> = {}; for (const action of SHORTCUT_ACTION_ORDER) { const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled || !binding.combo) continue; const conflicts = findReservedConflictsForAction( action, normalizeShortcutCombo(binding.combo), activeShortcutPlatform, ); if (conflicts.length > 0) { map[action] = conflicts; } } return map; }, [activeShortcutPlatform, language, shortcutOptions]); const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); const [proxyDraft, setProxyDraft] = useState(() => createGlobalProxyComparableDraft(globalProxy)); const [proxyDraftClearPassword, setProxyDraftClearPassword] = useState(false); const [proxyApplying, setProxyApplying] = useState(false); const [proxyTestUrl, setProxyTestUrl] = useState(DEFAULT_GLOBAL_PROXY_TEST_URL); const [proxyTesting, setProxyTesting] = useState(false); const [proxyTestResult, setProxyTestResult] = useState(null); const [isDataRootModalOpen, setIsDataRootModalOpen] = useState(false); const [dataRootInfo, setDataRootInfo] = useState(null); const [selectedDataRootPath, setSelectedDataRootPath] = useState(''); const [dataRootLoading, setDataRootLoading] = useState(false); const [dataRootApplying, setDataRootApplying] = useState(false); const aiEntryPlacement = resolveAIEntryPlacement(); const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible); const aiPanelOverlayActive = aiPanelVisible && shouldOverlayAIPanel({ isV2Ui, viewportWidth, sidebarWidth, panelWidth: DEFAULT_AI_PANEL_WIDTH, }); const aiPanelRenderWidth = aiPanelOverlayActive ? resolveOverlayAIPanelWidth({ viewportWidth, sidebarWidth, panelWidth: DEFAULT_AI_PANEL_WIDTH, }) : DEFAULT_AI_PANEL_WIDTH; const appliedGlobalProxyDraft = useMemo(() => ( createGlobalProxyComparableDraft(globalProxy) ), [ globalProxy.enabled, globalProxy.type, globalProxy.host, globalProxy.port, globalProxy.user, globalProxy.password, globalProxy.hasPassword, ]); const proxyDraftHost = String(proxyDraft.host || '').trim(); const proxyDraftUser = String(proxyDraft.user || '').trim(); const proxyDraftPort = Number(proxyDraft.port); const proxyDraftPortValid = Number.isFinite(proxyDraftPort) && proxyDraftPort > 0 && proxyDraftPort <= 65535; const proxyDraftValid = !proxyDraft.enabled || (proxyDraftHost !== '' && proxyDraftPortValid); const proxyDraftDirty = proxyDraftClearPassword || !areGlobalProxyDraftsEqual(proxyDraft, appliedGlobalProxyDraft); const proxyPanelOpen = isProxyModalOpen || activeSettingsCenterPane?.key === 'proxy'; const proxyPanelWasOpenRef = useRef(false); const proxyStatusTone = proxyDraft.enabled ? (proxyDraftValid ? 'success' : 'warning') : 'info'; const proxyStatusTitle = proxyDraft.enabled ? (proxyDraftValid ? t('app.proxy.status.enabled') : t('app.proxy.status.incomplete')) : t('app.proxy.status.disabled'); const proxyStatusDescription = proxyDraft.enabled && proxyDraftValid ? t('app.proxy.status.enabled_description', { type: proxyDraft.type.toUpperCase(), endpoint: `${proxyDraftHost}:${proxyDraftPort}`, }) : (proxyDraft.enabled ? t('app.proxy.status.incomplete_description') : t('app.proxy.status.disabled_description')); const proxyPresetItems = useMemo(() => ([ { key: 'clash-mixed', label: t('app.proxy.preset.clash_mixed'), type: 'socks5' as const, host: '127.0.0.1', port: 7890 }, { key: 'socks5-local', label: t('app.proxy.preset.socks5_local'), type: 'socks5' as const, host: '127.0.0.1', port: 1080 }, { key: 'http-local', label: t('app.proxy.preset.http_local'), type: 'http' as const, host: '127.0.0.1', port: 8080 }, ]), [t]); const proxyTestPresetItems = useMemo(() => ([ { key: 'github-api', label: t('app.proxy.test.preset.github_api'), url: 'https://api.github.com/' }, { key: 'github-release', label: t('app.proxy.test.preset.github_release'), url: 'https://github.com/Syngnat/GoNavi/releases/latest' }, { key: 'go-module-proxy', label: t('app.proxy.test.preset.go_module_proxy'), url: 'https://proxy.golang.org/' }, { key: 'baidu', label: t('app.proxy.test.preset.baidu'), url: 'https://www.baidu.com/' }, ]), [t]); const proxyTestUrlTrimmed = String(proxyTestUrl || '').trim(); const proxyCanTest = proxyDraft.enabled && proxyDraftValid && proxyTestUrlTrimmed !== '' && !proxyTesting; useEffect(() => { if (!proxyPanelOpen) { proxyPanelWasOpenRef.current = false; return; } if (proxyPanelWasOpenRef.current) { return; } proxyPanelWasOpenRef.current = true; setProxyDraft(appliedGlobalProxyDraft); setProxyDraftClearPassword(false); }, [appliedGlobalProxyDraft, proxyPanelOpen]); useEffect(() => { setProxyTestResult(null); }, [ proxyDraft.enabled, proxyDraft.type, proxyDraft.host, proxyDraft.port, proxyDraft.user, proxyDraft.password, proxyDraftClearPassword, proxyTestUrlTrimmed, ]); const resetProxyDraftToCurrent = useCallback(() => { setProxyDraft(appliedGlobalProxyDraft); setProxyDraftClearPassword(false); }, [appliedGlobalProxyDraft]); const updateProxyDraftType = useCallback((type: GlobalProxyConfig['type']) => { setProxyDraft((current) => { const currentPort = Number(current.port); const previousDefault = getGlobalProxyDefaultPort(current.type); const shouldSwitchPort = !Number.isFinite(currentPort) || currentPort === previousDefault; return { ...current, type, port: shouldSwitchPort ? getGlobalProxyDefaultPort(type) : current.port, }; }); }, []); const applyProxyPreset = useCallback((preset: { type: GlobalProxyConfig['type']; host: string; port: number }) => { setProxyDraft((current) => ({ ...current, enabled: true, type: preset.type, host: preset.host, port: preset.port, })); }, []); const handleTestGlobalProxyDraft = useCallback(async () => { if (!proxyDraft.enabled) { void message.warning(t('app.proxy.test.message.enable_first')); return; } if (!proxyDraftValid) { void message.warning(t('app.proxy.message.invalid_enabled')); return; } if (proxyTestUrlTrimmed === '') { void message.warning(t('app.proxy.test.message.url_required')); return; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.TestGlobalProxyConnection !== 'function') { void message.error(t('app.proxy.test.message.unavailable')); return; } setProxyTesting(true); try { const res = await backendApp.TestGlobalProxyConnection({ proxy: toSaveGlobalProxyInput({ ...proxyDraft, host: proxyDraftHost, user: proxyDraftUser, port: proxyDraftPortValid ? proxyDraftPort : getGlobalProxyDefaultPort(proxyDraft.type), clearPassword: proxyDraftClearPassword, }), url: proxyTestUrlTrimmed, timeoutSeconds: 8, }); const data = (res?.data || {}) as Partial; const statusCode = Number(data.statusCode); setProxyTestResult({ success: res?.success === true, message: res?.message || t('common.unknown'), url: data.url || proxyTestUrlTrimmed, finalUrl: data.finalUrl, statusCode: Number.isFinite(statusCode) ? statusCode : undefined, durationMs: typeof data.durationMs === 'number' ? data.durationMs : undefined, viaProxy: data.viaProxy === true, }); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err || t('common.unknown')); setProxyTestResult({ success: false, message: errMsg, url: proxyTestUrlTrimmed, }); } finally { setProxyTesting(false); } }, [ proxyDraft, proxyDraftClearPassword, proxyDraftHost, proxyDraftPort, proxyDraftPortValid, proxyDraftUser, proxyDraftValid, proxyTestUrlTrimmed, t, ]); const handleApplyGlobalProxyDraft = useCallback(async () => { if (!proxyDraftValid) { void message.warning({ content: t('app.proxy.message.invalid_enabled'), key: 'global-proxy-invalid', }); return; } void message.destroy('global-proxy-invalid'); const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.SaveGlobalProxy !== 'function') { void message.error({ content: t('app.proxy.message.save_failed', { error: t('common.unknown') }), key: 'global-proxy-sync-error', }); return; } const saveInput = toSaveGlobalProxyInput({ ...proxyDraft, host: proxyDraftHost, user: proxyDraftUser, port: proxyDraftPortValid ? proxyDraftPort : getGlobalProxyDefaultPort(proxyDraft.type), clearPassword: proxyDraftClearPassword, }); setProxyApplying(true); try { const saved = await backendApp.SaveGlobalProxy(saveInput); const nextDraft = createGlobalProxyComparableDraft(saved || saveInput); replaceGlobalProxy(nextDraft); setProxyDraft(nextDraft); setProxyDraftClearPassword(false); void message.success({ content: t('app.proxy.message.config_applied'), key: 'global-proxy-applied', }); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err || t('common.unknown')); void message.error({ content: t('app.proxy.message.save_failed', { error: errMsg }), key: 'global-proxy-sync-error', }); } finally { setProxyApplying(false); } }, [ proxyDraft, proxyDraftClearPassword, proxyDraftHost, proxyDraftPort, proxyDraftPortValid, proxyDraftUser, proxyDraftValid, replaceGlobalProxy, t, ]); const legacyAiEdgeHandleDockStyle = useMemo( () => resolveLegacyAIEdgeHandleDockStyle(legacyAiEdgeHandleAttachment), [legacyAiEdgeHandleAttachment], ); const legacyAiEdgeHandleStyle = useMemo(() => ( resolveLegacyAIEdgeHandleStyle({ darkMode, aiPanelVisible, effectiveUiScale, }) ), [aiPanelVisible, darkMode, effectiveUiScale]); const handleOpenToolsModal = useCallback((group: ToolCenterGroupKey = 'config') => { setToolCenterBackGroupKey(null); setActiveToolCenterPane(null); setActiveToolCenterGroupKey(group); setIsToolsModalOpen(true); }, []); const handleOpenSettingsModal = useCallback((group: SettingsCenterGroupKey = 'preferences') => { setActiveSettingsCenterGroupKey(group); setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group)); setIsSettingsModalOpen(true); }, []); const handleOpenSettingsCenterPane = useCallback((group: SettingsCenterGroupKey, key: SettingsCenterPaneKey) => { setActiveSettingsCenterGroupKey(group); setActiveSettingsCenterPane({ key, group }); setIsSettingsModalOpen(true); }, []); const finalizeSecurityRepairReturnFromAISettings = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setFocusedAIProviderId(undefined); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); const handleBackFromSettingsCenterPane = useCallback(() => { const leavingAI = activeSettingsCenterPane?.key === 'ai'; const returnGroup = activeSettingsCenterPane?.group ?? activeSettingsCenterGroupKey; setActiveSettingsCenterGroupKey(returnGroup); setActiveSettingsCenterPane(null); if (leavingAI) { finalizeSecurityRepairReturnFromAISettings(); } }, [ activeSettingsCenterGroupKey, activeSettingsCenterPane?.group, activeSettingsCenterPane?.key, finalizeSecurityRepairReturnFromAISettings, ]); const handleCancelSettingsCenterPane = useCallback(() => { const leavingAI = activeSettingsCenterPane?.key === 'ai'; setActiveSettingsCenterPane(null); setIsSettingsModalOpen(false); if (leavingAI) { finalizeSecurityRepairReturnFromAISettings(); } }, [activeSettingsCenterPane?.key, finalizeSecurityRepairReturnFromAISettings]); const isSettingsAboutPaneOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'about-go-navi'; const isSettingsAboutPaneOpenRef = useRef(false); useEffect(() => { isSettingsAboutPaneOpenRef.current = isSettingsAboutPaneOpen; }, [isSettingsAboutPaneOpen]); useEffect(() => { updateCenterBridgeRef.current = { open: () => { handleOpenSettingsCenterPane('about', 'about-go-navi'); }, close: () => { setActiveSettingsCenterPane(null); setIsSettingsModalOpen(false); }, isOpen: () => isSettingsAboutPaneOpenRef.current, }; return () => { updateCenterBridgeRef.current = null; }; }, [handleOpenSettingsCenterPane]); useEffect(() => { if (!isSettingsAboutPaneOpen) { return; } prepareAboutSurface(); }, [isSettingsAboutPaneOpen, prepareAboutSurface]); const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => { setToolCenterBackGroupKey(group); setActiveToolCenterGroupKey(group); setActiveToolCenterPane({ key, group }); setIsToolsModalOpen(true); }, []); const handleReturnToToolCenter = useCallback((closeChild?: () => void) => { const returnGroup = toolCenterBackGroupKey ?? 'config'; closeChild?.(); setToolCenterBackGroupKey(null); setActiveToolCenterGroupKey(returnGroup); setActiveToolCenterPane(null); setIsToolsModalOpen(true); }, [toolCenterBackGroupKey]); const sidebarUtilityItems = useMemo(() => { const itemMap = { tools: { key: 'tools', title: t('app.sidebar.tools'), icon: , onClick: () => handleOpenToolsModal(), }, settings: { key: 'settings', title: t('app.sidebar.settings'), icon: , onClick: () => handleOpenSettingsModal(), }, } as const; return SIDEBAR_UTILITY_ITEM_KEYS.map((key) => itemMap[key]); }, [handleOpenSettingsModal, handleOpenToolsModal, t]); const handleFocusSidebarSearch = useCallback(() => { window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); }, []); const renderLegacyAIEdgeHandle = () => ( ); const loadDataRootInfo = useCallback(async () => { setDataRootLoading(true); try { const res = await GetDataRootDirectoryInfo(); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.load_failed')); } const data = (res?.data || {}) as any; setDataRootInfo(data); setSelectedDataRootPath(String(data.path || '')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.load_failed_with_error', { error: errMsg })); } finally { setDataRootLoading(false); } }, [t]); useEffect(() => { if (!isDataRootModalOpen && activeToolCenterPane?.key !== 'data-root') { return; } void loadDataRootInfo(); }, [activeToolCenterPane?.key, isDataRootModalOpen, loadDataRootInfo]); const handleSelectDataRoot = useCallback(async () => { try { const res = await SelectDataRootDirectory(selectedDataRootPath || dataRootInfo?.path || ''); if (!res?.success) { if (String(res?.message || '') !== '已取消') { throw new Error(res?.message || t('app.data_root.message.select_failed')); } return; } const data = (res?.data || {}) as any; setSelectedDataRootPath(String(data.path || '')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.select_failed_with_error', { error: errMsg })); } }, [dataRootInfo?.path, selectedDataRootPath, t]); const handleApplyDataRoot = useCallback(async (migrate: boolean, useDefaultPath = false) => { const nextPath = useDefaultPath ? String(dataRootInfo?.defaultPath || '') : String(selectedDataRootPath || '').trim(); if (!nextPath) { void message.warning(t('app.data_root.message.select_valid_first')); return; } setDataRootApplying(true); try { const res = await ApplyDataRootDirectory(nextPath, migrate); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.apply_failed')); } const data = (res?.data || {}) as any; setDataRootInfo(data); setSelectedDataRootPath(String(data.path || nextPath)); void message.success(res?.message || t('app.data_root.message.updated')); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.apply_failed_with_error', { error: errMsg })); } finally { setDataRootApplying(false); } }, [dataRootInfo?.defaultPath, selectedDataRootPath, t]); const handleOpenDataRoot = useCallback(async () => { try { const res = await OpenDataRootDirectory(); if (!res?.success) { throw new Error(res?.message || t('app.data_root.message.open_failed')); } } catch (error) { const errMsg = error instanceof Error ? error.message : String(error || t('common.unknown')); void message.error(t('app.data_root.message.open_failed_with_error', { error: errMsg })); } }, [t]); const { handleCloseLogPanel: handleCloseAppLogPanel, handleLogResizeStart, handleToggleLogPanel: toggleAppLogPanel, isLogPanelOpen, logGhostRef, logPanelHeight, } = useAppLogPanelResize(); const handleToggleLogPanel = useCallback(() => { if (isV2Ui) { window.dispatchEvent(new CustomEvent('gonavi:show-sql-execution-log', { detail: { mode: 'open' } })); return; } toggleAppLogPanel(); }, [isV2Ui, toggleAppLogPanel]); const handleCloseLogPanel = useCallback(() => { handleCloseAppLogPanel(); }, [handleCloseAppLogPanel]); const handleCreateConnection = useCallback(() => { setSecurityUpdateRepairSource(null); setEditingConnection(null); setIsConnectionModalMounted(true); setIsModalOpen(true); }, []); const handleEditConnection = useCallback((conn: SavedConnection) => { setSecurityUpdateRepairSource(null); setIsConnectionModalMounted(true); void (async () => { const backendApp = (window as any).go?.app?.App; let nextConnection = conn; if (typeof backendApp?.GetEditableSavedConnection === 'function') { try { const editableConnection = await backendApp.GetEditableSavedConnection(conn.id); if (editableConnection) { nextConnection = editableConnection; } } catch (error: any) { const errorMessage = error?.message; const detail = ( typeof errorMessage === 'string' ? errorMessage : ( typeof errorMessage === 'number' || typeof errorMessage === 'boolean' ? String(errorMessage) : String(error ?? '') ) ).trim(); void message.warning( detail ? t('app.connection.message.editable_load_failed_with_detail', { detail }) : t('app.connection.message.editable_load_failed') ); } } setEditingConnection(nextConnection); setIsModalOpen(true); })(); }, [t]); useEffect(() => { if (connectionModalWarmupDoneRef.current) { return; } connectionModalWarmupDoneRef.current = true; const warmup = () => setIsConnectionModalMounted(true); if (typeof window === 'undefined') { warmup(); return; } if (typeof window.requestIdleCallback === 'function') { const idleId = window.requestIdleCallback(() => warmup(), { timeout: 1200 }); return () => window.cancelIdleCallback?.(idleId); } const timerId = window.setTimeout(warmup, 300); return () => window.clearTimeout(timerId); }, []); const handleConnectionSaved = useCallback(async (savedConnection: SavedConnection) => { if (!shouldRetrySecurityUpdateAfterRepairSave(securityUpdateRepairSource)) { return; } const backendApp = (window as any).go?.app?.App; if (securityUpdateStatus.migrationId) { if (typeof backendApp?.RetrySecurityUpdateCurrentRound !== 'function') { return; } const rawStatus = await backendApp.RetrySecurityUpdateCurrentRound({ migrationId: securityUpdateStatus.migrationId, }); const nextStatus = await finalizeSecurityUpdateStatus({ backend: backendApp, replaceConnections, replaceGlobalProxy, t, }, normalizeSecurityUpdateStatus(rawStatus)); applySecurityUpdateStatus(nextStatus, { openSettings: false, }); if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); } return; } if (!securityUpdateRawPayload || !savedConnection?.id) { return; } const nextRawPayload = stripLegacyPersistedConnectionById(securityUpdateRawPayload, savedConnection.id); if (!nextRawPayload || nextRawPayload === securityUpdateRawPayload) { return; } window.localStorage.setItem(LEGACY_PERSIST_KEY, nextRawPayload); const rawStatus = typeof backendApp?.GetSecurityUpdateStatus === 'function' ? await backendApp.GetSecurityUpdateStatus() : securityUpdateStatus; const nextStatus = mergeSecurityUpdateStatusWithLegacySource(rawStatus, nextRawPayload, { previousStatus: securityUpdateStatus, t, }); const nextHasLegacySensitiveItems = hasLegacyMigratableSensitiveItems(nextRawPayload); setSecurityUpdateRawPayload(nextRawPayload); setSecurityUpdateHasLegacySensitiveItems(nextHasLegacySensitiveItems); applySecurityUpdateStatus(nextStatus, { openSettings: false, }); }, [ applySecurityUpdateStatus, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, securityUpdateRawPayload, securityUpdateRepairSource, securityUpdateStatus, securityUpdateStatus.migrationId, t, ]); const handleCloseModal = () => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsModalOpen(false); setEditingConnection(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }; const handleOpenDriverManagerFromConnection = () => { setIsModalOpen(false); setEditingConnection(null); setToolCenterBackGroupKey(null); setIsDriverModalOpen(true); }; const handleCloseDriverManager = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsDriverModalOpen(false); setToolCenterBackGroupKey(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); const handleOpenGlobalProxySettings = useCallback(() => { setSecurityUpdateRepairSource(null); setIsProxyModalOpen(true); }, []); const handleCloseGlobalProxySettings = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setIsProxyModalOpen(false); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { setIsSecurityUpdateSettingsOpen(true); } }, [securityUpdateRepairSource]); /** 从聊天面板等入口打开 AI 配置:走设置中心,不再弹独立 AISettingsModal */ const handleOpenAISettings = useCallback((providerId?: string) => { setSecurityUpdateRepairSource(null); setFocusedAIProviderId(providerId); setActiveSettingsCenterGroupKey('services'); setActiveSettingsCenterPane({ key: 'ai', group: 'services' }); setIsSettingsModalOpen(true); }, []); const handleAIPanelRenderError = useCallback((error: Error, errorInfo: React.ErrorInfo) => { try { (window as any).__gonaviLastAIPanelRenderError = { message: error?.message || '', stack: error?.stack || '', componentStack: errorInfo?.componentStack || '', }; } catch { // ignore debug capture failures } console.error('AIChatPanel render error:', error, errorInfo); }, []); const handleRetryAIPanelRender = useCallback(() => { setAiPanelRenderNonce((current) => current + 1); }, []); const handleWebLogout = useCallback(async () => { try { await fetch('/__gonavi/auth/logout', { method: 'POST', credentials: 'same-origin', }); } catch (_) { // ignore } window.location.assign('/login'); }, []); const handleTitleBarWindowToggle = async (options?: { allowMacNativeFullscreen?: boolean }) => { const allowMacNativeFullscreen = options?.allowMacNativeFullscreen === true; const syncWindowStateFromRuntime = async () => { try { const [isFullscreen, isMaximised] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); useStore.getState().setWindowState(isFullscreen ? 'fullscreen' : (isMaximised ? 'maximized' : 'normal')); } catch { // ignore } }; try { void emitWindowDiagnostic('action:titlebar-toggle:before'); if (await WindowIsFullscreen()) { await WindowUnfullscreen(); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-unfullscreen'); return; } if (allowMacNativeFullscreen && useNativeMacWindowControls && isMacRuntime) { await WindowFullscreen(); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen'); return; } const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false); if (isMaximised) { WindowUnmaximise(); } else { WindowMaximise(); } await new Promise((resolve) => window.setTimeout(resolve, 96)); await syncWindowStateFromRuntime(); void emitWindowDiagnostic('action:titlebar-toggle:after-set-maximise-state'); } catch (_) { // ignore } }; const handleTitleBarDoubleClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement | null; if (target?.closest('[data-no-titlebar-toggle="true"]')) { return; } void handleTitleBarWindowToggle({ allowMacNativeFullscreen: false }); }; // handleManualResetWindowZoom 由 resetWindowZoom 快捷键(默认 Ctrl+Shift+0)触发, // 作为自动路径失败时的兜底入口。 // // 优先调 backend App.ResetWebViewZoom 走 WebView2 zoom reset(零动画零感知); // 失败时回退到 Unmaximise→Maximise toggle —— 用户主动按了快捷键,预期看见动画。 const handleManualResetWindowZoom = React.useCallback(async () => { if (!isWindowsPlatform()) { message.info(t('app.window_zoom.message.windows_only')); return; } try { const res = await (window as any).go?.app?.App?.ResetWebViewZoom?.(); if (res?.success) { window.dispatchEvent(new Event('resize')); message.success(t('app.window_zoom.message.reset_success')); return; } console.warn('ResetWebViewZoom backend reported failure, falling back to maximise toggle:', res?.message); } catch (e) { console.warn('ResetWebViewZoom backend unavailable, falling back to maximise toggle', e); } try { const isFullscreen = await safeWindowRuntimeCall(() => WindowIsFullscreen(), false); if (isFullscreen) { message.info(t('app.window_zoom.message.fullscreen_exit_first')); return; } const isMaximised = await safeWindowRuntimeCall(() => WindowIsMaximised(), false); if (isMaximised) { WindowUnmaximise(); await new Promise((resolve) => window.setTimeout(resolve, 96)); WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, 96)); } else { const size = await safeWindowRuntimeCall(() => WindowGetSize(), null); const width = Math.trunc(Number(size?.w) || 0); const height = Math.trunc(Number(size?.h) || 0); if (width > 0 && height > 0) { WindowSetSize(getWindowsScaleFixNudgedWidth(width), height); await new Promise((resolve) => window.setTimeout(resolve, 28)); WindowSetSize(width, height); } } window.dispatchEvent(new Event('resize')); message.success(t('app.window_zoom.message.reset_success_fallback')); } catch (e) { console.warn('Failed to reset window zoom', e); message.error(t('app.window_zoom.message.reset_failed')); } }, [t]); const { ghostRef, handleSidebarMouseDown, sidebarResizeHandleWidth, siderRef, } = useAppSidebarResize({ effectiveUiScale, setSidebarWidth, sidebarWidth, }); useEffect(() => { document.body.style.backgroundColor = 'transparent'; document.body.style.color = darkMode ? '#ffffff' : '#000000'; document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); document.body.setAttribute('data-ui-version', appearance.uiVersion); document.body.setAttribute('data-platform', runtimePlatform || ''); document.body.style.fontSize = `${effectiveFontSize}px`; document.body.style.setProperty('--gn-font-sans', resolvedUiFontFamily); document.body.style.setProperty('--gn-font-mono', resolvedMonoFontFamily); document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`); document.documentElement.style.setProperty('--gn-font-sans', resolvedUiFontFamily); document.documentElement.style.setProperty('--gn-font-mono', resolvedMonoFontFamily); document.documentElement.style.setProperty('--gn-ui-scale', `${effectiveUiScale}`); document.documentElement.style.setProperty('--gn-font-size', `${effectiveFontSize}px`); document.documentElement.style.setProperty('--gn-font-size-sm', `${Math.max(10, Math.round(effectiveFontSize * 0.86))}px`); document.documentElement.style.setProperty('--gn-font-size-xs', `${Math.max(9, Math.round(effectiveFontSize * 0.76))}px`); document.documentElement.style.setProperty('--gn-font-size-mono', `${Math.max(10, Math.round(effectiveDataTableFontSize * 0.92))}px`); document.documentElement.style.setProperty('--gn-data-table-font-size', `${effectiveDataTableFontSize}px`); document.documentElement.style.setProperty('--gn-sidebar-tree-font-size', `${effectiveSidebarTreeFontSize}px`); document.documentElement.style.setProperty('--gn-sidebar-rail-scale', `${effectiveSidebarRailScale}`); document.documentElement.style.setProperty('--gn-control-height', `${tokenControlHeight}px`); document.documentElement.style.setProperty('--gn-control-height-sm', `${tokenControlHeightSM}px`); }, [ appearance.uiVersion, darkMode, effectiveDataTableFontSize, effectiveFontSize, resolvedMonoFontFamily, resolvedUiFontFamily, runtimePlatform, effectiveSidebarRailScale, effectiveSidebarTreeFontSize, effectiveUiScale, tokenControlHeight, tokenControlHeightSM, ]); useEffect(() => { const handleOpenShortcutSettingsEvent = () => { setIsShortcutModalOpen(true); }; window.addEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); }; }, []); useEffect(() => { const handleOpenSnippetSettingsEvent = () => { setIsSnippetModalOpen(false); handleOpenToolCenterPane('workspace', 'snippet-settings'); }; window.addEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); }; }, [handleOpenToolCenterPane]); useEffect(() => { const handleOpenTabDisplaySettingsEvent = () => { setIsSettingsModalOpen(false); setThemeModalSection('workspace'); setIsThemeModalOpen(true); setTabDisplaySettingsFocusRequest((current) => current + 1); }; window.addEventListener('gonavi:open-tab-display-settings', handleOpenTabDisplaySettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-tab-display-settings', handleOpenTabDisplaySettingsEvent as EventListener); }; }, []); useEffect(() => { const handleCreateQueryTabEvent = () => { handleNewQuery(); }; window.addEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener); return () => { window.removeEventListener('gonavi:create-query-tab', handleCreateQueryTabEvent as EventListener); }; }, [handleNewQuery]); useEffect(() => { if (!isMacRuntime || !useNativeMacWindowControls) { return; } const handleMacNativeEscapeCapture = (event: KeyboardEvent) => { if (!shouldSuppressMacNativeEscapeExit( isMacRuntime, useNativeMacWindowControls, useStore.getState().windowState === 'fullscreen', event, { isEditableTarget: isEditableElement(event.target) }, )) { return; } event.preventDefault(); event.stopPropagation(); }; window.addEventListener('keydown', handleMacNativeEscapeCapture, true); return () => { window.removeEventListener('keydown', handleMacNativeEscapeCapture, true); }; }, [isMacRuntime, useNativeMacWindowControls]); useEffect(() => { const handleGlobalShortcut = (event: KeyboardEvent) => { const matchedAction = SHORTCUT_ACTION_ORDER.find((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.scope && meta.scope !== 'global') { return false; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } if (isEditableElement(event.target) && !meta.allowInEditable) { return false; } return isShortcutMatch(event, binding.combo); }); if (!matchedAction) { return; } event.preventDefault(); event.stopPropagation(); switch (matchedAction) { case 'runQuery': window.dispatchEvent(new CustomEvent('gonavi:run-active-query')); break; case 'focusSidebarSearch': window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); break; case 'newQueryTab': handleNewQuery(); break; case 'switchToNextTab': switchActiveTabByOffset(1); break; case 'switchToPreviousTab': switchActiveTabByOffset(-1); break; case 'newConnection': handleCreateConnection(); break; case 'toggleAIPanel': toggleAIPanel(); break; case 'toggleLogPanel': handleToggleLogPanel(); break; case 'toggleTheme': setThemePreference(themeMode === 'dark' ? 'light' : 'dark'); break; case 'openShortcutManager': setIsShortcutModalOpen(true); break; case 'toggleMacFullscreen': if (isMacRuntime && useNativeMacWindowControls) { void handleTitleBarWindowToggle({ allowMacNativeFullscreen: true }); } break; case 'resetWindowZoom': void handleManualResetWindowZoom(); break; } }; window.addEventListener('keydown', handleGlobalShortcut, true); return () => { window.removeEventListener('keydown', handleGlobalShortcut, true); }; }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, switchActiveTabByOffset, themeMode, setThemePreference, toggleAIPanel, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { return; } const handleShortcutCapture = (event: KeyboardEvent) => { event.preventDefault(); event.stopPropagation(); if (event.key === 'Escape') { setCapturingShortcutAction(null); return; } const combo = eventToShortcut(event); if (!combo) { return; } const normalizedCombo = normalizeShortcutCombo(combo); if (!canRecordShortcutForAction(capturingShortcutAction, normalizedCombo)) { const meta = SHORTCUT_ACTION_META[capturingShortcutAction]; void message.warning(meta.scope === 'aiComposer' ? t('app.shortcuts.message.ai_send_limit') : t('app.shortcuts.message.modifier_required')); return; } const conflictAction = SHORTCUT_ACTION_ORDER.find((action) => { if (action === capturingShortcutAction) { return false; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); if (!binding?.enabled) { return false; } return normalizeShortcutCombo(binding.combo) === normalizedCombo; }); if (conflictAction) { void message.warning(t('app.shortcuts.message.conflict', { action: SHORTCUT_ACTION_META[conflictAction].label })); return; } const reservedConflicts = findReservedConflictsForAction( capturingShortcutAction, normalizedCombo, activeShortcutPlatform, ); if (reservedConflicts.length > 0) { const { hasMonaco, hasOther, monacoLabels, otherLabels, otherContexts } = splitConflictsByContext(reservedConflicts); if (hasMonaco) { void message.info(t('app.shortcuts.message.reserved_conflict_info', { labels: monacoLabels }), 4); } if (hasOther) { void message.warning(t('app.shortcuts.message.reserved_conflict_warning', { contexts: otherContexts, labels: otherLabels }), 4); } } updateShortcut(capturingShortcutAction, { combo: normalizedCombo, enabled: true }, activeShortcutPlatform); setCapturingShortcutAction(null); }; window.addEventListener('keydown', handleShortcutCapture, true); return () => { window.removeEventListener('keydown', handleShortcutCapture, true); }; }, [activeShortcutPlatform, capturingShortcutAction, shortcutOptions, t, updateShortcut]); const linuxResizeHandleStyleBase = { position: 'fixed', zIndex: 12000, background: 'transparent', WebkitAppRegion: 'drag', '--wails-draggable': 'drag', userSelect: 'none' } as any; const showLinuxResizeHandles = isLinuxRuntime; const resizeGuideColor = isV2Ui ? 'var(--gn-accent, #16a34a)' : (darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'); const v2AntPrimaryColor = darkMode ? '#22c55e' : '#16a34a'; const v2AntPrimaryHoverColor = darkMode ? '#4ade80' : '#15803d'; const v2AntPrimaryActiveColor = darkMode ? '#16a34a' : '#166534'; const v2AntPrimaryBgColor = darkMode ? 'rgba(34, 197, 94, 0.20)' : '#dcfce7'; const v2AntPrimaryBgHoverColor = darkMode ? 'rgba(34, 197, 94, 0.28)' : '#bbf7d0'; const v2AntPrimaryBorderColor = darkMode ? 'rgba(34, 197, 94, 0.42)' : '#86efac'; const v2AntPrimaryBorderHoverColor = darkMode ? 'rgba(74, 222, 128, 0.58)' : '#4ade80'; const v2AntControlActiveBg = darkMode ? 'rgba(34, 197, 94, 0.16)' : 'rgba(34, 197, 94, 0.10)'; const v2AntControlActiveHoverBg = darkMode ? 'rgba(34, 197, 94, 0.24)' : 'rgba(34, 197, 94, 0.16)'; const v2AntControlOutline = darkMode ? 'rgba(34, 197, 94, 0.42)' : 'rgba(22, 163, 74, 0.22)'; const antdTheme = useMemo(() => ({ algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm, token: { fontSize: tokenFontSize, fontSizeSM: tokenFontSizeSM, fontSizeLG: tokenFontSizeLG, fontFamily: resolvedUiFontFamily, fontFamilyCode: resolvedMonoFontFamily, controlHeight: tokenControlHeight, controlHeightSM: tokenControlHeightSM, controlHeightLG: tokenControlHeightLG, colorBgLayout: 'transparent', colorBgContainer: darkMode ? `rgba(29, 29, 29, ${effectiveOpacity})` : `rgba(255, 255, 255, ${effectiveOpacity})`, colorBgElevated: darkMode ? '#1f1f1f' : '#ffffff', colorFillAlter: darkMode ? `rgba(38, 38, 38, ${effectiveOpacity})` : `rgba(250, 250, 250, ${effectiveOpacity})`, colorPrimary: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#f6c453' : '#1677ff'), colorPrimaryHover: isV2Ui ? v2AntPrimaryHoverColor : (darkMode ? '#ffd666' : '#4096ff'), colorPrimaryActive: isV2Ui ? v2AntPrimaryActiveColor : (darkMode ? '#d8a93b' : '#0958d9'), colorInfo: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#f6c453' : '#1677ff'), colorLink: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#ffd666' : '#1677ff'), colorLinkHover: isV2Ui ? v2AntPrimaryHoverColor : (darkMode ? '#ffe58f' : '#4096ff'), colorLinkActive: isV2Ui ? v2AntPrimaryActiveColor : (darkMode ? '#d8a93b' : '#0958d9'), colorPrimaryBg: isV2Ui ? v2AntPrimaryBgColor : (darkMode ? 'rgba(246, 196, 83, 0.22)' : '#e6f4ff'), colorPrimaryBgHover: isV2Ui ? v2AntPrimaryBgHoverColor : (darkMode ? 'rgba(246, 196, 83, 0.30)' : '#bae0ff'), colorPrimaryBorder: isV2Ui ? v2AntPrimaryBorderColor : (darkMode ? 'rgba(246, 196, 83, 0.45)' : '#91caff'), colorPrimaryBorderHover: isV2Ui ? v2AntPrimaryBorderHoverColor : (darkMode ? 'rgba(246, 196, 83, 0.60)' : '#69b1ff'), controlItemBgActive: isV2Ui ? v2AntControlActiveBg : (darkMode ? 'rgba(246, 196, 83, 0.20)' : 'rgba(22, 119, 255, 0.12)'), controlItemBgActiveHover: isV2Ui ? v2AntControlActiveHoverBg : (darkMode ? 'rgba(246, 196, 83, 0.28)' : 'rgba(22, 119, 255, 0.18)'), controlOutline: isV2Ui ? v2AntControlOutline : (darkMode ? 'rgba(246, 196, 83, 0.50)' : 'rgba(5, 145, 255, 0.24)'), }, components: { Layout: { bodyBg: 'transparent', headerBg: 'transparent', siderBg: 'transparent', triggerBg: 'transparent' }, Table: { headerBg: 'transparent', rowHoverBg: darkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.02)', }, Tabs: { cardBg: 'transparent', itemActiveColor: isV2Ui ? v2AntPrimaryHoverColor : (darkMode ? '#ffd666' : '#1890ff'), itemHoverColor: isV2Ui ? v2AntPrimaryHoverColor : (darkMode ? '#ffe58f' : '#40a9ff'), itemSelectedColor: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#ffd666' : '#1677ff'), inkBarColor: isV2Ui ? v2AntPrimaryColor : (darkMode ? '#ffd666' : '#1677ff'), } } }), [ darkMode, effectiveOpacity, isV2Ui, v2AntControlActiveBg, v2AntControlActiveHoverBg, v2AntControlOutline, v2AntPrimaryActiveColor, v2AntPrimaryBgColor, v2AntPrimaryBgHoverColor, v2AntPrimaryBorderColor, v2AntPrimaryBorderHoverColor, v2AntPrimaryColor, v2AntPrimaryHoverColor, tokenControlHeight, tokenControlHeightLG, tokenControlHeightSM, tokenFontSize, tokenFontSizeLG, tokenFontSizeSM, resolvedMonoFontFamily, resolvedUiFontFamily, ]); const filterFontOption = useCallback((input: string, option?: { value?: string; label?: React.ReactNode }) => ( matchFontFamilyOption(input, { value: String(option?.value || ''), label: String(option?.label || ''), }) ), []); const renderFontOptionLabel = useCallback((option: FontFamilyOption) => (
{option.label} {option.value}
), [darkMode]); const showLinuxCJKFontBanner = Boolean( linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && !isLinuxCJKFontBannerDismissed, ); const sidebarMetadataFieldItems = useMemo(() => { const labelByField: Record = { comment: t('sidebar.v2_table_group_menu.show_table_comments'), rows: t('sidebar.v2_table_group_menu.display_table_rows'), size: t('sidebar.v2_table_group_menu.display_table_size'), createdAt: t('sidebar.v2_table_group_menu.display_create_time'), updatedAt: t('sidebar.v2_table_group_menu.display_update_time'), }; return sidebarTableMetadataFieldOrder.map((field) => ({ field, label: labelByField[field], })); }, [sidebarTableMetadataFieldOrder, t]); const toggleSidebarMetadataFieldFromSettings = useCallback((field: SidebarTableMetadataField, selected: boolean) => { setQueryOptions({ sidebarTableMetadataFields: setSidebarTableMetadataFieldSelected( sidebarTableMetadataFields, field, selected, sidebarTableMetadataFieldOrder, ), }); }, [setQueryOptions, sidebarTableMetadataFieldOrder, sidebarTableMetadataFields]); const handleSidebarMetadataDragEnd = useCallback((event: DragEndEvent) => { const activeField = String(event.active.id || '') as SidebarTableMetadataField; const overField = String(event.over?.id || '') as SidebarTableMetadataField; if (!overField || activeField === overField) { return; } const currentOrder = sidebarMetadataFieldItems.map((item) => item.field); const activeIndex = currentOrder.indexOf(activeField); const overIndex = currentOrder.indexOf(overField); if (activeIndex < 0 || overIndex < 0) { return; } const nextOrder = arrayMove(currentOrder, activeIndex, overIndex); setQueryOptions({ sidebarTableMetadataFieldOrder: nextOrder, sidebarTableMetadataFields: applySidebarTableMetadataFieldOrder( sidebarTableMetadataFields, nextOrder, ), }); }, [setQueryOptions, sidebarMetadataFieldItems, sidebarTableMetadataFields]); const renderProxySettingsContent = useCallback(() => { const fieldLabelStyle: React.CSSProperties = { marginBottom: 6, fontSize: 12, color: darkMode ? 'rgba(255,255,255,0.55)' : 'rgba(16,24,40,0.58)', }; const proxyGridColumns = viewportWidth < 760 ? '1fr' : '1fr 1fr'; const proxyCardAccent = proxyDraft.enabled ? (darkMode ? 'rgba(34,197,94,0.12)' : 'rgba(16,185,129,0.10)') : (darkMode ? 'rgba(148,163,184,0.12)' : 'rgba(71,85,105,0.08)'); const proxyTestAlertType = proxyTestResult ? (!proxyTestResult.success ? 'error' : ((proxyTestResult.statusCode || 0) >= 400 ? 'warning' : 'success')) : 'info'; return (
{t('app.proxy.section_title')}
{t('app.proxy.description')}
setProxyDraft((current) => ({ ...current, enabled: checked }))} />
{proxyPresetItems.map((preset) => ( ))}
{proxyDraftDirty && (
{t('app.proxy.unsaved_hint')}
)}
{t('app.proxy.connection_title')}
{t('app.proxy.type')}
updateProxyDraftType(value as GlobalProxyConfig['type'])} />
{t('app.proxy.port')}
setProxyDraft((current) => ({ ...current, port: typeof value === 'number' ? value : getGlobalProxyDefaultPort(current.type), }))} />
{t('app.proxy.host')}
setProxyDraft((current) => ({ ...current, host: e.target.value }))} />
{proxyDraft.enabled ? t('app.proxy.enabled_edit_hint') : t('app.proxy.disabled_hint')}
{t('app.proxy.auth_title')}
{t('app.proxy.username_optional')}
setProxyDraft((current) => ({ ...current, user: e.target.value }))} />
{t('app.proxy.password_optional')}
{ const nextPassword = e.target.value; setProxyDraft((current) => ({ ...current, password: nextPassword, hasPassword: nextPassword !== '' ? true : current.hasPassword, })); setProxyDraftClearPassword(false); }} />
{proxyDraftClearPassword ? ( ) : proxyDraft.hasPassword && proxyDraft.password === '' ? (
{t('app.proxy.password_saved_hint')}
) : (
{t('app.proxy.no_auth_hint')}
)}
{t('app.proxy.test.title')}
{t('app.proxy.test.description')}
{t('app.proxy.test.target_label')}
setProxyTestUrl(event.target.value)} onPressEnter={() => { if (proxyCanTest) { void handleTestGlobalProxyDraft(); } }} /> {!proxyDraft.enabled && (
{t('app.proxy.test.disabled_hint')}
)} {proxyTestResult && ( )}
{t('app.proxy.scope_hint')}
); }, [ applyProxyPreset, darkMode, handleApplyGlobalProxyDraft, proxyApplying, proxyCanTest, proxyDraft.enabled, proxyDraft.hasPassword, proxyDraft.host, proxyDraft.password, proxyDraft.port, proxyDraft.type, proxyDraft.user, proxyDraftClearPassword, proxyDraftDirty, proxyDraftHost, proxyDraftPortValid, proxyTestPresetItems, proxyTestResult, proxyTesting, proxyTestUrl, proxyTestUrlTrimmed, proxyPresetItems, proxyStatusDescription, proxyStatusTitle, proxyStatusTone, resetProxyDraftToCurrent, t, handleTestGlobalProxyDraft, updateProxyDraftType, utilityMutedTextStyle, utilityPanelStyle, viewportWidth, ]); const renderSidebarMetadataSettingsPane = useCallback(() => (
{t('app.settings.sidebar_metadata.title')}
{t('app.settings.sidebar_metadata.description')}
item.field)} strategy={verticalListSortingStrategy} >
{sidebarMetadataFieldItems.map((item) => { const checked = sidebarTableMetadataFields.includes(item.field); return ( toggleSidebarMetadataFieldFromSettings(item.field, selected)} /> ); })}
), [ overlayTheme.divider, overlayTheme.titleText, setQueryOptions, sidebarMetadataFieldItems, sidebarMetadataDragSensors, handleSidebarMetadataDragEnd, sidebarTableMetadataFields, t, toggleSidebarMetadataFieldFromSettings, utilityMutedTextStyle, utilityPanelStyle, ]); const renderAboutUpdateActions = (closeAction?: React.ReactNode) => [ isBackgroundProgressForLatestUpdate && !isLatestUpdateDownloaded ? ( ) : null, lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( ) : null, , closeAction ?? null, lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( ) : null, isLatestUpdateDownloaded ? ( ) : null, isLatestUpdateDownloaded ? ( ) : null, ].filter(Boolean); const renderAboutSettingsContent = () => ( aboutLoading ? (
) : (
{t('app.about.field.version')}
{aboutDisplayVersion}
{t('app.about.field.author')}
{aboutInfo?.author || t('common.unknown')}
{t('app.about.field.update_status')}
{aboutUpdateStatus || t('app.about.update_status.not_checked')}
{t('app.about.field.update_channel')}
setAppearance({ customUIFontFamily: sanitizeFontFamilyInput(value), })} onClear={() => setAppearance({ customUIFontFamily: null })} options={uiFontOptions.map((option) => ({ value: option.value, label: option.label, }))} filterOption={filterFontOption} popupMatchSelectWidth style={{ width: '100%' }} optionRender={(option) => renderFontOptionLabel({ value: String(option.data.value), label: String(option.data.label), })} />
{fontFamiliesLoadError ? t('app.theme.font_family.load_failed_fallback', { error: fontFamiliesLoadError }) : (installedFontFamilies.length > 0 ? t('app.theme.font_family.loaded_ui_hint', { count: installedFontFamilies.length }) : t('app.theme.font_family.loading_ui_hint'))}
{linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError ? (
{t('app.theme.font_family.linux_cjk_install_prefix')} {linuxCJKFontInstallHint} {t('app.theme.font_family.linux_cjk_install_suffix')}
) : null}
{t('app.theme.font_family.mono_title')}
setAppearance({ customUIFontFamily: sanitizeFontFamilyInput(value), })} onClear={() => setAppearance({ customUIFontFamily: null })} options={uiFontOptions.map((option) => ({ value: option.value, label: option.label, }))} filterOption={filterFontOption} popupMatchSelectWidth style={{ width: '100%' }} optionRender={(option) => renderFontOptionLabel({ value: String(option.data.value), label: String(option.data.label), })} />
{fontFamiliesLoadError ? t('app.theme.font_family.load_failed_fallback', { error: fontFamiliesLoadError }) : (installedFontFamilies.length > 0 ? t('app.theme.font_family.loaded_ui_hint', { count: installedFontFamilies.length }) : t('app.theme.font_family.loading_ui_hint'))}
{linuxCJKFontInstallHint && hasLoadedInstalledFontsRef.current && !isFontFamiliesLoading && !fontFamiliesLoadError && (
{t('app.theme.font_family.linux_cjk_install_prefix')} {linuxCJKFontInstallHint} {t('app.theme.font_family.linux_cjk_install_suffix')}
)}
{t('app.theme.font_family.mono_title')}
{t('app.data_root.default_directory')}
{dataRootInfo?.defaultPath || '-'}
{t('app.data_root.driver_directory')}
{dataRootInfo?.driverPath || '-'}
{t('app.data_root.switch_target')}
{t('app.data_root.apply_method')}
{t('app.data_root.restart_hint')}
)} ); } if (activeToolCenterPane.key === 'security-update') { return ( ); } if ( activeToolCenterPane.key === 'schema-compare' || activeToolCenterPane.key === 'data-compare' || activeToolCenterPane.key === 'sync' ) { return ( ); } if (activeToolCenterPane.key === 'drivers') { return ( ); } if (activeToolCenterPane.key === 'snippet-settings') { return ( ); } if (activeToolCenterPane.key === 'shortcut-settings') { return ( { setCapturingShortcutAction(null); closeToolCenterPane(); }} footer={[ , , , ]} styles={{ header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} >
{t('app.shortcuts.capture_hint')}
{SHORTCUT_ACTION_ORDER.map((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); const isCapturing = capturingShortcutAction === action; const conflicts = shortcutConflictMap[action]; const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; return (
{meta.label}
{meta.description}
{conflictInfo && (
{conflictInfo.hasMonaco && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_info', { labels: conflictInfo.monacoLabels })} )} {conflictInfo.hasOther && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_warning', { contexts: conflictInfo.otherContexts, labels: conflictInfo.otherLabels })} )}
)}
updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} />
); })}
); } return null; }; return ( , t('app.tools.title'), t('app.tools.description'))} open={isToolsModalOpen} onCancel={() => { if (activeToolCenterPane?.key === 'connection-package') { closeConnectionPackageDialog(); } setActiveToolCenterPane(null); setToolCenterBackGroupKey(null); setIsToolsModalOpen(false); }} footer={null} centered width={1080} styles={{ content: toolCenterModalContentStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, paddingBottom: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} >
{toolCenterGroups.map((group) => { const active = group.key === activeToolCenterGroup.key; return ( ); })}
{activeToolCenterPane ? (
{activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title}
{activeToolCenterPaneItem?.description ?? activeToolCenterGroup.description}
{renderToolCenterPane()}
) : ( <>
{activeToolCenterGroup.title}
{activeToolCenterGroup.description}
{activeToolCenterGroup.items.map((item, index) => ( ))}
)}
); })()} {isSettingsModalOpen && ( , t('app.settings.title'), t('app.settings.description'))} open={isSettingsModalOpen} onCancel={handleCancelSettingsCenterPane} footer={null} centered width={1080} styles={{ content: toolCenterModalContentStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, paddingBottom: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 }, }} >
{settingsCenterGroups.map((group) => { const active = group.key === activeSettingsCenterGroup.key; return ( ); })}
{activeSettingsCenterPane ? (
{activeSettingsCenterPaneItem?.title ?? activeSettingsCenterGroup.title}
{activeSettingsCenterPaneItem?.description ?? activeSettingsCenterGroup.description}
{renderSettingsCenterPane()}
{activeSettingsCenterPane.key === 'about-go-navi' ? ( renderSettingsCenterAboutFooter() ) : isV2Ui && activeSettingsCenterPane.key === 'theme' ? ( <> ) : ( <> )}
) : ( <>
{activeSettingsCenterGroup.title}
{activeSettingsCenterGroup.description}
{activeSettingsCenterGroup.items.map((item, index) => ( ))}
)}
)} {isDataRootModalOpen && ( , t('app.data_root.title'), t('app.data_root.description'), )} open={isDataRootModalOpen} onCancel={() => { setIsDataRootModalOpen(false); setToolCenterBackGroupKey(null); }} footer={[ , toolCenterBackGroupKey === 'config' ? ( ) : null, ]} width={720} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} > {dataRootLoading ? (
) : (
{t('app.data_root.current_directory')}
{t('app.data_root.default_directory')}
{dataRootInfo?.defaultPath || '-'}
{t('app.data_root.driver_directory')}
{dataRootInfo?.driverPath || '-'}
{t('app.data_root.switch_target')}
{t('app.data_root.apply_method')}
{t('app.data_root.restart_hint')}
)}
)} {isSyncModalOpen && ( { setIsSyncModalOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'workflow' ? () => handleReturnToToolCenter(() => setIsSyncModalOpen(false)) : undefined} entryMode={syncModalEntryMode} /> )} {isDriverModalOpen && ( handleReturnToToolCenter(() => setIsDriverModalOpen(false)) : undefined} onOpenGlobalProxySettings={handleOpenGlobalProxySettings} /> )} handleOpenSecurityUpdateSettings()} /> { setIsSecurityUpdateSettingsOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'config' ? () => handleReturnToToolCenter(() => setIsSecurityUpdateSettingsOpen(false)) : undefined} onStart={handleStartSecurityUpdate} onRetry={handleRetrySecurityUpdate} onRestart={handleRestartSecurityUpdate} onIssueAction={handleSecurityUpdateIssueAction} /> handleReturnToToolCenter(closeConnectionPackageDialog) : undefined} onIncludeSecretsChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, includeSecrets: value, useFilePassword: value ? current.useFilePassword : false, password: value ? current.password : '', error: '', })); }} onUseFilePasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, useFilePassword: value, password: value ? current.password : '', error: '', })); }} onPasswordChange={(value) => { setConnectionPackageDialog((current) => ({ ...current, password: value, error: '', })); }} onConfirm={() => { void handleConfirmConnectionPackageDialog(); }} onCancel={closeConnectionPackageDialog} /> , t('app.about.title'), t('app.about.description'))} open={isAboutOpen} onCancel={() => setIsAboutOpen(false)} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10, display: 'flex', flexWrap: 'wrap', gap: 10, justifyContent: 'flex-end' } }} footer={renderAboutUpdateActions( , )} > {renderAboutSettingsContent()} {isThemeModalOpen && ( : themeModalSection === 'appearance' ? : , themeModalSection === 'theme' ? t('app.theme.theme_settings_title') : themeModalSection === 'appearance' ? t('app.theme.appearance_settings_title') : t('app.theme.workspace_settings_title'), themeModalSection === 'theme' ? t('app.theme.theme_settings_description') : themeModalSection === 'appearance' ? t('app.theme.appearance_settings_description') : t('app.theme.workspace_settings_description') )} open={isThemeModalOpen} onCancel={() => { setIsThemeModalOpen(false); }} footer={null} width={820} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, height: 620, overflow: 'hidden' }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} > {renderThemeSettingsContent()} )} {isShortcutModalOpen && ( , t('app.shortcuts.title'), t('app.shortcuts.description'), )} open={isShortcutModalOpen} onCancel={() => { setIsShortcutModalOpen(false); setCapturingShortcutAction(null); setToolCenterBackGroupKey(null); }} width={760} centered style={{ top: 0, maxHeight: 'calc(100vh - 80px)' }} styles={{ content: { ...utilityModalShellStyle, height: 'min(760px, calc(100vh - 80px))', display: 'flex', flexDirection: 'column', }, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} footer={[ , , toolCenterBackGroupKey === 'workspace' ? ( ) : null, ]} >
{t('app.shortcuts.capture_hint')}
{SHORTCUT_ACTION_ORDER.map((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); const isCapturing = capturingShortcutAction === action; const conflicts = shortcutConflictMap[action]; const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; return (
{meta.label}
{meta.description}
{conflictInfo && (
{conflictInfo.hasMonaco && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_info', { labels: conflictInfo.monacoLabels })} )} {conflictInfo.hasOther && ( <>⚠ {t('app.shortcuts.message.reserved_conflict_warning', { contexts: conflictInfo.otherContexts, labels: conflictInfo.otherLabels })} )}
)}
updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} />
); })}
)} {isSnippetModalOpen && ( { setIsSnippetModalOpen(false); setToolCenterBackGroupKey(null); }} onBack={toolCenterBackGroupKey === 'workspace' ? () => handleReturnToToolCenter(() => setIsSnippetModalOpen(false)) : undefined} darkMode={darkMode} overlayTheme={overlayTheme} /> )} {isProxyModalOpen && ( , t('app.proxy.title'), t('app.proxy.description'))} open={isProxyModalOpen} onCancel={handleCloseGlobalProxySettings} footer={null} width={680} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} > {renderProxySettingsContent()} )} { markUpdateProgressDismissed(); hideUpdateDownloadProgress(); }} > {t('app.about.action.hide_to_background')} ] : (updateDownloadProgress.status === 'done' ? [ , , ] : (updateDownloadProgress.status === 'error' ? [ ] : null))} >
{updateDownloadProgress.status === 'done' ? t('app.about.download_progress.complete_hint') : `${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
{updateDownloadProgress.message ? (
{updateDownloadProgress.message}
) : null}
{showLinuxResizeHandles && ( <> {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */}
)} {/* Ghost Resize Line for Sidebar */}
{/* Ghost Resize Line for Log Panel */}
); } export default App;