import { defineStore } from "pinia"; import { computed, ref } from "vue"; import { aiConfigToItem, generateId, getConfigKey } from "@/lib/ai/aiConfigList"; import { DEFAULT_DATA_GRID_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts"; import * as api from "@/lib/backend/api"; import { setDebugLoggingEnabled } from "@/lib/backend/debugLog"; import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage"; import { type ColumnFormatterConfig, type CustomColumnFormatterConfig, normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlobalDateTimePattern } from "@/lib/dataGrid/columnFormatter"; import { type DataGridCopyPreference, type DataGridExtractorOptions, DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, normalizeDataGridCopyPreference, normalizeDataGridExtractorOptions } from "@/lib/dataGrid/dataGridCopyExtractor"; import { DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT, normalizeDataGridTextFilterPanelHeight } from "@/lib/dataGrid/dataGridTextFilterPanel"; import { DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, type DataGridTypeColorScheme, normalizeActiveDataGridTypeColorSchemeId, normalizeDataGridTypeColorSchemes } from "@/lib/dataGrid/dataGridTypeColorScheme"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { DEFAULT_QUERY_RESULT_MAX_ROWS, normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits"; import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry"; import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget"; import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort"; import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay"; import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax"; import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates"; import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode"; import { normalizeCompletionTriggerMode, type SqlCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy"; import { configureMetadataRuntimeCache, METADATA_CACHE_DEFAULT_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache"; import type { AiApiStyle, AiAssistantMode, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai"; import type { SqlSnippet, TableInfoTab } from "@/types/database"; export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode }; export interface DesktopSettings { show_tray_icon: boolean; icon_theme: DesktopIconTheme; quit_on_close: boolean; close_action_prompted: boolean; debug_logging_enabled: boolean; metadata_cache_max_memory_mb: number; duckdb_worker_process_isolation: boolean; duckdb_worker_max_processes: number; saved_sql_sync_dir?: string | null; driver_store_dir?: string | null; plugin_store_dir?: string | null; agent_store_dir?: string | null; sidebar_table_page_size?: number | null; } export interface McpGlobalPolicy { readOnly: boolean; allowDangerousSql: boolean; allowedConnectionIds: string[] | null; configured: boolean; } export type DesktopIconTheme = "default" | "black"; export type InterfaceLayout = "separated" | "classic"; export type UpdateDownloadSource = "official" | "cnb"; export type SqlSemanticDiagnosticsMode = "auto" | "enabled" | "disabled"; export type OpenTabsRestoreMode = "all" | "pinned" | "none"; export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000; export const DUCKDB_WORKER_MAX_PROCESSES_MIN = 1; export const DUCKDB_WORKER_MAX_PROCESSES_MAX = 16; export const DUCKDB_WORKER_MAX_PROCESSES_DEFAULT = 4; const SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED = false; export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { show_tray_icon: true, icon_theme: "default", quit_on_close: false, close_action_prompted: false, debug_logging_enabled: false, metadata_cache_max_memory_mb: METADATA_CACHE_DEFAULT_MEMORY_MB, duckdb_worker_process_isolation: false, duckdb_worker_max_processes: DUCKDB_WORKER_MAX_PROCESSES_DEFAULT, saved_sql_sync_dir: null, driver_store_dir: null, plugin_store_dir: null, agent_store_dir: null, sidebar_table_page_size: DEFAULT_SIDEBAR_TABLE_PAGE_SIZE, }; export const DEFAULT_MCP_GLOBAL_POLICY: McpGlobalPolicy = { readOnly: false, allowDangerousSql: false, allowedConnectionIds: null, configured: false, }; export function normalizeMcpGlobalPolicy(policy: Partial | null | undefined): McpGlobalPolicy { const allowedConnectionIds = policy?.allowedConnectionIds === null || policy?.allowedConnectionIds === undefined ? null : [...new Set(policy.allowedConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))]; return { readOnly: policy?.readOnly === true, allowDangerousSql: policy?.allowDangerousSql === true, allowedConnectionIds, configured: policy?.configured === true, }; } export function normalizeDesktopSettings(settings: Partial | null | undefined): DesktopSettings { const iconTheme = settings?.icon_theme === "black" ? "black" : DEFAULT_DESKTOP_SETTINGS.icon_theme; const sidebarTablePageSize = typeof settings?.sidebar_table_page_size === "number" && settings.sidebar_table_page_size > 0 ? settings.sidebar_table_page_size : DEFAULT_DESKTOP_SETTINGS.sidebar_table_page_size; return { show_tray_icon: settings?.show_tray_icon ?? DEFAULT_DESKTOP_SETTINGS.show_tray_icon, icon_theme: iconTheme, quit_on_close: settings?.quit_on_close ?? DEFAULT_DESKTOP_SETTINGS.quit_on_close, close_action_prompted: settings?.close_action_prompted ?? DEFAULT_DESKTOP_SETTINGS.close_action_prompted, debug_logging_enabled: settings?.debug_logging_enabled ?? DEFAULT_DESKTOP_SETTINGS.debug_logging_enabled, metadata_cache_max_memory_mb: normalizeMetadataCacheMemoryMb(settings?.metadata_cache_max_memory_mb), duckdb_worker_process_isolation: settings?.duckdb_worker_process_isolation ?? DEFAULT_DESKTOP_SETTINGS.duckdb_worker_process_isolation, duckdb_worker_max_processes: normalizeDuckDbWorkerMaxProcesses(settings?.duckdb_worker_max_processes), saved_sql_sync_dir: settings?.saved_sql_sync_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.saved_sql_sync_dir, driver_store_dir: settings?.driver_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.driver_store_dir, plugin_store_dir: settings?.plugin_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.plugin_store_dir, agent_store_dir: settings?.agent_store_dir?.trim() || DEFAULT_DESKTOP_SETTINGS.agent_store_dir, sidebar_table_page_size: sidebarTablePageSize, }; } export function normalizeDuckDbWorkerMaxProcesses(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DUCKDB_WORKER_MAX_PROCESSES_DEFAULT; return Math.min(DUCKDB_WORKER_MAX_PROCESSES_MAX, Math.max(DUCKDB_WORKER_MAX_PROCESSES_MIN, Math.round(value))); } export interface AiProviderPreset extends Omit { label: string; iconSlug?: string; requiresApiKey: boolean; } export const AI_PROVIDER_PRESETS: Record = { claude: { label: "Claude", iconSlug: "anthropic", provider: "claude", endpoint: "https://api.anthropic.com/v1/messages", model: "claude-sonnet-4-20250514", apiStyle: "completions", authMethod: "api-key", requiresApiKey: true, }, openai: { label: "OpenAI", iconSlug: "openai", provider: "openai", endpoint: "https://api.openai.com/v1/chat/completions", model: "gpt-4o-mini", apiStyle: "completions", authMethod: "bearer", requiresApiKey: true, }, gemini: { label: "Gemini", iconSlug: "googlegemini", provider: "gemini", endpoint: "https://generativelanguage.googleapis.com", model: "gemini-1.5-pro", apiStyle: "completions", authMethod: "api-key", requiresApiKey: true, }, deepseek: { label: "DeepSeek", iconSlug: "deepseek", provider: "deepseek", endpoint: "https://api.deepseek.com/v1", model: "deepseek-v4-flash", apiStyle: "completions", authMethod: "bearer", requiresApiKey: true, }, qwen: { label: "Qwen", iconSlug: "alibabacloud", provider: "qwen", endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen-plus", apiStyle: "completions", authMethod: "bearer", requiresApiKey: true, }, minimax: { label: "MiniMax", iconSlug: "minimax", provider: "minimax", endpoint: "https://api.minimax.io/v1", model: "MiniMax-M3", apiStyle: "completions", authMethod: "bearer", requiresApiKey: true, }, ollama: { label: "Ollama", iconSlug: "ollama", provider: "ollama", endpoint: "http://localhost:11434/v1", model: "llama3.1", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "anthropic-compatible": { label: "Anthropic Compatible", iconSlug: "anthropic", provider: "anthropic-compatible", endpoint: "", model: "", apiStyle: "anthropic-messages", authMethod: "bearer", requiresApiKey: false, }, "openai-compatible": { label: "OpenAI Compatible", iconSlug: "openai", provider: "openai-compatible", endpoint: "", model: "", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "claude-code-cli": { label: "Claude Code CLI", iconSlug: "claudecode", provider: "claude-code-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "codex-cli": { label: "Codex CLI", iconSlug: "codex", provider: "codex-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "opencode-cli": { label: "OpenCode CLI", iconSlug: "opencode", provider: "opencode-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "cursor-cli": { label: "Cursor CLI", iconSlug: "cursor", provider: "cursor-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "codebuddy-cli": { label: "CodeBuddy Code", iconSlug: "codebuddy", provider: "codebuddy-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "qoder-cli": { label: "Qoder CLI", provider: "qoder-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "grok-cli": { label: "Grok CLI", iconSlug: "grok", provider: "grok-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, "pi-agent-cli": { label: "Pi Coding Agent", iconSlug: "pi", provider: "pi-agent-cli", endpoint: "", model: "default", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, custom: { label: "Custom", provider: "custom", endpoint: "", model: "", apiStyle: "completions", authMethod: "bearer", requiresApiKey: false, }, }; const defaultConfigs: Record> = Object.fromEntries( Object.entries(AI_PROVIDER_PRESETS).map(([provider, preset]) => { const { label: _label, iconSlug: _iconSlug, requiresApiKey: _requiresApiKey, ...config } = preset; return [provider, config]; }), ) as Record>; const AI_REASONING_LEVELS: AiReasoningLevel[] = ["default", "minimal", "low", "medium", "high", "xhigh", "max"]; const AI_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; function normalizeAiReasoningLevel(value: unknown): AiReasoningLevel { return typeof value === "string" && AI_REASONING_LEVELS.includes(value as AiReasoningLevel) ? (value as AiReasoningLevel) : "default"; } export function normalizeAiEnv(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const result: Record = {}; for (const [rawKey, rawValue] of Object.entries(value as Record)) { const key = rawKey.trim(); if (!key || !AI_ENV_KEY_RE.test(key)) continue; result[key] = rawValue == null ? "" : String(rawValue); } return result; } export function normalizeAiConfig(config: Partial | null | undefined): AiConfig { const provider = config?.provider && config.provider in AI_PROVIDER_PRESETS ? config.provider : inferAiProviderFromConfig(config); return { ...defaultConfigs[provider], ...config, provider, apiKey: (config?.apiKey ?? "").trim(), apiStyle: config?.apiStyle ?? defaultConfigs[provider].apiStyle, authMethod: config?.authMethod ?? defaultConfigs[provider].authMethod, proxyEnabled: !!config?.proxyEnabled, proxyUrl: config?.proxyUrl ?? "", enableThinking: config?.enableThinking ?? true, reasoningLevel: normalizeAiReasoningLevel(config?.reasoningLevel), contextWindow: config?.contextWindow ?? undefined, codexCliPath: config?.codexCliPath?.trim() || undefined, codexCliEnv: normalizeAiEnv(config?.codexCliEnv), claudeCodeCliPath: config?.claudeCodeCliPath?.trim() || undefined, claudeCodeCliEnv: normalizeAiEnv(config?.claudeCodeCliEnv), piAgentCliPath: config?.piAgentCliPath?.trim() || undefined, piAgentCliEnv: normalizeAiEnv(config?.piAgentCliEnv), opencodeCliPath: config?.opencodeCliPath?.trim() || undefined, opencodeCliEnv: normalizeAiEnv(config?.opencodeCliEnv), cursorCliPath: config?.cursorCliPath?.trim() || undefined, cursorCliEnv: normalizeAiEnv(config?.cursorCliEnv), grokCliPath: config?.grokCliPath?.trim() || undefined, grokCliEnv: normalizeAiEnv(config?.grokCliEnv), codebuddyCliPath: config?.codebuddyCliPath?.trim() || undefined, codebuddyCliEnv: normalizeAiEnv(config?.codebuddyCliEnv), qoderCliPath: config?.qoderCliPath?.trim() || undefined, qoderCliEnv: normalizeAiEnv(config?.qoderCliEnv), }; } function normalizeAiConfigItem(config: AiConfigItem): AiConfigItem { return { ...config, ...normalizeAiConfig(config) }; } function inferAiProviderFromConfig(config: Partial | null | undefined): AiProvider { const endpoint = config?.endpoint?.toLowerCase() ?? ""; const model = config?.model?.toLowerCase() ?? ""; if (endpoint.includes("deepseek") || model.includes("deepseek")) return "deepseek"; if (endpoint.includes("dashscope") || endpoint.includes("aliyuncs") || model.includes("qwen")) return "qwen"; if (endpoint.includes("generativelanguage.googleapis.com") || model.includes("gemini")) return "gemini"; if (endpoint.includes("minimax.io") || endpoint.includes("minimaxi.com") || model.includes("minimax")) return "minimax"; if (endpoint.includes("localhost:11434") || endpoint.includes("127.0.0.1:11434")) return "ollama"; if (endpoint.includes("openai.com") || model.startsWith("gpt-")) return "openai"; return "claude"; } export type EditorTheme = | "app" | "one-dark" | "vscode-dark" | "vscode-light" | "nord" | "okaidia" | "material" | "duotone-light" | "duotone-dark" | "xcode" | "xcode-dark" | "idea-light" | "idea-dark" | "jetbrains-light" | "jetbrains-dark" | "cursor-light" | "cursor-dark" | "claude-light" | "claude-dark" | "custom"; const STRUCTURE_EDITOR_DENSITIES = ["compact", "standard", "comfortable"] as const; export type StructureEditorDensity = (typeof STRUCTURE_EDITOR_DENSITIES)[number]; const COLUMN_WIDTH_DENSITIES = ["compact", "standard", "comfortable"] as const; export type ColumnWidthDensity = (typeof COLUMN_WIDTH_DENSITIES)[number]; const CELL_DETAIL_PANEL_LAYOUTS = ["bottom", "right"] as const; export type CellDetailPanelLayout = (typeof CELL_DETAIL_PANEL_LAYOUTS)[number]; const TAB_LAYOUT_MODES = ["scroll", "wrap"] as const; export type TabLayoutMode = (typeof TAB_LAYOUT_MODES)[number]; const DATA_GRID_RENDER_MODES = ["dom", "canvas"] as const; export type DataGridRenderMode = (typeof DATA_GRID_RENDER_MODES)[number]; const DATA_GRID_SEARCH_MODES = ["filter", "highlight"] as const; export type DataGridSearchMode = (typeof DATA_GRID_SEARCH_MODES)[number]; export type DataGridFilterEditorView = "quick" | "conditions" | "text"; const RESULT_RUN_DISPLAY_MODES = ["tabs", "list"] as const; export type ResultRunDisplayMode = (typeof RESULT_RUN_DISPLAY_MODES)[number]; export const TABLE_FONT_SIZE_MIN = 8; export const TABLE_FONT_SIZE_MAX = 16; export const TABLE_FONT_SIZE_DEFAULT = 13; export const SIDEBAR_FONT_SIZE_MIN = 9; export const SIDEBAR_FONT_SIZE_MAX = 24; export const SIDEBAR_FONT_SIZE_DEFAULT = 14; export const SIDEBAR_INDENT_MIN = 4; export const SIDEBAR_INDENT_MAX = 32; export const SIDEBAR_INDENT_DEFAULT = 16; const DISCONNECT_TAB_HANDLING_MODES = ["close-tabs", "keep-tabs-clear-results", "keep-tabs-keep-results"] as const; export type DisconnectTabHandlingMode = (typeof DISCONNECT_TAB_HANDLING_MODES)[number]; const CLICK_TABLE_NAVIGATION_TARGETS = ["data", "ddl"] as const; export type ClickTableNavigationTarget = (typeof CLICK_TABLE_NAVIGATION_TARGETS)[number]; export interface CustomThemeColors { keyword: string; field: string; function: string; string: string; number: string; comment: string; table: string; operator: string; type: string; builtin: string; background?: string; foreground?: string; } export const DEFAULT_CUSTOM_THEME_COLORS: CustomThemeColors = { keyword: "#cba6f7", field: "#f9e2af", function: "#89dceb", string: "#a6e3a1", number: "#fab387", comment: "#6c7086", table: "#a6e3a1", operator: "#89b4fa", type: "#89b4fa", builtin: "#f38ba8", }; export interface CustomThemeDdlColors { addedRowBg: string; addedRowBgAlpha: number; removedRowBg: string; removedRowBgAlpha: number; modifiedRowBg: string; modifiedRowBgAlpha: number; modifiedCharBg: string; modifiedCharBgAlpha: number; } export const DEFAULT_CUSTOM_THEME_DDL_COLORS: CustomThemeDdlColors = { addedRowBg: "#22c55e", addedRowBgAlpha: 10, removedRowBg: "#ef4444", removedRowBgAlpha: 10, modifiedRowBg: "#eab308", modifiedRowBgAlpha: 10, modifiedCharBg: "#f59e0b", modifiedCharBgAlpha: 50, }; export interface CustomTheme { id: string; name: string; colors: CustomThemeColors; ddlColors: CustomThemeDdlColors; } export const DEFAULT_CUSTOM_THEMES: CustomTheme[] = [ { id: "default", name: "Custom", colors: { ...DEFAULT_CUSTOM_THEME_COLORS }, ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS }, }, ]; export type SidebarObjectInfoMode = "comment-inline" | "comment-aligned" | "comment-right" | "size" | "hidden"; export interface EditorSettings { fontFamily: string; fontSize: number; uiFontFamily: string; uiScale: number; theme: EditorTheme; customThemeColors: CustomThemeColors; customThemes: CustomTheme[]; activeCustomThemeId: string; executeMode: "all" | "current"; executeModeDefaultVersion: number; executeAllOnBlankLine: boolean; globalConnectTimeoutSecs: number; connectTimeoutInheritConnectionIds: string[]; globalQueryTimeoutSecs: number; queryTimeoutInheritConnectionIds: string[]; timeoutInheritanceMigrationVersion: number; showExecutionTargetPicker: boolean; showStatementRunButtons: boolean; showCurrentStatementFrame: boolean; showInsertValueHints: boolean; autoAliasTables: boolean; insertSpaceAfterCompletion: boolean; sortCompletionColumnsAlphabetically: boolean; wordWrap: boolean; tableDdlWordWrap: boolean; vimModeEnabled: boolean; autoCloseBrackets: boolean; sqlSemanticDiagnosticsMode: SqlSemanticDiagnosticsMode; sqlSemanticDiagnosticsEnabled: boolean; confirmDangerousSqlExecution: boolean; confirmUnsavedSqlClose: boolean; savedSqlOpenTargetMode: SavedSqlOpenTargetMode; compactTabTitle: boolean; tabLayout: TabLayoutMode; appLayout: "separated" | "classic"; pageSize: number; tableOpenPageSize: number; queryResultMaxRowsEnabled: boolean; queryResultMaxRows: number; infiniteScroll: boolean; /** Preserved for downgrade compatibility; current clients use queryResultMaxRows. */ infiniteScrollMaxRows: number; flatteningMultiLineText: boolean; regexMaxMatchCount: number; autoCalculateTotalRows: boolean; mongoViewMode: "document" | "table"; showColumnCommentsInHeader: boolean; showColumnTypesInHeader: boolean; dataGridShowTransposeFieldMetadata: boolean; colorizeDataGridCellTypes: boolean; dataGridTypeColorSchemes: DataGridTypeColorScheme[]; activeDataGridTypeColorSchemeId: string; showIndexIndicatorsInHeader: boolean; compactColumnHeaderActions: boolean; columnWidthDensity: ColumnWidthDensity; dataGridQuickEntry: boolean; dataGridFilterEditorView: DataGridFilterEditorView; dataGridTextFilterPanelHeight: number; dataGridRenderMode: DataGridRenderMode; dataGridSearchMode: DataGridSearchMode; dataGridCopyExtractor: DataGridCopyPreference; dataGridExtractorOptions: DataGridExtractorOptions; resultRunDisplayMode: ResultRunDisplayMode; dataGridAutoTransposeSingleRow: boolean; dataGridMultiRowTranspose: boolean; dataGridHideNullColumns: boolean; dataGridBooleanDisplayMode: "dropdown" | "checkbox"; numericColumnRightAlign: boolean; tableFontFamily: string; tableFontSize: number; structureEditorDensity: StructureEditorDensity; tableInfoActiveTab: TableInfoTab; tableInfoDrawerWidth: number; cellDetailDrawerWidth: number; cellDetailPanelLayout: CellDetailPanelLayout; cellDetailJsonFormatted: boolean; cellDetailMetadataCollapsed: boolean; shortcuts: ShortcutSettings; sqlFormatter: SqlFormatterSettings; sidebarActivation: SidebarActivation; sidebarConnectionSortMode: ConnectionListSortMode; sidebarObjectDisplay: "grouped" | "simple"; routineSourceOpenMode: "query-tab" | "dialog"; sidebarTableSearchEnabled: boolean; sidebarTableSearchLocal: boolean; sidebarGlobalSearchLocal: boolean; autoSelectActiveSidebarNode: boolean; sidebarOpenDatabaseOnSingleClick: boolean; openTabsRestoreMode: OpenTabsRestoreMode; disconnectTabHandlingMode: DisconnectTabHandlingMode; dataTabReuseMode: DataTabReuseMode; openDataTabsNextToActive: boolean; prefillNewQueryWithSelect: boolean; updateNotificationsEnabled: boolean; sidebarHiddenTablePrefixes: string[]; sidebarObjectInfoMode: SidebarObjectInfoMode; sidebarShowConnectionNotes: boolean; sidebarAllowHorizontalScroll: boolean; sidebarIndent: number; sidebarFontSize: number; columnFormatters: Record; customColumnFormatters: Record; globalDateTimeDisplayFormat: string; globalDateTimeExportFormat: string; globalDateTimeImportFormat: string; snippets: SqlSnippet[]; tableColumnTemplateFields: string[]; exportBatchSize: number; exportRowLimitEnabled: boolean; exportRowLimit: number; queryExportKeysetOptimizationEnabled: boolean; updateDownloadSource: UpdateDownloadSource; ignoredUpdateVersion: string; toolbarItems: ToolbarItems; objectBrowserShowCheckbox: boolean; objectBrowserViewMode: "list" | "grid"; sqlVariableSubstitutionEnabled: boolean; sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides; continueOnErrorOnBatch: boolean; clickTableNavigationTarget: ClickTableNavigationTarget; completionTriggerMode: SqlCompletionTriggerMode; } export interface ToolbarItems { dataTransfer: boolean; driverManager: boolean; sqlFile: boolean; schemaDiff: boolean; dataCompare: boolean; checkUpdates: boolean; sqlLibrary: boolean; sqlFileTree: boolean; history: boolean; ai: boolean; theme: boolean; github: boolean; exclusiveRightSidebarPanels: boolean; } export const DEFAULT_TOOLBAR_ITEMS: ToolbarItems = { dataTransfer: true, driverManager: true, sqlFile: true, schemaDiff: true, dataCompare: true, checkUpdates: true, sqlLibrary: true, sqlFileTree: true, history: true, ai: true, theme: true, github: true, exclusiveRightSidebarPanels: true, }; export const RIGHT_SIDEBAR_PANEL_IDS = ["ai", "history", "sqlLibrary", "sqlFile"] as const; export type RightSidebarPanelId = (typeof RIGHT_SIDEBAR_PANEL_IDS)[number]; export type RightSidebarPanelState = Record; export function transitionRightSidebarPanels(current: RightSidebarPanelState, panel: RightSidebarPanelId, open: boolean, exclusive: boolean): RightSidebarPanelState { const next = { ...current }; if (open && exclusive) { for (const panelId of RIGHT_SIDEBAR_PANEL_IDS) next[panelId] = false; } next[panel] = open; return next; } export function enforceRightSidebarPanelExclusivity(current: RightSidebarPanelState, preferred?: RightSidebarPanelId): RightSidebarPanelState { const panelToKeep = preferred && current[preferred] ? preferred : RIGHT_SIDEBAR_PANEL_IDS.find((panelId) => current[panelId]); if (!panelToKeep) return { ...current }; return transitionRightSidebarPanels(current, panelToKeep, true, true); } export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean; }[] = [ { value: "app", label: "Follow app theme", dark: false }, { value: "one-dark", label: "One Dark", dark: true }, { value: "vscode-dark", label: "VS Dark+", dark: true }, { value: "vscode-light", label: "VS Light+", dark: false }, { value: "nord", label: "Nord", dark: true }, { value: "okaidia", label: "Okaidia", dark: true }, { value: "material", label: "Material", dark: true }, { value: "duotone-light", label: "Duotone Light", dark: false }, { value: "duotone-dark", label: "Duotone Dark", dark: true }, { value: "xcode", label: "Xcode", dark: false }, { value: "xcode-dark", label: "Xcode Dark", dark: true }, { value: "idea-light", label: "IDEA Light", dark: false }, { value: "idea-dark", label: "IDEA Darcula", dark: true }, { value: "jetbrains-light", label: "JetBrains Light", dark: false }, { value: "jetbrains-dark", label: "JetBrains Dark", dark: true }, { value: "cursor-light", label: "Cursor Light", dark: false }, { value: "cursor-dark", label: "Cursor Dark", dark: true }, { value: "claude-light", label: "Claude Code Light", dark: false }, { value: "claude-dark", label: "Claude Code Dark", dark: true }, { value: "custom", label: "Custom", dark: true }, ]; const EDITOR_THEME_VALUES = new Set(EDITOR_THEMES.map((theme) => theme.value)); export const EXECUTE_MODE_CURRENT_DEFAULT_VERSION = 1; export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { fontFamily: "'Fira Code', 'Cascadia Code', 'Cascadia Mono', 'JetBrains Mono', monospace", fontSize: 13, uiFontFamily: DEFAULT_UI_FONT_FAMILY, uiScale: 1, theme: "app", customThemeColors: { ...DEFAULT_CUSTOM_THEME_COLORS }, customThemes: [...DEFAULT_CUSTOM_THEMES], activeCustomThemeId: "default", executeMode: "current", executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION, executeAllOnBlankLine: false, globalConnectTimeoutSecs: 10, connectTimeoutInheritConnectionIds: [], globalQueryTimeoutSecs: 30, queryTimeoutInheritConnectionIds: [], timeoutInheritanceMigrationVersion: 2, showExecutionTargetPicker: false, showStatementRunButtons: true, showCurrentStatementFrame: true, showInsertValueHints: true, autoAliasTables: true, insertSpaceAfterCompletion: true, sortCompletionColumnsAlphabetically: true, wordWrap: false, tableDdlWordWrap: true, vimModeEnabled: false, autoCloseBrackets: true, sqlSemanticDiagnosticsMode: "auto", sqlSemanticDiagnosticsEnabled: SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED, confirmDangerousSqlExecution: true, confirmUnsavedSqlClose: true, savedSqlOpenTargetMode: "saved", compactTabTitle: false, tabLayout: "scroll", appLayout: "classic", pageSize: 100, tableOpenPageSize: 100, queryResultMaxRowsEnabled: true, queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS, infiniteScroll: false, infiniteScrollMaxRows: 5000, flatteningMultiLineText: false, regexMaxMatchCount: 1000, autoCalculateTotalRows: false, mongoViewMode: "document", showColumnCommentsInHeader: true, showColumnTypesInHeader: true, dataGridShowTransposeFieldMetadata: false, colorizeDataGridCellTypes: false, dataGridTypeColorSchemes: [], activeDataGridTypeColorSchemeId: DATA_GRID_TYPE_COLOR_SCHEME_AUTO_ID, showIndexIndicatorsInHeader: true, compactColumnHeaderActions: true, columnWidthDensity: "standard", dataGridQuickEntry: false, dataGridFilterEditorView: "quick", dataGridTextFilterPanelHeight: DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT, dataGridRenderMode: "canvas", dataGridSearchMode: "filter", dataGridCopyExtractor: "smart", dataGridExtractorOptions: normalizeDataGridExtractorOptions(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS), resultRunDisplayMode: "tabs", dataGridAutoTransposeSingleRow: false, dataGridMultiRowTranspose: false, dataGridHideNullColumns: false, dataGridBooleanDisplayMode: "dropdown", numericColumnRightAlign: true, tableFontFamily: DEFAULT_DATA_GRID_FONT_FAMILY, tableFontSize: TABLE_FONT_SIZE_DEFAULT, structureEditorDensity: "compact", tableInfoActiveTab: "ddl", tableInfoDrawerWidth: 320, cellDetailDrawerWidth: 380, cellDetailPanelLayout: "bottom", cellDetailJsonFormatted: false, cellDetailMetadataCollapsed: false, shortcuts: normalizeShortcutSettings(), sqlFormatter: normalizeSqlFormatterSettings(DEFAULT_SQL_FORMATTER_SETTINGS), sidebarActivation: "single", sidebarConnectionSortMode: "manual", sidebarObjectDisplay: "grouped", routineSourceOpenMode: "query-tab", sidebarTableSearchEnabled: false, sidebarTableSearchLocal: true, sidebarGlobalSearchLocal: false, autoSelectActiveSidebarNode: false, sidebarOpenDatabaseOnSingleClick: false, openTabsRestoreMode: "all", disconnectTabHandlingMode: "close-tabs", dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE, openDataTabsNextToActive: false, prefillNewQueryWithSelect: true, updateNotificationsEnabled: true, sidebarHiddenTablePrefixes: [], sidebarObjectInfoMode: "comment-inline", sidebarShowConnectionNotes: false, sidebarAllowHorizontalScroll: false, sidebarIndent: SIDEBAR_INDENT_DEFAULT, sidebarFontSize: SIDEBAR_FONT_SIZE_DEFAULT, columnFormatters: {}, customColumnFormatters: {}, globalDateTimeDisplayFormat: "", globalDateTimeExportFormat: "", globalDateTimeImportFormat: "", snippets: DEFAULT_SQL_SNIPPETS, tableColumnTemplateFields: [...DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS], exportBatchSize: 2000, exportRowLimitEnabled: false, exportRowLimit: 100000, queryExportKeysetOptimizationEnabled: true, updateDownloadSource: "official", ignoredUpdateVersion: "", toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS }, objectBrowserShowCheckbox: false, objectBrowserViewMode: "list", sqlVariableSubstitutionEnabled: true, sqlVariableSyntaxOverrides: {}, continueOnErrorOnBatch: false, clickTableNavigationTarget: "data", completionTriggerMode: "positional", }; export const STORAGE_KEY = "dbx-editor-settings"; const OLD_FONT_SIZE_KEY = "dbx-query-editor-font-size"; const EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY = "dbx-export-batch-size-default-migrated-v1"; const LEGACY_DEFAULT_EXPORT_BATCH_SIZE = 10000; const MIN_UI_SCALE = 0.75; const MAX_UI_SCALE = 2; export function normalizeGlobalQueryTimeoutSecs(value: unknown): number { return normalizeQueryTimeoutSecs(value); } export function normalizeGlobalConnectTimeoutSecs(value: unknown): number { return normalizeConnectTimeoutSecs(value); } function normalizeUiScale(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_EDITOR_SETTINGS.uiScale; return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(value * 100) / 100)); } function normalizeFontFamily(value: unknown, fallback: string): string { if (typeof value !== "string") return fallback; const trimmed = value.trim(); return trimmed || fallback; } function normalizeDrawerWidth(value: unknown, min: number, fallback: number): number { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.min(900, Math.max(min, Math.round(value))); } function normalizeStructureEditorDensity(value: unknown): StructureEditorDensity { return STRUCTURE_EDITOR_DENSITIES.includes(value as StructureEditorDensity) ? (value as StructureEditorDensity) : DEFAULT_EDITOR_SETTINGS.structureEditorDensity; } function normalizeColumnWidthDensity(value: unknown): ColumnWidthDensity { return COLUMN_WIDTH_DENSITIES.includes(value as ColumnWidthDensity) ? (value as ColumnWidthDensity) : DEFAULT_EDITOR_SETTINGS.columnWidthDensity; } function normalizeTabLayout(value: unknown): TabLayoutMode { return TAB_LAYOUT_MODES.includes(value as TabLayoutMode) ? (value as TabLayoutMode) : DEFAULT_EDITOR_SETTINGS.tabLayout; } function normalizeCellDetailPanelLayout(value: unknown): CellDetailPanelLayout { return CELL_DETAIL_PANEL_LAYOUTS.includes(value as CellDetailPanelLayout) ? (value as CellDetailPanelLayout) : DEFAULT_EDITOR_SETTINGS.cellDetailPanelLayout; } function normalizeDataGridRenderMode(value: unknown): DataGridRenderMode { return DATA_GRID_RENDER_MODES.includes(value as DataGridRenderMode) ? (value as DataGridRenderMode) : DEFAULT_EDITOR_SETTINGS.dataGridRenderMode; } function normalizeDataGridSearchMode(value: unknown): DataGridSearchMode { return DATA_GRID_SEARCH_MODES.includes(value as DataGridSearchMode) ? (value as DataGridSearchMode) : DEFAULT_EDITOR_SETTINGS.dataGridSearchMode; } function normalizeDataGridFilterEditorView(value: unknown): DataGridFilterEditorView { return value === "conditions" || value === "text" ? value : DEFAULT_EDITOR_SETTINGS.dataGridFilterEditorView; } function normalizeResultRunDisplayMode(value: unknown): ResultRunDisplayMode { return RESULT_RUN_DISPLAY_MODES.includes(value as ResultRunDisplayMode) ? (value as ResultRunDisplayMode) : DEFAULT_EDITOR_SETTINGS.resultRunDisplayMode; } function normalizeTableFontSize(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return TABLE_FONT_SIZE_DEFAULT; return Math.min(TABLE_FONT_SIZE_MAX, Math.max(TABLE_FONT_SIZE_MIN, Math.round(value))); } function normalizeSidebarFontSize(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return SIDEBAR_FONT_SIZE_DEFAULT; return Math.min(SIDEBAR_FONT_SIZE_MAX, Math.max(SIDEBAR_FONT_SIZE_MIN, Math.round(value))); } function normalizeSidebarIndent(value: unknown): number { if (typeof value !== "number" || !Number.isFinite(value)) return SIDEBAR_INDENT_DEFAULT; return Math.min(SIDEBAR_INDENT_MAX, Math.max(SIDEBAR_INDENT_MIN, Math.round(value))); } function normalizeUpdateDownloadSource(value: unknown): UpdateDownloadSource { // Preserve the intent of users who previously selected the mainland-China mirror. if (value === "atomgit") return "cnb"; return value === "cnb" ? "cnb" : DEFAULT_EDITOR_SETTINGS.updateDownloadSource; } function normalizeSqlSemanticDiagnosticsMode(value: unknown, legacyEnabled?: unknown): SqlSemanticDiagnosticsMode { if (value === "auto" || value === "enabled" || value === "disabled") return value; if (typeof legacyEnabled === "boolean") return legacyEnabled ? "enabled" : "disabled"; return DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode; } function sqlSemanticDiagnosticsEnabledForMode(mode: SqlSemanticDiagnosticsMode): boolean { if (mode === "enabled") return true; if (mode === "disabled") return false; return SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED; } function normalizeDisconnectTabHandlingMode(value: unknown, legacyCloseTabsOnDisconnect?: unknown): DisconnectTabHandlingMode { if (DISCONNECT_TAB_HANDLING_MODES.includes(value as DisconnectTabHandlingMode)) { return value as DisconnectTabHandlingMode; } if (value === "clear-state") return "keep-tabs-clear-results"; if (value === "keep-tabs") return "keep-tabs-keep-results"; if (typeof legacyCloseTabsOnDisconnect === "boolean") { return legacyCloseTabsOnDisconnect ? "close-tabs" : "keep-tabs-clear-results"; } return DEFAULT_EDITOR_SETTINGS.disconnectTabHandlingMode; } function normalizeClickTableNavigationTarget(value: unknown): ClickTableNavigationTarget { return value === "data" || value === "ddl" ? value : DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget; } function normalizeOpenTabsRestoreMode(value: unknown, legacyRestoreOpenTabsOnLaunch?: unknown): OpenTabsRestoreMode { if (value === "all" || value === "pinned" || value === "none") return value; if (typeof legacyRestoreOpenTabsOnLaunch === "boolean") return legacyRestoreOpenTabsOnLaunch ? "all" : "none"; return DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode; } function normalizeConnectionListSortMode(value: unknown): ConnectionListSortMode { return value === "asc" || value === "desc" ? value : "manual"; } function normalizeSidebarObjectInfoMode(value: unknown, legacyCommentLayout?: unknown, legacyHideTableComments?: unknown, legacyShowDatabaseSizes?: unknown): SidebarObjectInfoMode { if (value === "comment-inline" || value === "comment-aligned" || value === "comment-right" || value === "size" || value === "hidden") return value; if (legacyCommentLayout === "hidden" || legacyHideTableComments === true) return "hidden"; if (legacyShowDatabaseSizes === true) return "size"; if (legacyCommentLayout === "aligned") return "comment-aligned"; return DEFAULT_EDITOR_SETTINGS.sidebarObjectInfoMode; } function normalizeColumnFormatters(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const formatters: Record = {}; for (const [key, formatter] of Object.entries(value as Record)) { const normalized = normalizeColumnFormatter(formatter); if (normalized) formatters[key] = normalized; } return formatters; } function normalizeCustomColumnFormatters(value: unknown): Record { if (!value || typeof value !== "object" || Array.isArray(value)) return {}; const formatters: Record = {}; for (const formatter of Object.values(value as Record)) { const normalized = normalizeCustomColumnFormatter(formatter); if (normalized) formatters[normalized.id] = normalized; } return formatters; } function normalizeSqlSnippets(value: unknown, existing?: SqlSnippet[]): SqlSnippet[] { if (!Array.isArray(value)) return existing ?? DEFAULT_SQL_SNIPPETS; if (value.length === 0) return []; const valid: SqlSnippet[] = []; const seenPrefixes = new Set(); for (const item of value) { if (!item || typeof item !== "object" || typeof item.id !== "string" || !item.id || typeof item.label !== "string" || !item.label || typeof item.prefix !== "string" || !item.prefix || typeof item.body !== "string") { continue; } if (seenPrefixes.has(item.prefix)) continue; seenPrefixes.add(item.prefix); // Older settings do not have this field; only an explicit false disables a snippet. valid.push({ id: item.id, label: item.label, prefix: item.prefix, body: item.body, enabled: item.enabled !== false, }); } if (valid.length === 0) return existing ?? DEFAULT_SQL_SNIPPETS; return valid; } function normalizeToolbarItems(items: Partial | undefined): ToolbarItems { const defaults = DEFAULT_TOOLBAR_ITEMS; if (!items || typeof items !== "object") return { ...defaults }; return { dataTransfer: items.dataTransfer ?? defaults.dataTransfer, driverManager: items.driverManager ?? defaults.driverManager, sqlFile: items.sqlFile ?? defaults.sqlFile, schemaDiff: items.schemaDiff ?? defaults.schemaDiff, dataCompare: items.dataCompare ?? defaults.dataCompare, checkUpdates: items.checkUpdates ?? defaults.checkUpdates, sqlLibrary: items.sqlLibrary ?? defaults.sqlLibrary, sqlFileTree: items.sqlFileTree ?? defaults.sqlFileTree, history: items.history ?? defaults.history, ai: items.ai ?? defaults.ai, theme: items.theme ?? defaults.theme, github: items.github ?? defaults.github, // Saved settings from before right-sidebar exclusivity must adopt the new default. exclusiveRightSidebarPanels: items.exclusiveRightSidebarPanels !== false, }; } const TABLE_INFO_TABS = new Set(["ddl", "columns", "indexes", "foreignKeys", "constraints", "triggers"]); function normalizeTableInfoTab(value: unknown): TableInfoTab { return typeof value === "string" && TABLE_INFO_TABS.has(value as TableInfoTab) ? (value as TableInfoTab) : DEFAULT_EDITOR_SETTINGS.tableInfoActiveTab; } export function normalizeEditorSettings(settings: Partial, existing?: EditorSettings): EditorSettings { const legacyTimeoutSettings = settings as Partial & { queryTimeoutSecs?: unknown; queryTimeoutInheritanceMigrationVersion?: unknown }; const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled); const savedExecuteModeDefaultVersion = settings.executeModeDefaultVersion; const executeModeDefaultVersion = typeof savedExecuteModeDefaultVersion === "number" && savedExecuteModeDefaultVersion >= EXECUTE_MODE_CURRENT_DEFAULT_VERSION ? savedExecuteModeDefaultVersion : EXECUTE_MODE_CURRENT_DEFAULT_VERSION; const hasCurrentExecuteModeDefault = executeModeDefaultVersion === savedExecuteModeDefaultVersion; // The active id can only be validated once the scheme list it points into is known. const dataGridTypeColorSchemes = normalizeDataGridTypeColorSchemes(settings.dataGridTypeColorSchemes); return { fontFamily: normalizeFontFamily(settings.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily), fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize, uiFontFamily: normalizeFontFamily(settings.uiFontFamily, DEFAULT_EDITOR_SETTINGS.uiFontFamily), uiScale: normalizeUiScale(settings.uiScale), theme: settings.theme && EDITOR_THEME_VALUES.has(settings.theme) ? settings.theme : DEFAULT_EDITOR_SETTINGS.theme, customThemeColors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...settings.customThemeColors, }, customThemes: (() => { if (Array.isArray(settings.customThemes) && settings.customThemes.length > 0) { return settings.customThemes.map((theme) => { const renamed = theme.name === "默认" ? { ...theme, name: "Custom" } : { ...theme }; return { ...renamed, colors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...renamed.colors }, ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS, ...(renamed as any).ddlColors, }, }; }); } return settings.customThemeColors ? [ { id: "migrated", name: "Migrated", colors: { ...DEFAULT_CUSTOM_THEME_COLORS, ...settings.customThemeColors, }, ddlColors: { ...DEFAULT_CUSTOM_THEME_DDL_COLORS }, }, ] : []; })(), activeCustomThemeId: settings.activeCustomThemeId ?? "default", executeMode: hasCurrentExecuteModeDefault && (settings.executeMode === "all" || settings.executeMode === "current") ? settings.executeMode : DEFAULT_EDITOR_SETTINGS.executeMode, executeModeDefaultVersion, executeAllOnBlankLine: settings.executeAllOnBlankLine === true, globalConnectTimeoutSecs: normalizeGlobalConnectTimeoutSecs(settings.globalConnectTimeoutSecs), connectTimeoutInheritConnectionIds: Array.isArray(settings.connectTimeoutInheritConnectionIds) ? [...new Set(settings.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [], globalQueryTimeoutSecs: normalizeGlobalQueryTimeoutSecs(settings.globalQueryTimeoutSecs ?? legacyTimeoutSettings.queryTimeoutSecs), queryTimeoutInheritConnectionIds: Array.isArray(settings.queryTimeoutInheritConnectionIds) ? [...new Set(settings.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))] : [], timeoutInheritanceMigrationVersion: typeof settings.timeoutInheritanceMigrationVersion === "number" && settings.timeoutInheritanceMigrationVersion >= 1 ? Math.floor(settings.timeoutInheritanceMigrationVersion) : typeof legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion === "number" && legacyTimeoutSettings.queryTimeoutInheritanceMigrationVersion >= 1 ? 1 : 0, showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker, showStatementRunButtons: typeof settings.showStatementRunButtons === "boolean" ? settings.showStatementRunButtons : DEFAULT_EDITOR_SETTINGS.showStatementRunButtons, showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame, showInsertValueHints: typeof settings.showInsertValueHints === "boolean" ? settings.showInsertValueHints : DEFAULT_EDITOR_SETTINGS.showInsertValueHints, autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables, insertSpaceAfterCompletion: typeof settings.insertSpaceAfterCompletion === "boolean" ? settings.insertSpaceAfterCompletion : DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion, sortCompletionColumnsAlphabetically: typeof settings.sortCompletionColumnsAlphabetically === "boolean" ? settings.sortCompletionColumnsAlphabetically : DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically, wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap, tableDdlWordWrap: typeof settings.tableDdlWordWrap === "boolean" ? settings.tableDdlWordWrap : DEFAULT_EDITOR_SETTINGS.tableDdlWordWrap, vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled, autoCloseBrackets: typeof settings.autoCloseBrackets === "boolean" ? settings.autoCloseBrackets : DEFAULT_EDITOR_SETTINGS.autoCloseBrackets, sqlSemanticDiagnosticsMode, sqlSemanticDiagnosticsEnabled: sqlSemanticDiagnosticsEnabledForMode(sqlSemanticDiagnosticsMode), confirmDangerousSqlExecution: settings.confirmDangerousSqlExecution ?? DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution, confirmUnsavedSqlClose: settings.confirmUnsavedSqlClose ?? DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose, savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode, compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle, tabLayout: normalizeTabLayout(settings.tabLayout), appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout, pageSize: normalizeResultPageSize(settings.pageSize), tableOpenPageSize: normalizeResultPageSize(settings.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize), queryResultMaxRowsEnabled: settings.queryResultMaxRowsEnabled !== false, queryResultMaxRows: normalizeQueryResultMaxRows(settings.queryResultMaxRows), infiniteScroll: settings.infiniteScroll ?? DEFAULT_EDITOR_SETTINGS.infiniteScroll, infiniteScrollMaxRows: typeof settings.infiniteScrollMaxRows === "number" && settings.infiniteScrollMaxRows >= 1000 && settings.infiniteScrollMaxRows <= 50000 ? Math.round(settings.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows, flatteningMultiLineText: settings.flatteningMultiLineText ?? DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText, regexMaxMatchCount: typeof settings.regexMaxMatchCount === "number" && Number.isFinite(settings.regexMaxMatchCount) && settings.regexMaxMatchCount >= 100 && settings.regexMaxMatchCount <= 10000 ? Math.round(settings.regexMaxMatchCount) : DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount, autoCalculateTotalRows: settings.autoCalculateTotalRows ?? DEFAULT_EDITOR_SETTINGS.autoCalculateTotalRows, mongoViewMode: settings.mongoViewMode === "table" ? "table" : DEFAULT_EDITOR_SETTINGS.mongoViewMode, showColumnCommentsInHeader: settings.showColumnCommentsInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnCommentsInHeader, showColumnTypesInHeader: settings.showColumnTypesInHeader ?? DEFAULT_EDITOR_SETTINGS.showColumnTypesInHeader, dataGridShowTransposeFieldMetadata: settings.dataGridShowTransposeFieldMetadata === true, colorizeDataGridCellTypes: settings.colorizeDataGridCellTypes ?? DEFAULT_EDITOR_SETTINGS.colorizeDataGridCellTypes, dataGridTypeColorSchemes, activeDataGridTypeColorSchemeId: normalizeActiveDataGridTypeColorSchemeId(dataGridTypeColorSchemes, settings.activeDataGridTypeColorSchemeId), showIndexIndicatorsInHeader: settings.showIndexIndicatorsInHeader ?? DEFAULT_EDITOR_SETTINGS.showIndexIndicatorsInHeader, compactColumnHeaderActions: settings.compactColumnHeaderActions ?? DEFAULT_EDITOR_SETTINGS.compactColumnHeaderActions, columnWidthDensity: normalizeColumnWidthDensity(settings.columnWidthDensity), dataGridQuickEntry: settings.dataGridQuickEntry ?? DEFAULT_EDITOR_SETTINGS.dataGridQuickEntry, dataGridFilterEditorView: normalizeDataGridFilterEditorView(settings.dataGridFilterEditorView), dataGridTextFilterPanelHeight: normalizeDataGridTextFilterPanelHeight(settings.dataGridTextFilterPanelHeight), dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode), dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode), dataGridCopyExtractor: normalizeDataGridCopyPreference(settings.dataGridCopyExtractor), dataGridExtractorOptions: normalizeDataGridExtractorOptions(settings.dataGridExtractorOptions), resultRunDisplayMode: normalizeResultRunDisplayMode(settings.resultRunDisplayMode), dataGridAutoTransposeSingleRow: settings.dataGridAutoTransposeSingleRow === true, dataGridMultiRowTranspose: settings.dataGridMultiRowTranspose === true, dataGridHideNullColumns: settings.dataGridHideNullColumns === true, dataGridBooleanDisplayMode: settings.dataGridBooleanDisplayMode === "checkbox" ? "checkbox" : "dropdown", numericColumnRightAlign: typeof settings.numericColumnRightAlign === "boolean" ? settings.numericColumnRightAlign : DEFAULT_EDITOR_SETTINGS.numericColumnRightAlign, tableFontFamily: normalizeFontFamily(settings.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily), tableFontSize: normalizeTableFontSize(settings.tableFontSize), structureEditorDensity: normalizeStructureEditorDensity(settings.structureEditorDensity), tableInfoActiveTab: normalizeTableInfoTab(settings.tableInfoActiveTab), tableInfoDrawerWidth: normalizeDrawerWidth(settings.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth), cellDetailDrawerWidth: normalizeDrawerWidth(settings.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth), cellDetailPanelLayout: normalizeCellDetailPanelLayout(settings.cellDetailPanelLayout), cellDetailJsonFormatted: typeof settings.cellDetailJsonFormatted === "boolean" ? settings.cellDetailJsonFormatted : DEFAULT_EDITOR_SETTINGS.cellDetailJsonFormatted, cellDetailMetadataCollapsed: typeof settings.cellDetailMetadataCollapsed === "boolean" ? settings.cellDetailMetadataCollapsed : DEFAULT_EDITOR_SETTINGS.cellDetailMetadataCollapsed, shortcuts: normalizeShortcutSettings(settings.shortcuts), sqlFormatter: normalizeSqlFormatterSettings(settings.sqlFormatter), sidebarActivation: settings.sidebarActivation === "single" || settings.sidebarActivation === "double" ? settings.sidebarActivation : DEFAULT_EDITOR_SETTINGS.sidebarActivation, sidebarConnectionSortMode: normalizeConnectionListSortMode(settings.sidebarConnectionSortMode), sidebarObjectDisplay: settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped" ? settings.sidebarObjectDisplay : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay, routineSourceOpenMode: settings.routineSourceOpenMode === "query-tab" || settings.routineSourceOpenMode === "dialog" ? settings.routineSourceOpenMode : DEFAULT_EDITOR_SETTINGS.routineSourceOpenMode, sidebarTableSearchEnabled: typeof settings.sidebarTableSearchEnabled === "boolean" ? settings.sidebarTableSearchEnabled : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchEnabled, sidebarTableSearchLocal: typeof settings.sidebarTableSearchLocal === "boolean" ? settings.sidebarTableSearchLocal : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchLocal, sidebarGlobalSearchLocal: typeof settings.sidebarGlobalSearchLocal === "boolean" ? settings.sidebarGlobalSearchLocal : DEFAULT_EDITOR_SETTINGS.sidebarGlobalSearchLocal, autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode, sidebarOpenDatabaseOnSingleClick: typeof settings.sidebarOpenDatabaseOnSingleClick === "boolean" ? settings.sidebarOpenDatabaseOnSingleClick : DEFAULT_EDITOR_SETTINGS.sidebarOpenDatabaseOnSingleClick, openTabsRestoreMode: normalizeOpenTabsRestoreMode( (settings as Partial).openTabsRestoreMode, ( settings as Partial & { restoreOpenTabsOnLaunch?: boolean; } ).restoreOpenTabsOnLaunch, ), disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode( (settings as Partial).disconnectTabHandlingMode, ( settings as Partial & { closeQueryTabsOnDisconnect?: boolean; } ).closeQueryTabsOnDisconnect, ), dataTabReuseMode: normalizeDataTabReuseMode( settings.dataTabReuseMode, ( settings as Partial & { reuseDataTab?: boolean; } ).reuseDataTab, ), openDataTabsNextToActive: typeof settings.openDataTabsNextToActive === "boolean" ? settings.openDataTabsNextToActive : DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive, prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect, updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes), sidebarObjectInfoMode: normalizeSidebarObjectInfoMode( settings.sidebarObjectInfoMode, ( settings as Partial & { sidebarTableCommentLayout?: string; } ).sidebarTableCommentLayout, ( settings as Partial & { sidebarHideTableComments?: boolean; } ).sidebarHideTableComments, ( settings as Partial & { sidebarShowDatabaseSizes?: boolean; } ).sidebarShowDatabaseSizes, ), sidebarShowConnectionNotes: settings.sidebarShowConnectionNotes === true, sidebarAllowHorizontalScroll: settings.sidebarAllowHorizontalScroll ?? DEFAULT_EDITOR_SETTINGS.sidebarAllowHorizontalScroll, sidebarIndent: normalizeSidebarIndent(settings.sidebarIndent), sidebarFontSize: normalizeSidebarFontSize(settings.sidebarFontSize), columnFormatters: normalizeColumnFormatters(settings.columnFormatters), customColumnFormatters: normalizeCustomColumnFormatters(settings.customColumnFormatters), globalDateTimeDisplayFormat: normalizeGlobalDateTimePattern(settings.globalDateTimeDisplayFormat), globalDateTimeExportFormat: normalizeGlobalDateTimePattern(settings.globalDateTimeExportFormat), globalDateTimeImportFormat: normalizeGlobalDateTimePattern(settings.globalDateTimeImportFormat), snippets: normalizeSqlSnippets(settings.snippets, existing?.snippets), tableColumnTemplateFields: normalizeTableColumnTemplateFields(settings.tableColumnTemplateFields), exportBatchSize: typeof settings.exportBatchSize === "number" && settings.exportBatchSize >= 100 && settings.exportBatchSize <= 100000 ? Math.round(settings.exportBatchSize) : DEFAULT_EDITOR_SETTINGS.exportBatchSize, exportRowLimitEnabled: typeof settings.exportRowLimitEnabled === "boolean" ? settings.exportRowLimitEnabled : DEFAULT_EDITOR_SETTINGS.exportRowLimitEnabled, exportRowLimit: typeof settings.exportRowLimit === "number" && settings.exportRowLimit >= 100 && settings.exportRowLimit <= 2147483647 ? Math.round(settings.exportRowLimit) : DEFAULT_EDITOR_SETTINGS.exportRowLimit, queryExportKeysetOptimizationEnabled: typeof settings.queryExportKeysetOptimizationEnabled === "boolean" ? settings.queryExportKeysetOptimizationEnabled : DEFAULT_EDITOR_SETTINGS.queryExportKeysetOptimizationEnabled, updateDownloadSource: normalizeUpdateDownloadSource(settings.updateDownloadSource), ignoredUpdateVersion: typeof settings.ignoredUpdateVersion === "string" ? settings.ignoredUpdateVersion : DEFAULT_EDITOR_SETTINGS.ignoredUpdateVersion, toolbarItems: normalizeToolbarItems(settings.toolbarItems), objectBrowserShowCheckbox: typeof settings.objectBrowserShowCheckbox === "boolean" ? settings.objectBrowserShowCheckbox : DEFAULT_EDITOR_SETTINGS.objectBrowserShowCheckbox, objectBrowserViewMode: settings.objectBrowserViewMode === "grid" ? "grid" : DEFAULT_EDITOR_SETTINGS.objectBrowserViewMode, sqlVariableSubstitutionEnabled: typeof settings.sqlVariableSubstitutionEnabled === "boolean" ? settings.sqlVariableSubstitutionEnabled : DEFAULT_EDITOR_SETTINGS.sqlVariableSubstitutionEnabled, sqlVariableSyntaxOverrides: normalizeSqlVariableSyntaxOverrides(settings.sqlVariableSyntaxOverrides), continueOnErrorOnBatch: settings.continueOnErrorOnBatch === true, clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget), completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode), }; } function loadLegacyEditorSettings(): EditorSettings | null { const raw = safeLocalStorageGet(STORAGE_KEY); if (raw) { try { const parsed = JSON.parse(raw) as Partial; if (parsed.exportBatchSize === LEGACY_DEFAULT_EXPORT_BATCH_SIZE && safeLocalStorageGet(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY) !== "1") { parsed.exportBatchSize = DEFAULT_EDITOR_SETTINGS.exportBatchSize; } return normalizeEditorSettings(parsed); } catch { return null; } } const oldSize = safeLocalStorageGet(OLD_FONT_SIZE_KEY); if (!oldSize) return null; const parsed = parseInt(oldSize, 10); return Number.isNaN(parsed) ? null : normalizeEditorSettings({ fontSize: parsed }); } function clearLegacyEditorSettings() { safeLocalStorageRemove(STORAGE_KEY); safeLocalStorageRemove(OLD_FONT_SIZE_KEY); safeLocalStorageRemove(EXPORT_BATCH_SIZE_DEFAULT_MIGRATION_KEY); } function editorSettingsSnapshot(settings: EditorSettings): EditorSettings { return JSON.parse(JSON.stringify(settings)) as EditorSettings; } function editorSettingsPatchSnapshot(settings: Partial): Partial { return JSON.parse(JSON.stringify(settings)) as Partial; } export interface SettingsNavigationRequest { id: number; tab: string; section?: string; } export const useSettingsStore = defineStore("settings", () => { const settingsPageActive = ref(false); const settingsNavigationRequest = ref(null); const activeModel = ref<{ configId: string; modelId: string } | null>(null); const effortPreferences = ref([]); const defaultAiMode = ref("ask"); const isAiConfigLoaded = ref(false); const aiConfigs = ref([]); const desktopSettings = ref({ ...DEFAULT_DESKTOP_SETTINGS }); const mcpGlobalPolicy = ref({ ...DEFAULT_MCP_GLOBAL_POLICY, }); const isDesktopSettingsLoaded = ref(false); const isMcpGlobalPolicyLoaded = ref(false); const isEditorSettingsLoaded = ref(false); let initEditorSettingsPromise: Promise | null = null; let pendingEditorSettingsPatches: Partial[] = []; let editorSettingsSaveQueue: Promise | null = null; let editorSettingsAtomicUpdateQueue: Promise = Promise.resolve(); let editorSettingsPatchRevision = 0; const editorSettingsFieldRevisions = new Map(); let pendingAiChatSelection: AiChatSelectionState | null = null; let aiChatSelectionSaveRunning = false; const editorSettings = ref(normalizeEditorSettings({})); function enqueueEditorSettingsSave(): Promise { const saveCurrentSettings = () => api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value)); const save = editorSettingsSaveQueue ? editorSettingsSaveQueue.catch(() => {}).then(saveCurrentSettings) : saveCurrentSettings(); const trackedSave = save.finally(() => { if (editorSettingsSaveQueue === trackedSave) editorSettingsSaveQueue = null; }); editorSettingsSaveQueue = trackedSave; return trackedSave; } function saveEditorSettings() { void enqueueEditorSettingsSave().catch(() => {}); } function markEditorSettingsPatch(partial: Partial): number { const revision = ++editorSettingsPatchRevision; for (const key of Object.keys(partial) as (keyof EditorSettings)[]) { if (partial[key] !== undefined) editorSettingsFieldRevisions.set(key, revision); } return revision; } function requestSettingsNavigation(tab: string, section?: string) { settingsNavigationRequest.value = { id: Date.now(), tab, section, }; } function clearSettingsNavigationRequest(id: number) { if (settingsNavigationRequest.value?.id === id) settingsNavigationRequest.value = null; } function completeEditorSettingsInitialization() { const pendingPatches = pendingEditorSettingsPatches; pendingEditorSettingsPatches = []; for (const patch of pendingPatches) applyEditorSettingsPatch(patch); isEditorSettingsLoaded.value = true; if (pendingPatches.length) saveEditorSettings(); } async function initEditorSettings() { if (isEditorSettingsLoaded.value) return; if (!initEditorSettingsPromise) { initEditorSettingsPromise = (async () => { // A read failure is not the same as an empty settings record. Keeping the // store unloaded prevents startup migrations from persisting defaults over // settings that are temporarily unavailable. const saved = await api.loadEditorSettings(); if (saved && typeof saved === "object" && !Array.isArray(saved)) { const savedSettings = saved as Partial; const normalized = normalizeEditorSettings(savedSettings); editorSettings.value = normalized; const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION; const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource; if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration) { // Persist one-time migrations so removed or unsafe defaults cannot reappear. await enqueueEditorSettingsSave().catch(() => {}); } completeEditorSettingsInitialization(); return; } const legacy = loadLegacyEditorSettings(); if (legacy) { editorSettings.value = legacy; try { await enqueueEditorSettingsSave(); // Existing desktop users keep settings in localStorage; remove them only // after the async store has accepted the migrated value. clearLegacyEditorSettings(); } catch { /* keep legacy values for a later migration attempt */ } } completeEditorSettingsInitialization(); })().finally(() => { initEditorSettingsPromise = null; }); } await initEditorSettingsPromise; } async function initDesktopSettings() { if (isDesktopSettingsLoaded.value) return; desktopSettings.value = normalizeDesktopSettings(await api.loadDesktopSettings().catch(() => null)); setDebugLoggingEnabled(desktopSettings.value.debug_logging_enabled); configureMetadataRuntimeCache(desktopSettings.value.metadata_cache_max_memory_mb); isDesktopSettingsLoaded.value = true; } async function updateDesktopSettings(partial: Partial) { const previous = desktopSettings.value; const next = { ...desktopSettings.value, ...partial, }; desktopSettings.value = normalizeDesktopSettings(next); setDebugLoggingEnabled(desktopSettings.value.debug_logging_enabled); configureMetadataRuntimeCache(desktopSettings.value.metadata_cache_max_memory_mb); try { await api.saveDesktopSettings(desktopSettings.value); } catch (error) { desktopSettings.value = previous; setDebugLoggingEnabled(previous.debug_logging_enabled); configureMetadataRuntimeCache(previous.metadata_cache_max_memory_mb); throw error; } } async function initMcpGlobalPolicy(force = false) { if (isMcpGlobalPolicyLoaded.value && !force) return; mcpGlobalPolicy.value = normalizeMcpGlobalPolicy(await api.loadMcpGlobalPolicy()); isMcpGlobalPolicyLoaded.value = true; } async function updateMcpGlobalPolicy(partial: Partial>) { const previous = mcpGlobalPolicy.value; const next = normalizeMcpGlobalPolicy({ ...previous, ...partial, configured: true, }); mcpGlobalPolicy.value = next; try { await api.saveMcpGlobalPolicy({ readOnly: next.readOnly, allowDangerousSql: next.allowDangerousSql, allowedConnectionIds: next.allowedConnectionIds, }); } catch (error) { mcpGlobalPolicy.value = previous; throw error; } } async function initAiConfigs(): Promise { if (isAiConfigLoaded.value) return; // 尝试加载新格式 const newConfigs = await api.loadAiConfigs(); if (newConfigs.length > 0) { aiConfigs.value = newConfigs.map(normalizeAiConfigItem); } else { // 迁移旧格式 aiConfigs.value = []; await migrateToMultiConfig(); } const savedSelection = await api.loadAiChatSelection().catch(() => null); effortPreferences.value = (savedSelection?.effortPreferences ?? []).filter((preference) => aiConfigs.value.some((config) => config.id === preference.configId)); defaultAiMode.value = savedSelection?.defaultMode ?? "ask"; const savedActive = savedSelection?.active; const savedConfig = savedActive ? aiConfigs.value.find((config) => config.id === savedActive.configId) : undefined; if (savedConfig && savedActive?.modelId.trim()) { activeModel.value = { configId: savedConfig.id, modelId: savedActive.modelId.trim(), }; } else { const fallback = aiConfigs.value.find((config) => config.isDefault) || aiConfigs.value[0]; activeModel.value = fallback?.model.trim() ? { configId: fallback.id, modelId: fallback.model.trim() } : null; if (activeModel.value) persistAiChatSelection(); } isAiConfigLoaded.value = true; } async function reloadAiConfigs(): Promise { isAiConfigLoaded.value = false; await initAiConfigs(); } async function migrateToMultiConfig(): Promise { const oldActiveConfig = await api.loadAiConfig().catch(() => null); const oldProviderConfigs = await api.loadAiProviderConfigs().catch(() => null); if (!oldActiveConfig && (!oldProviderConfigs || Object.keys(oldProviderConfigs).length === 0)) { return; } const newConfigs: AiConfigItem[] = []; const seenKeys = new Set(); if (oldActiveConfig) { const item = aiConfigToItem(normalizeAiConfig(oldActiveConfig), generateId(), oldActiveConfig.provider); item.isDefault = true; newConfigs.push(item); seenKeys.add(getConfigKey(oldActiveConfig)); } if (oldProviderConfigs) { for (const [provider, config] of Object.entries(oldProviderConfigs)) { const key = getConfigKey(config); if (!seenKeys.has(key)) { const item = aiConfigToItem(normalizeAiConfig(config), generateId(), provider); item.isDefault = false; newConfigs.push(item); seenKeys.add(key); } } } if (newConfigs.length > 0) { await api.saveAiConfigs(newConfigs); aiConfigs.value = newConfigs; } } async function createAiConfig(config: AiConfigItem): Promise { const normalized = normalizeAiConfigItem(config); await api.saveAiConfigItem(normalized); aiConfigs.value.push(normalized); if (aiConfigs.value.length === 1 && normalized.model.trim()) { activeModel.value = { configId: normalized.id, modelId: normalized.model, }; persistAiChatSelection(); } } async function updateAiConfigItem(id: string, config: Partial): Promise { const index = aiConfigs.value.findIndex((c) => c.id === id); if (index !== -1) { const previous = aiConfigs.value[index]; const updated = normalizeAiConfigItem({ ...previous, ...config }); await api.saveAiConfigItem(updated); aiConfigs.value[index] = updated; if (previous.provider !== updated.provider) { effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== id); if (activeModel.value?.configId === id) activeModel.value = null; persistAiChatSelection(); } } } async function deleteAiConfig(id: string): Promise { await api.deleteAiConfig(id); aiConfigs.value = aiConfigs.value.filter((c) => c.id !== id); effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== id); if (activeModel.value?.configId === id) { const fallback = aiConfigs.value.find((config) => config.isDefault) || aiConfigs.value[0]; activeModel.value = fallback?.model.trim() ? { configId: fallback.id, modelId: fallback.model.trim() } : null; } persistAiChatSelection(); } async function setDefaultAiConfig(id: string): Promise { await api.setDefaultAiConfig(id); aiConfigs.value.forEach((c) => { c.isDefault = c.id === id; }); } function updateActiveModel(model: { configId: string; modelId: string }) { activeModel.value = { configId: model.configId, modelId: model.modelId.trim(), }; persistAiChatSelection(); } const activeEffort = computed(() => { const active = activeModel.value; if (!active) return null; return effortPreferences.value.find((preference) => preference.configId === active.configId && preference.modelId === active.modelId)?.selection ?? null; }); function updateActiveEffort(selection: AiEffortSelection | null) { const active = activeModel.value; if (!active) return; effortPreferences.value = effortPreferences.value.filter((preference) => preference.configId !== active.configId || preference.modelId !== active.modelId); if (selection) { effortPreferences.value.push({ configId: active.configId, modelId: active.modelId, selection, }); } persistAiChatSelection(); } function setDefaultAiMode(mode: AiAssistantMode) { if (mode === defaultAiMode.value) return; defaultAiMode.value = mode; persistAiChatSelection(); } function persistAiChatSelection() { pendingAiChatSelection = { version: 1, active: activeModel.value ? { ...activeModel.value } : undefined, effortPreferences: effortPreferences.value.map((preference) => ({ ...preference, selection: { ...preference.selection }, })), defaultMode: defaultAiMode.value, }; if (!aiChatSelectionSaveRunning) void flushAiChatSelection(); } async function flushAiChatSelection() { aiChatSelectionSaveRunning = true; try { while (pendingAiChatSelection) { const selection = pendingAiChatSelection; pendingAiChatSelection = null; await api.saveAiChatSelection(selection).catch(() => {}); } } finally { aiChatSelectionSaveRunning = false; if (pendingAiChatSelection) void flushAiChatSelection(); } } const isConfigured = computed((): boolean => { if (!activeModel.value) return false; const config = aiConfigs.value.find((c) => c.id === activeModel.value!.configId); if (!config) return false; const preset = AI_PROVIDER_PRESETS[config.provider]; if ( config.provider === "codex-cli" || config.provider === "claude-code-cli" || config.provider === "pi-agent-cli" || config.provider === "opencode-cli" || config.provider === "cursor-cli" || config.provider === "grok-cli" || config.provider === "codebuddy-cli" || config.provider === "qoder-cli" ) return true; return !!config.endpoint && !!activeModel.value!.modelId && (!preset.requiresApiKey || !!config.apiKey); }); function applyEditorSettingsPatch(partial: Partial) { if (partial.fontFamily !== undefined) editorSettings.value.fontFamily = normalizeFontFamily(partial.fontFamily, DEFAULT_EDITOR_SETTINGS.fontFamily); if (partial.fontSize !== undefined) editorSettings.value.fontSize = partial.fontSize; if (partial.uiFontFamily !== undefined) editorSettings.value.uiFontFamily = normalizeFontFamily(partial.uiFontFamily, DEFAULT_EDITOR_SETTINGS.uiFontFamily); if (partial.uiScale !== undefined) editorSettings.value.uiScale = normalizeUiScale(partial.uiScale); if (partial.theme !== undefined) editorSettings.value.theme = partial.theme; if (partial.customThemeColors !== undefined) { editorSettings.value.customThemeColors = { ...editorSettings.value.customThemeColors, ...partial.customThemeColors, }; } if (partial.customThemes !== undefined) { editorSettings.value.customThemes = Array.isArray(partial.customThemes) ? partial.customThemes : editorSettings.value.customThemes; } if (partial.activeCustomThemeId !== undefined) { editorSettings.value.activeCustomThemeId = partial.activeCustomThemeId; } if (partial.customThemes !== undefined || partial.activeCustomThemeId !== undefined) { const themes = editorSettings.value.customThemes; const activeId = editorSettings.value.activeCustomThemeId; const activeTheme = themes.find((t) => t.id === activeId) || themes[0]; if (activeTheme) { editorSettings.value.customThemeColors = { ...activeTheme.colors }; } } if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode; if (partial.executeAllOnBlankLine !== undefined) editorSettings.value.executeAllOnBlankLine = partial.executeAllOnBlankLine === true; if (partial.globalConnectTimeoutSecs !== undefined) editorSettings.value.globalConnectTimeoutSecs = normalizeGlobalConnectTimeoutSecs(partial.globalConnectTimeoutSecs); if (partial.connectTimeoutInheritConnectionIds !== undefined) { editorSettings.value.connectTimeoutInheritConnectionIds = [...new Set(partial.connectTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))]; } if (partial.globalQueryTimeoutSecs !== undefined) editorSettings.value.globalQueryTimeoutSecs = normalizeGlobalQueryTimeoutSecs(partial.globalQueryTimeoutSecs); if (partial.queryTimeoutInheritConnectionIds !== undefined) { editorSettings.value.queryTimeoutInheritConnectionIds = [...new Set(partial.queryTimeoutInheritConnectionIds.filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))]; } if (partial.timeoutInheritanceMigrationVersion !== undefined) editorSettings.value.timeoutInheritanceMigrationVersion = Math.max(0, Math.floor(partial.timeoutInheritanceMigrationVersion)); if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker; if (partial.showStatementRunButtons !== undefined) editorSettings.value.showStatementRunButtons = partial.showStatementRunButtons === true; if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true; if (partial.showInsertValueHints !== undefined) editorSettings.value.showInsertValueHints = partial.showInsertValueHints === true; if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables; if (partial.insertSpaceAfterCompletion !== undefined) editorSettings.value.insertSpaceAfterCompletion = partial.insertSpaceAfterCompletion === true; if (partial.sortCompletionColumnsAlphabetically !== undefined) editorSettings.value.sortCompletionColumnsAlphabetically = partial.sortCompletionColumnsAlphabetically === true; if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap; if (partial.tableDdlWordWrap !== undefined) editorSettings.value.tableDdlWordWrap = partial.tableDdlWordWrap === true; if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true; if (partial.autoCloseBrackets !== undefined) editorSettings.value.autoCloseBrackets = partial.autoCloseBrackets === true; if (partial.sqlSemanticDiagnosticsMode !== undefined || partial.sqlSemanticDiagnosticsEnabled !== undefined) { const nextMode = normalizeSqlSemanticDiagnosticsMode(partial.sqlSemanticDiagnosticsMode, partial.sqlSemanticDiagnosticsEnabled); editorSettings.value.sqlSemanticDiagnosticsMode = nextMode; editorSettings.value.sqlSemanticDiagnosticsEnabled = sqlSemanticDiagnosticsEnabledForMode(nextMode); } if (partial.confirmDangerousSqlExecution !== undefined) editorSettings.value.confirmDangerousSqlExecution = partial.confirmDangerousSqlExecution; if (partial.confirmUnsavedSqlClose !== undefined) editorSettings.value.confirmUnsavedSqlClose = partial.confirmUnsavedSqlClose; if (partial.savedSqlOpenTargetMode !== undefined) editorSettings.value.savedSqlOpenTargetMode = partial.savedSqlOpenTargetMode === "current" ? "current" : "saved"; if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle; if (partial.tabLayout !== undefined) editorSettings.value.tabLayout = normalizeTabLayout(partial.tabLayout); if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout; if (partial.pageSize !== undefined) editorSettings.value.pageSize = normalizeResultPageSize(partial.pageSize); if (partial.tableOpenPageSize !== undefined) editorSettings.value.tableOpenPageSize = normalizeResultPageSize(partial.tableOpenPageSize, DEFAULT_EDITOR_SETTINGS.tableOpenPageSize); if (partial.queryResultMaxRowsEnabled !== undefined) editorSettings.value.queryResultMaxRowsEnabled = Boolean(partial.queryResultMaxRowsEnabled); if (partial.queryResultMaxRows !== undefined) editorSettings.value.queryResultMaxRows = normalizeQueryResultMaxRows(partial.queryResultMaxRows, editorSettings.value.queryResultMaxRows); if (partial.infiniteScroll !== undefined) editorSettings.value.infiniteScroll = partial.infiniteScroll; if (partial.infiniteScrollMaxRows !== undefined) editorSettings.value.infiniteScrollMaxRows = typeof partial.infiniteScrollMaxRows === "number" && partial.infiniteScrollMaxRows >= 1000 && partial.infiniteScrollMaxRows <= 50000 ? Math.round(partial.infiniteScrollMaxRows) : DEFAULT_EDITOR_SETTINGS.infiniteScrollMaxRows; if (partial.regexMaxMatchCount !== undefined) editorSettings.value.regexMaxMatchCount = typeof partial.regexMaxMatchCount === "number" && Number.isFinite(partial.regexMaxMatchCount) && partial.regexMaxMatchCount >= 100 && partial.regexMaxMatchCount <= 10000 ? Math.round(partial.regexMaxMatchCount) : DEFAULT_EDITOR_SETTINGS.regexMaxMatchCount; if (partial.autoCalculateTotalRows !== undefined) editorSettings.value.autoCalculateTotalRows = partial.autoCalculateTotalRows === true; if (partial.mongoViewMode !== undefined) editorSettings.value.mongoViewMode = partial.mongoViewMode; if (partial.showColumnCommentsInHeader !== undefined) editorSettings.value.showColumnCommentsInHeader = partial.showColumnCommentsInHeader; if (partial.showColumnTypesInHeader !== undefined) editorSettings.value.showColumnTypesInHeader = partial.showColumnTypesInHeader; if (partial.dataGridShowTransposeFieldMetadata !== undefined) editorSettings.value.dataGridShowTransposeFieldMetadata = partial.dataGridShowTransposeFieldMetadata === true; if (partial.colorizeDataGridCellTypes !== undefined) editorSettings.value.colorizeDataGridCellTypes = partial.colorizeDataGridCellTypes === true; if (partial.dataGridTypeColorSchemes !== undefined) { editorSettings.value.dataGridTypeColorSchemes = normalizeDataGridTypeColorSchemes(partial.dataGridTypeColorSchemes); // Dropping the scheme the grid is painting with has to fall back to the theme defaults. editorSettings.value.activeDataGridTypeColorSchemeId = normalizeActiveDataGridTypeColorSchemeId(editorSettings.value.dataGridTypeColorSchemes, editorSettings.value.activeDataGridTypeColorSchemeId); } if (partial.activeDataGridTypeColorSchemeId !== undefined) { editorSettings.value.activeDataGridTypeColorSchemeId = normalizeActiveDataGridTypeColorSchemeId(editorSettings.value.dataGridTypeColorSchemes, partial.activeDataGridTypeColorSchemeId); } if (partial.showIndexIndicatorsInHeader !== undefined) editorSettings.value.showIndexIndicatorsInHeader = partial.showIndexIndicatorsInHeader; if (partial.compactColumnHeaderActions !== undefined) editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions; if (partial.columnWidthDensity !== undefined) editorSettings.value.columnWidthDensity = normalizeColumnWidthDensity(partial.columnWidthDensity); if (partial.dataGridQuickEntry !== undefined) editorSettings.value.dataGridQuickEntry = partial.dataGridQuickEntry; if (partial.dataGridFilterEditorView !== undefined) editorSettings.value.dataGridFilterEditorView = normalizeDataGridFilterEditorView(partial.dataGridFilterEditorView); if (partial.dataGridTextFilterPanelHeight !== undefined) editorSettings.value.dataGridTextFilterPanelHeight = normalizeDataGridTextFilterPanelHeight(partial.dataGridTextFilterPanelHeight); if (partial.dataGridRenderMode !== undefined) editorSettings.value.dataGridRenderMode = normalizeDataGridRenderMode(partial.dataGridRenderMode); if (partial.dataGridSearchMode !== undefined) editorSettings.value.dataGridSearchMode = normalizeDataGridSearchMode(partial.dataGridSearchMode); if (partial.dataGridCopyExtractor !== undefined) editorSettings.value.dataGridCopyExtractor = normalizeDataGridCopyPreference(partial.dataGridCopyExtractor); if (partial.dataGridExtractorOptions !== undefined) editorSettings.value.dataGridExtractorOptions = normalizeDataGridExtractorOptions(partial.dataGridExtractorOptions); if (partial.resultRunDisplayMode !== undefined) editorSettings.value.resultRunDisplayMode = normalizeResultRunDisplayMode(partial.resultRunDisplayMode); if (partial.dataGridAutoTransposeSingleRow !== undefined) editorSettings.value.dataGridAutoTransposeSingleRow = partial.dataGridAutoTransposeSingleRow === true; if (partial.dataGridMultiRowTranspose !== undefined) editorSettings.value.dataGridMultiRowTranspose = partial.dataGridMultiRowTranspose === true; if (partial.dataGridHideNullColumns !== undefined) editorSettings.value.dataGridHideNullColumns = partial.dataGridHideNullColumns === true; if (partial.dataGridBooleanDisplayMode !== undefined) editorSettings.value.dataGridBooleanDisplayMode = partial.dataGridBooleanDisplayMode === "dropdown" ? "dropdown" : "checkbox"; if (partial.numericColumnRightAlign !== undefined) editorSettings.value.numericColumnRightAlign = partial.numericColumnRightAlign === true; if (partial.tableFontFamily !== undefined) editorSettings.value.tableFontFamily = normalizeFontFamily(partial.tableFontFamily, DEFAULT_EDITOR_SETTINGS.tableFontFamily); if (partial.tableFontSize !== undefined) editorSettings.value.tableFontSize = normalizeTableFontSize(partial.tableFontSize); if (partial.structureEditorDensity !== undefined) editorSettings.value.structureEditorDensity = normalizeStructureEditorDensity(partial.structureEditorDensity); if (partial.tableInfoActiveTab !== undefined) editorSettings.value.tableInfoActiveTab = normalizeTableInfoTab(partial.tableInfoActiveTab); if (partial.tableInfoDrawerWidth !== undefined) editorSettings.value.tableInfoDrawerWidth = normalizeDrawerWidth(partial.tableInfoDrawerWidth, 240, DEFAULT_EDITOR_SETTINGS.tableInfoDrawerWidth); if (partial.cellDetailDrawerWidth !== undefined) editorSettings.value.cellDetailDrawerWidth = normalizeDrawerWidth(partial.cellDetailDrawerWidth, 260, DEFAULT_EDITOR_SETTINGS.cellDetailDrawerWidth); if (partial.cellDetailPanelLayout !== undefined) editorSettings.value.cellDetailPanelLayout = normalizeCellDetailPanelLayout(partial.cellDetailPanelLayout); if (partial.cellDetailJsonFormatted !== undefined) editorSettings.value.cellDetailJsonFormatted = partial.cellDetailJsonFormatted === true; if (partial.cellDetailMetadataCollapsed !== undefined) editorSettings.value.cellDetailMetadataCollapsed = partial.cellDetailMetadataCollapsed === true; if (partial.shortcuts !== undefined) editorSettings.value.shortcuts = normalizeShortcutSettings(partial.shortcuts); if (partial.sqlFormatter !== undefined) editorSettings.value.sqlFormatter = normalizeSqlFormatterSettings(partial.sqlFormatter); if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation; if (partial.sidebarConnectionSortMode !== undefined) editorSettings.value.sidebarConnectionSortMode = normalizeConnectionListSortMode(partial.sidebarConnectionSortMode); if (partial.sidebarObjectDisplay !== undefined) editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay; if (partial.routineSourceOpenMode !== undefined) editorSettings.value.routineSourceOpenMode = partial.routineSourceOpenMode; if (partial.sidebarTableSearchEnabled !== undefined) editorSettings.value.sidebarTableSearchEnabled = partial.sidebarTableSearchEnabled; if (partial.sidebarTableSearchLocal !== undefined) editorSettings.value.sidebarTableSearchLocal = partial.sidebarTableSearchLocal; if (partial.sidebarGlobalSearchLocal !== undefined) editorSettings.value.sidebarGlobalSearchLocal = partial.sidebarGlobalSearchLocal; if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode; if (partial.sidebarOpenDatabaseOnSingleClick !== undefined) editorSettings.value.sidebarOpenDatabaseOnSingleClick = partial.sidebarOpenDatabaseOnSingleClick === true; if (partial.openTabsRestoreMode !== undefined) editorSettings.value.openTabsRestoreMode = normalizeOpenTabsRestoreMode(partial.openTabsRestoreMode); if (partial.disconnectTabHandlingMode !== undefined) editorSettings.value.disconnectTabHandlingMode = normalizeDisconnectTabHandlingMode(partial.disconnectTabHandlingMode); if (partial.dataTabReuseMode !== undefined) editorSettings.value.dataTabReuseMode = normalizeDataTabReuseMode(partial.dataTabReuseMode); if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true; if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect; if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled; if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes); if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode); if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true; if (partial.sidebarAllowHorizontalScroll !== undefined) editorSettings.value.sidebarAllowHorizontalScroll = partial.sidebarAllowHorizontalScroll; if (partial.sidebarIndent !== undefined) editorSettings.value.sidebarIndent = normalizeSidebarIndent(partial.sidebarIndent); if (partial.sidebarFontSize !== undefined) editorSettings.value.sidebarFontSize = normalizeSidebarFontSize(partial.sidebarFontSize); if (partial.columnFormatters !== undefined) editorSettings.value.columnFormatters = partial.columnFormatters; if (partial.customColumnFormatters !== undefined) editorSettings.value.customColumnFormatters = partial.customColumnFormatters; if (partial.globalDateTimeDisplayFormat !== undefined) editorSettings.value.globalDateTimeDisplayFormat = normalizeGlobalDateTimePattern(partial.globalDateTimeDisplayFormat); if (partial.globalDateTimeExportFormat !== undefined) editorSettings.value.globalDateTimeExportFormat = normalizeGlobalDateTimePattern(partial.globalDateTimeExportFormat); if (partial.globalDateTimeImportFormat !== undefined) editorSettings.value.globalDateTimeImportFormat = normalizeGlobalDateTimePattern(partial.globalDateTimeImportFormat); if (partial.snippets !== undefined) editorSettings.value.snippets = normalizeSqlSnippets(partial.snippets); if (partial.tableColumnTemplateFields !== undefined) editorSettings.value.tableColumnTemplateFields = normalizeTableColumnTemplateFields(partial.tableColumnTemplateFields); if (partial.exportBatchSize !== undefined) editorSettings.value.exportBatchSize = Math.min(100000, Math.max(100, Math.round(partial.exportBatchSize))); if (partial.exportRowLimitEnabled !== undefined) editorSettings.value.exportRowLimitEnabled = partial.exportRowLimitEnabled; if (partial.exportRowLimit !== undefined) editorSettings.value.exportRowLimit = Math.min(2147483647, Math.max(100, Math.round(partial.exportRowLimit))); if (partial.queryExportKeysetOptimizationEnabled !== undefined) editorSettings.value.queryExportKeysetOptimizationEnabled = partial.queryExportKeysetOptimizationEnabled; if (partial.updateDownloadSource !== undefined) editorSettings.value.updateDownloadSource = normalizeUpdateDownloadSource(partial.updateDownloadSource); if (partial.ignoredUpdateVersion !== undefined) editorSettings.value.ignoredUpdateVersion = typeof partial.ignoredUpdateVersion === "string" ? partial.ignoredUpdateVersion : ""; if (partial.toolbarItems !== undefined) editorSettings.value.toolbarItems = normalizeToolbarItems(partial.toolbarItems); if (partial.objectBrowserShowCheckbox !== undefined) editorSettings.value.objectBrowserShowCheckbox = partial.objectBrowserShowCheckbox === true; if (partial.objectBrowserViewMode !== undefined) editorSettings.value.objectBrowserViewMode = partial.objectBrowserViewMode === "grid" ? "grid" : "list"; if (partial.sqlVariableSubstitutionEnabled !== undefined) editorSettings.value.sqlVariableSubstitutionEnabled = partial.sqlVariableSubstitutionEnabled === true; if (partial.sqlVariableSyntaxOverrides !== undefined) editorSettings.value.sqlVariableSyntaxOverrides = normalizeSqlVariableSyntaxOverrides(partial.sqlVariableSyntaxOverrides); if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true; if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget); if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode); if (partial.flatteningMultiLineText !== undefined) editorSettings.value.flatteningMultiLineText = partial.flatteningMultiLineText; } function updateEditorSettings(partial: Partial) { applyEditorSettingsPatch(partial); markEditorSettingsPatch(partial); if (!isEditorSettingsLoaded.value) { pendingEditorSettingsPatches.push(editorSettingsPatchSnapshot(partial)); return; } saveEditorSettings(); } async function persistEditorSettings(): Promise { await initEditorSettings(); await enqueueEditorSettingsSave(); } function updateEditorSettingsAndPersist(partial: Partial): Promise { const update = editorSettingsAtomicUpdateQueue .catch(() => {}) .then(async () => { await initEditorSettings(); const previous = editorSettingsSnapshot(editorSettings.value); applyEditorSettingsPatch(partial); const revision = markEditorSettingsPatch(partial); try { await enqueueEditorSettingsSave(); } catch (error) { const restored = editorSettingsSnapshot(editorSettings.value) as unknown as Record; const previousSettings = previous as unknown as Record; let changed = false; for (const key of Object.keys(partial) as (keyof EditorSettings)[]) { if (partial[key] === undefined || editorSettingsFieldRevisions.get(key) !== revision) continue; restored[key] = previousSettings[key]; editorSettingsFieldRevisions.set(key, ++editorSettingsPatchRevision); changed = true; } if (changed) editorSettings.value = normalizeEditorSettings(restored); throw error; } }); editorSettingsAtomicUpdateQueue = update.catch(() => {}); return update; } function updateColumnFormatter(key: string, formatter: ColumnFormatterConfig | undefined) { const columnFormatters = { ...editorSettings.value.columnFormatters }; const normalized = normalizeColumnFormatter(formatter); if (normalized) { columnFormatters[key] = normalized; } else { delete columnFormatters[key]; } updateEditorSettings({ columnFormatters }); } function upsertCustomColumnFormatter(formatter: CustomColumnFormatterConfig): CustomColumnFormatterConfig | undefined { const normalized = normalizeCustomColumnFormatter(formatter); if (!normalized) return undefined; updateEditorSettings({ customColumnFormatters: { ...editorSettings.value.customColumnFormatters, [normalized.id]: normalized, }, }); return normalized; } function deleteCustomColumnFormatter(id: string) { const customColumnFormatters = { ...editorSettings.value.customColumnFormatters, }; delete customColumnFormatters[id]; const columnFormatters = Object.fromEntries( Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => { return formatter.kind !== "custom-ref" || formatter.formatterId !== id; }), ); updateEditorSettings({ customColumnFormatters, columnFormatters }); } return { settingsPageActive, settingsNavigationRequest, requestSettingsNavigation, clearSettingsNavigationRequest, activeModel, activeEffort, defaultAiMode, setDefaultAiMode, isAiConfigLoaded, aiConfigs, initAiConfigs, reloadAiConfigs, migrateToMultiConfig, createAiConfig, updateAiConfigItem, deleteAiConfig, setDefaultAiConfig, updateActiveModel, updateActiveEffort, isConfigured, isEditorSettingsLoaded, editorSettings, desktopSettings, mcpGlobalPolicy, initEditorSettings, updateEditorSettings, updateEditorSettingsAndPersist, persistEditorSettings, initDesktopSettings, updateDesktopSettings, initMcpGlobalPolicy, updateMcpGlobalPolicy, updateColumnFormatter, upsertCustomColumnFormatter, deleteCustomColumnFormatter, }; });