Files
dbx/apps/desktop/src/stores/settingsStore.ts
T

2573 lines
134 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { defaultBackgroundImageSettings, normalizeBackgroundImageSettings, type BackgroundImageSettings } from "@/lib/app/appBackgroundImage";
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, DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION, 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 { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
import { DEFAULT_QUERY_TIMEOUT_SECS, normalizeConnectTimeoutSecs, normalizeQueryTimeoutSecs } from "@/lib/connection/timeoutLimits";
import { needsTabNavigationHistoryShortcutMigration, normalizeShortcutSettings, isReservedShortcut, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import type { SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
import { type ColumnNameCopySeparator } from "@/lib/dataGrid/dataGridColumnNameCopy";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import { normalizeSidebarCopyTableNameSeparator } from "@/lib/sidebar/sidebarTableNameCopy";
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 { DEFAULT_CSV_QUOTE_MODE, normalizeCsvQuoteMode, type CsvQuoteMode } from "@/lib/export/csvQuoteMode";
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 { SqlShortcutAction, SqlSnippet, TableInfoTab } from "@/types/database";
export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, CsvQuoteMode, 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;
allowedGroupIds: string[];
allowedToolNames: string[] | null;
connectionPolicies: McpConnectionPolicy[];
groupPolicies: McpGroupPolicy[];
configured: boolean;
/** MCP query timeout override in seconds. null/undefined = inherit the connection; 0 = no limit. */
queryTimeoutSecs: number | null;
}
export interface McpGroupPolicy {
groupId: string;
readOnly: boolean;
allowDangerousSql: boolean;
}
export interface McpConnectionPolicy {
connectionId: string;
readOnly: boolean;
allowDangerousSql: boolean;
executionModeConfigured: boolean;
executionModePolicyVersion: number | null;
databaseScope: "all" | "selected" | "none";
allowedDatabases: string[];
databasePolicies: McpDatabasePolicy[];
}
export interface McpDatabasePolicy {
databaseName: string;
readOnly: boolean;
allowDangerousSql: 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 type AppCloseUnsavedTabsMode = "prompt" | "keep-drafts";
export type DefaultTransactionMode = "auto" | "manual";
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,
allowedGroupIds: [],
allowedToolNames: null,
connectionPolicies: [],
groupPolicies: [],
configured: false,
queryTimeoutSecs: null,
};
export function normalizeMcpGlobalPolicy(policy: Partial<McpGlobalPolicy> | 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()))];
const allowedGroupIds = allowedConnectionIds === null ? [] : [...new Set((policy?.allowedGroupIds ?? []).filter((id): id is string => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))];
const allowedToolNames = policy?.allowedToolNames === null || policy?.allowedToolNames === undefined ? null : [...new Set(policy.allowedToolNames.filter((name): name is string => typeof name === "string" && name.trim().length > 0).map((name) => name.trim()))];
const connectionPolicies = Object.values(
(policy?.connectionPolicies ?? []).reduce<Record<string, McpConnectionPolicy>>((rules, rule) => {
if (!rule || typeof rule.connectionId !== "string" || !rule.connectionId.trim()) return rules;
const databaseScope = rule.databaseScope === "selected" || rule.databaseScope === "none" ? rule.databaseScope : "all";
const allowedDatabases = databaseScope === "selected" ? [...new Set((rule.allowedDatabases ?? []).filter((database): database is string => typeof database === "string" && database.trim().length > 0).map((database) => database.trim()))] : [];
const allowedDatabaseNames = new Set(allowedDatabases);
const databasePolicies = Object.values(
(databaseScope === "selected" ? (rule.databasePolicies ?? []) : []).reduce<Record<string, McpDatabasePolicy>>((policies, databasePolicy) => {
if (!databasePolicy || typeof databasePolicy.databaseName !== "string") return policies;
const databaseName = databasePolicy.databaseName.trim();
if (!databaseName || !allowedDatabaseNames.has(databaseName)) return policies;
const current = policies[databaseName];
const readOnly = current?.readOnly === true || databasePolicy.readOnly === true;
policies[databaseName] = {
databaseName,
readOnly,
allowDangerousSql: !readOnly && (current ? current.allowDangerousSql && databasePolicy.allowDangerousSql === true : databasePolicy.allowDangerousSql === true),
};
return policies;
}, {}),
);
rules[rule.connectionId.trim()] = {
connectionId: rule.connectionId.trim(),
readOnly: rule.readOnly === true,
allowDangerousSql: rule.readOnly !== true && rule.allowDangerousSql === true,
executionModeConfigured: rule.executionModeConfigured !== false,
executionModePolicyVersion: rule.executionModePolicyVersion === 1 ? 1 : null,
databaseScope,
allowedDatabases,
databasePolicies,
};
return rules;
}, {}),
);
const groupPolicies = Object.values(
(policy?.groupPolicies ?? []).reduce<Record<string, McpGroupPolicy>>((rules, rule) => {
if (!rule || typeof rule.groupId !== "string" || !rule.groupId.trim()) return rules;
const groupId = rule.groupId.trim();
const current = rules[groupId];
const readOnly = current?.readOnly === true || rule.readOnly === true;
rules[groupId] = {
groupId,
readOnly,
allowDangerousSql: !readOnly && (current ? current.allowDangerousSql && rule.allowDangerousSql === true : rule.allowDangerousSql === true),
};
return rules;
}, {}),
);
// null / undefined / non-positive => null (inherit connection). 0 is preserved
// as an explicit "no limit" only here; the UI maps "no limit" <=> 0 and
// "inherit" <=> null.
const queryTimeoutSecs = policy?.queryTimeoutSecs === null || policy?.queryTimeoutSecs === undefined ? null : typeof policy.queryTimeoutSecs === "number" && Number.isFinite(policy.queryTimeoutSecs) && policy.queryTimeoutSecs >= 0 ? Math.round(policy.queryTimeoutSecs) : null;
return {
readOnly: policy?.readOnly === true,
allowDangerousSql: policy?.allowDangerousSql === true,
allowedConnectionIds,
allowedGroupIds,
allowedToolNames,
connectionPolicies,
groupPolicies,
configured: policy?.configured === true,
queryTimeoutSecs,
};
}
export function normalizeDesktopSettings(settings: Partial<DesktopSettings> | 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<AiConfig, "apiKey"> {
label: string;
iconSlug?: string;
iconPath?: string;
requiresApiKey: boolean;
group?: "builtin" | "partner";
}
export interface AiPartnerProviderPreset extends AiProviderPreset {
id: string;
group: "partner";
websiteUrl: string;
apiKeyUrl: string;
descriptionKey: string;
}
export const AI_PROVIDER_PRESETS: Record<AiProvider, AiProviderPreset> = {
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,
},
kimi: {
label: "Kimi",
provider: "kimi",
endpoint: "https://api.moonshot.cn/v1",
model: "",
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,
},
zhipu: {
label: "Zhipu",
iconSlug: "zhipu",
provider: "zhipu",
endpoint: "https://open.bigmodel.cn/api/paas/v4",
model: "glm-5.3",
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,
},
};
/** Brand names stay as preset labels; only the generic "custom" entry is localized. */
export function aiProviderLabel(provider: AiProvider, t: (key: string) => string): string {
if (provider === "custom") return t("ai.providerCustom");
return AI_PROVIDER_PRESETS[provider].label;
}
export const AI_PROVIDER_PARTNER_PRESETS: readonly AiPartnerProviderPreset[] = [
{
id: "jalapeno-cloud",
label: "Jalapeno Cloud",
iconPath: "/icons/ai/jalapeno-cloud.png",
group: "partner",
provider: "openai-compatible",
endpoint: "https://api.jalapeno-cloud.ai/v1",
model: "GLM-5.2",
models: [{ name: "GLM-5.2" }, { name: "DeepSeek-V4-Pro" }, { name: "MiniMax-M3" }],
apiStyle: "completions",
authMethod: "bearer",
requiresApiKey: true,
websiteUrl: "https://www.jalapeno-cloud.ai/dbx",
apiKeyUrl: "https://www.jalapeno-cloud.ai/dbx",
descriptionKey: "ai.jalapenoDescription",
},
{
id: "hualong-ai",
label: "HuaLongAI",
iconPath: "/icons/ai/hualong-ai.png",
group: "partner",
provider: "openai-compatible",
endpoint: "https://api.hualong.online/v1",
model: "gpt-5.6-terra",
models: [{ name: "gpt-5.6-terra" }],
apiStyle: "completions",
authMethod: "bearer",
requiresApiKey: true,
websiteUrl: "https://api.hualong.online/register?promo=DBX%26HUALONG",
apiKeyUrl: "https://api.hualong.online/register?promo=DBX%26HUALONG",
descriptionKey: "ai.hualongDescription",
},
];
function normalizeAiProviderEndpoint(endpoint: string): string {
return endpoint.trim().replace(/\/+$/, "").toLowerCase();
}
export function getAiProviderPreset(provider: AiProvider, endpoint = ""): AiProviderPreset | AiPartnerProviderPreset {
const normalizedEndpoint = normalizeAiProviderEndpoint(endpoint);
const partnerPreset = AI_PROVIDER_PARTNER_PRESETS.find((preset) => preset.provider === provider && normalizeAiProviderEndpoint(preset.endpoint) === normalizedEndpoint);
return partnerPreset ?? AI_PROVIDER_PRESETS[provider];
}
export function getAiProviderPresetOption(id: string): AiProviderPreset | AiPartnerProviderPreset {
return AI_PROVIDER_PARTNER_PRESETS.find((preset) => preset.id === id) ?? AI_PROVIDER_PRESETS[id as AiProvider] ?? AI_PROVIDER_PRESETS.custom;
}
export function getAiProviderPresetDefaultEndpoint(preset: AiProviderPreset | AiPartnerProviderPreset, locale: string): string {
if (preset.provider === "minimax" && locale === "zh-CN") {
return "https://api.minimaxi.com/v1";
}
if (preset.provider === "zhipu" && locale !== "zh-CN") {
return "https://api.z.ai/api/paas/v4";
}
return preset.endpoint;
}
export function getAiProviderPresetId(provider: AiProvider, endpoint = ""): string {
const preset = getAiProviderPreset(provider, endpoint);
return "id" in preset ? preset.id : provider;
}
export function isAiPartnerProviderPreset(preset: AiProviderPreset | AiPartnerProviderPreset): preset is AiPartnerProviderPreset {
return preset.group === "partner";
}
const defaultConfigs: Record<AiProvider, Omit<AiConfig, "apiKey">> = Object.fromEntries(
Object.entries(AI_PROVIDER_PRESETS).map(([provider, preset]) => {
const { label: _label, iconSlug: _iconSlug, iconPath: _iconPath, group: _group, requiresApiKey: _requiresApiKey, ...config } = preset;
return [provider, config];
}),
) as Record<AiProvider, Omit<AiConfig, "apiKey">>;
const AI_REASONING_LEVELS: AiReasoningLevel[] = ["default", "minimal", "low", "medium", "high", "xhigh", "max"];
export const AI_OUTPUT_TOKENS_MIN = 256;
export const AI_OUTPUT_TOKENS_MAX = 1_000_000;
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<string, string> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const result: Record<string, string> = {};
for (const [rawKey, rawValue] of Object.entries(value as Record<string, unknown>)) {
const key = rawKey.trim();
if (!key || !AI_ENV_KEY_RE.test(key)) continue;
result[key] = rawValue == null ? "" : String(rawValue);
}
return result;
}
export function normalizeAiHeaders(value: unknown): Record<string, string> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const result: Record<string, string> = {};
for (const [rawName, rawValue] of Object.entries(value as Record<string, unknown>)) {
const name = rawName.trim();
if (!name) continue;
result[name] = rawValue == null ? "" : String(rawValue);
}
return result;
}
export function normalizeAiConfig(config: Partial<AiConfig> | null | undefined): AiConfig {
const provider = config?.provider && config.provider in AI_PROVIDER_PRESETS ? config.provider : inferAiProviderFromConfig(config);
const rawMaxOutputTokens = config?.maxOutputTokens;
const maxOutputTokens = typeof rawMaxOutputTokens === "number" && Number.isFinite(rawMaxOutputTokens) && rawMaxOutputTokens >= AI_OUTPUT_TOKENS_MIN ? Math.min(AI_OUTPUT_TOKENS_MAX, Math.round(rawMaxOutputTokens)) : undefined;
return {
...defaultConfigs[provider],
...config,
provider,
apiKey: (config?.apiKey ?? "").trim(),
apiStyle: config?.apiStyle ?? defaultConfigs[provider].apiStyle,
customHeaders: normalizeAiHeaders(config?.customHeaders),
authMethod: config?.authMethod ?? defaultConfigs[provider].authMethod,
proxyEnabled: !!config?.proxyEnabled,
proxyUrl: config?.proxyUrl ?? "",
skipTlsVerify: !!config?.skipTlsVerify,
enableThinking: config?.enableThinking ?? true,
reasoningLevel: normalizeAiReasoningLevel(config?.reasoningLevel),
maxOutputTokens,
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<AiConfig> | 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("moonshot") || endpoint.includes("kimi.com") || model.includes("kimi")) return "kimi";
if (endpoint.includes("dashscope") || endpoint.includes("aliyuncs") || model.includes("qwen")) return "qwen";
if (endpoint.includes("bigmodel.cn") || endpoint.includes("api.z.ai") || model.startsWith("glm-")) return "zhipu";
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 TAB_PLACEMENTS = ["top", "bottom", "left", "right"] as const;
export type TabPlacement = (typeof TAB_PLACEMENTS)[number];
const TAB_GROUP_MODES = ["none", "database-type", "database", "connection"] as const;
export type TabGroupMode = (typeof TAB_GROUP_MODES)[number];
export interface TabGroupCustomization {
name?: string;
color?: string;
}
const TAB_SORT_MODES = ["manual", "created-asc", "title-asc"] as const;
export type TabSortMode = (typeof TAB_SORT_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];
const MULTI_STATEMENT_DEFAULT_VIEWS = ["result", "summary"] as const;
export type MultiStatementDefaultView = (typeof MULTI_STATEMENT_DEFAULT_VIEWS)[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;
backgroundImage: BackgroundImageSettings;
customThemeColors: CustomThemeColors;
customThemes: CustomTheme[];
activeCustomThemeId: string;
executeMode: "all" | "current";
executeModeDefaultVersion: number;
executeAllOnBlankLine: boolean;
/** Whether DBX blocks Redis commands classified as high risk. */
blockDangerousRedisCommands: boolean;
globalConnectTimeoutSecs: number;
connectTimeoutInheritConnectionIds: string[];
globalQueryTimeoutSecs: number;
queryTimeoutInheritConnectionIds: string[];
timeoutInheritanceMigrationVersion: number;
showExecutionTargetPicker: boolean;
showStatementRunButtons: boolean;
showLineNumbers: boolean;
showCurrentStatementFrame: boolean;
showInsertValueHints: boolean;
autoAliasTables: boolean;
insertSpaceAfterCompletion: boolean;
sortCompletionColumnsAlphabetically: boolean;
selectFirstCompletionOnOpen: boolean;
wordWrap: boolean;
tableDdlWordWrap: boolean;
refreshDdlOnOpen: boolean;
vimModeEnabled: boolean;
autoCloseBrackets: boolean;
sqlSemanticDiagnosticsMode: SqlSemanticDiagnosticsMode;
sqlSemanticDiagnosticsEnabled: boolean;
confirmDangerousSqlExecution: boolean;
confirmUnsavedSqlClose: boolean;
appCloseUnsavedTabsMode: AppCloseUnsavedTabsMode;
savedSqlOpenTargetMode: SavedSqlOpenTargetMode;
compactTabTitle: boolean;
tabLayout: TabLayoutMode;
tabPlacement: TabPlacement;
tabGroupMode: TabGroupMode;
tabGroupCustomizations: Record<string, TabGroupCustomization>;
tabSortMode: TabSortMode;
appLayout: "separated" | "classic";
pageSize: number;
tableOpenPageSize: number;
queryResultMaxRowsEnabled: boolean;
queryResultMaxRows: number;
externalSqlEditorMaxMb: number;
infiniteScroll: boolean;
/** Preserved for downgrade compatibility; current clients use queryResultMaxRows. */
infiniteScrollMaxRows: number;
flatteningMultiLineText: boolean;
dataGridShowWhitespace: 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;
dataGridKeepFilterEditorExpanded: boolean;
dataGridTextFilterPanelHeight: number;
localFilterPopoverWidth: number;
dataGridRenderMode: DataGridRenderMode;
dataGridSearchMode: DataGridSearchMode;
dataGridCopyExtractor: DataGridCopyPreference;
dataGridExtractorOptions: DataGridExtractorOptions;
dataGridExtractorOptionsMigrationVersion: number;
resultRunDisplayMode: ResultRunDisplayMode;
defaultAutoKeepResults: boolean;
multiStatementDefaultView: MultiStatementDefaultView;
dataGridAutoTransposeSingleRow: boolean;
dataGridCellDetailButtonVisible: boolean;
dataGridCrosshairHighlight: boolean;
dataGridMultiRowTranspose: boolean;
dataGridHideNullColumns: boolean;
dataGridBooleanDisplayMode: "dropdown" | "checkbox";
numericColumnRightAlign: boolean;
tableFontFamily: string;
tableFontSize: number;
structureEditorDensity: StructureEditorDensity;
tableInfoActiveTab: TableInfoTab;
tableInfoDrawerPinned: boolean;
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;
sidebarBrowseObjectsOnDatabaseActivation: boolean;
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: number;
openTabsRestoreMode: OpenTabsRestoreMode;
disconnectTabHandlingMode: DisconnectTabHandlingMode;
dataTabReuseMode: DataTabReuseMode;
openDataTabsNextToActive: boolean;
prefillNewQueryWithSelect: boolean;
generateSqlIncludeDatabaseName: boolean;
generateSqlQuoteIdentifiers: boolean;
formatSqlOnSqlFileSave: boolean;
updateNotificationsEnabled: boolean;
autoDownloadUpdates: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarCopyTableNameSeparator: ColumnNameCopySeparator;
sidebarCopyTableNameIncludeSchema: boolean;
sidebarObjectInfoMode: SidebarObjectInfoMode;
sidebarShowConnectionNotes: boolean;
sidebarShowTooltips: boolean;
sidebarAllowHorizontalScroll: boolean;
sidebarIndent: number;
sidebarFontSize: number;
columnFormatters: Record<string, ColumnFormatterConfig>;
customColumnFormatters: Record<string, CustomColumnFormatterConfig>;
globalDateTimeDisplayFormat: string;
globalDateTimeExportFormat: string;
globalDateTimeImportFormat: string;
snippets: SqlSnippet[];
sqlShortcuts: SqlShortcutAction[];
tableColumnTemplateFields: string[];
exportBatchSize: number;
csvQuoteMode: CsvQuoteMode;
/** Global Redis key-search templates; overridden by non-empty connection templates. */
redisKeyTemplates: string[];
exportRowLimitEnabled: boolean;
exportRowLimit: number;
queryExportKeysetOptimizationEnabled: boolean;
updateDownloadSource: UpdateDownloadSource;
ignoredUpdateVersion: string;
toolbarItems: ToolbarItems;
objectBrowserShowCheckbox: boolean;
objectBrowserViewMode: "list" | "grid";
sqlVariableSubstitutionEnabled: boolean;
sqlVariableSyntaxOverrides: SqlVariableSyntaxOverrides;
continueOnErrorOnBatch: boolean;
showTableDdlHoverPreview: boolean;
clickTableNavigationTarget: ClickTableNavigationTarget;
completionTriggerMode: SqlCompletionTriggerMode;
defaultTransactionMode: DefaultTransactionMode;
}
export interface ToolbarItems {
dataTransfer: boolean;
driverManager: boolean;
pluginCenter: 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,
pluginCenter: 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<RightSidebarPanelId, boolean>;
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<EditorTheme>(EDITOR_THEMES.map((theme) => theme.value));
export const EXECUTE_MODE_CURRENT_DEFAULT_VERSION = 1;
export const SIDEBAR_BROWSE_OBJECTS_MIGRATION_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",
backgroundImage: defaultBackgroundImageSettings(),
customThemeColors: { ...DEFAULT_CUSTOM_THEME_COLORS },
customThemes: [...DEFAULT_CUSTOM_THEMES],
activeCustomThemeId: "default",
executeMode: "current",
executeModeDefaultVersion: EXECUTE_MODE_CURRENT_DEFAULT_VERSION,
executeAllOnBlankLine: false,
blockDangerousRedisCommands: true,
globalConnectTimeoutSecs: 10,
connectTimeoutInheritConnectionIds: [],
globalQueryTimeoutSecs: DEFAULT_QUERY_TIMEOUT_SECS,
queryTimeoutInheritConnectionIds: [],
timeoutInheritanceMigrationVersion: 2,
showExecutionTargetPicker: false,
showStatementRunButtons: true,
showLineNumbers: true,
showCurrentStatementFrame: true,
showInsertValueHints: true,
autoAliasTables: true,
insertSpaceAfterCompletion: true,
sortCompletionColumnsAlphabetically: true,
selectFirstCompletionOnOpen: true,
wordWrap: false,
tableDdlWordWrap: true,
refreshDdlOnOpen: false,
vimModeEnabled: false,
autoCloseBrackets: true,
sqlSemanticDiagnosticsMode: "auto",
sqlSemanticDiagnosticsEnabled: SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED,
confirmDangerousSqlExecution: true,
confirmUnsavedSqlClose: true,
appCloseUnsavedTabsMode: "keep-drafts",
savedSqlOpenTargetMode: "saved",
compactTabTitle: false,
tabLayout: "scroll",
tabPlacement: "top",
tabGroupMode: "none",
tabGroupCustomizations: {},
tabSortMode: "manual",
appLayout: "classic",
pageSize: 100,
tableOpenPageSize: 100,
queryResultMaxRowsEnabled: true,
queryResultMaxRows: DEFAULT_QUERY_RESULT_MAX_ROWS,
externalSqlEditorMaxMb: 64,
infiniteScroll: false,
infiniteScrollMaxRows: 5000,
flatteningMultiLineText: false,
dataGridShowWhitespace: 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",
dataGridKeepFilterEditorExpanded: false,
dataGridTextFilterPanelHeight: DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT,
localFilterPopoverWidth: 360,
dataGridRenderMode: "canvas",
dataGridSearchMode: "filter",
dataGridCopyExtractor: "smart",
dataGridExtractorOptions: normalizeDataGridExtractorOptions(DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS),
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
resultRunDisplayMode: "tabs",
defaultAutoKeepResults: false,
multiStatementDefaultView: "result",
dataGridAutoTransposeSingleRow: false,
dataGridCellDetailButtonVisible: true,
dataGridCrosshairHighlight: false,
dataGridMultiRowTranspose: false,
dataGridHideNullColumns: false,
dataGridBooleanDisplayMode: "dropdown",
numericColumnRightAlign: true,
tableFontFamily: DEFAULT_DATA_GRID_FONT_FAMILY,
tableFontSize: TABLE_FONT_SIZE_DEFAULT,
structureEditorDensity: "compact",
tableInfoActiveTab: "ddl",
tableInfoDrawerPinned: false,
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,
sidebarBrowseObjectsOnDatabaseActivation: false,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
openTabsRestoreMode: "all",
disconnectTabHandlingMode: "close-tabs",
dataTabReuseMode: DEFAULT_DATA_TAB_REUSE_MODE,
openDataTabsNextToActive: false,
prefillNewQueryWithSelect: true,
generateSqlIncludeDatabaseName: false,
generateSqlQuoteIdentifiers: true,
formatSqlOnSqlFileSave: false,
updateNotificationsEnabled: true,
autoDownloadUpdates: false,
sidebarHiddenTablePrefixes: [],
sidebarCopyTableNameSeparator: "comma",
sidebarCopyTableNameIncludeSchema: false,
sidebarObjectInfoMode: "comment-inline",
sidebarShowConnectionNotes: false,
sidebarShowTooltips: true,
sidebarAllowHorizontalScroll: false,
sidebarIndent: SIDEBAR_INDENT_DEFAULT,
sidebarFontSize: SIDEBAR_FONT_SIZE_DEFAULT,
columnFormatters: {},
customColumnFormatters: {},
globalDateTimeDisplayFormat: "",
globalDateTimeExportFormat: "",
globalDateTimeImportFormat: "",
snippets: DEFAULT_SQL_SNIPPETS,
sqlShortcuts: [],
tableColumnTemplateFields: [...DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS],
exportBatchSize: 2000,
csvQuoteMode: DEFAULT_CSV_QUOTE_MODE,
redisKeyTemplates: [],
exportRowLimitEnabled: false,
exportRowLimit: 100000,
queryExportKeysetOptimizationEnabled: true,
updateDownloadSource: "official",
ignoredUpdateVersion: "",
toolbarItems: { ...DEFAULT_TOOLBAR_ITEMS },
objectBrowserShowCheckbox: false,
objectBrowserViewMode: "list",
sqlVariableSubstitutionEnabled: true,
sqlVariableSyntaxOverrides: {},
continueOnErrorOnBatch: false,
showTableDdlHoverPreview: true,
clickTableNavigationTarget: "data",
completionTriggerMode: "positional",
defaultTransactionMode: "auto",
};
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.7;
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 normalizeTabPlacement(value: unknown): TabPlacement {
return TAB_PLACEMENTS.includes(value as TabPlacement) ? (value as TabPlacement) : DEFAULT_EDITOR_SETTINGS.tabPlacement;
}
function normalizeTabGroupMode(value: unknown): TabGroupMode {
return TAB_GROUP_MODES.includes(value as TabGroupMode) ? (value as TabGroupMode) : DEFAULT_EDITOR_SETTINGS.tabGroupMode;
}
export function normalizeTabGroupCustomizations(value: unknown): Record<string, TabGroupCustomization> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const customizations: Record<string, TabGroupCustomization> = {};
for (const [rawKey, rawCustomization] of Object.entries(value as Record<string, unknown>).slice(0, 200)) {
const key = rawKey.trim().slice(0, 512);
if (!key || !rawCustomization || typeof rawCustomization !== "object" || Array.isArray(rawCustomization)) continue;
const customization = rawCustomization as Record<string, unknown>;
const name = typeof customization.name === "string" ? customization.name.trim().slice(0, 80) : "";
const color = typeof customization.color === "string" && /^#[0-9a-f]{6}$/i.test(customization.color.trim()) ? customization.color.trim().toLowerCase() : "";
if (name || color) customizations[key] = { ...(name ? { name } : {}), ...(color ? { color } : {}) };
}
return customizations;
}
function normalizeTabSortMode(value: unknown): TabSortMode {
return TAB_SORT_MODES.includes(value as TabSortMode) ? (value as TabSortMode) : DEFAULT_EDITOR_SETTINGS.tabSortMode;
}
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 normalizeMultiStatementDefaultView(value: unknown): MultiStatementDefaultView {
return MULTI_STATEMENT_DEFAULT_VIEWS.includes(value as MultiStatementDefaultView) ? (value as MultiStatementDefaultView) : DEFAULT_EDITOR_SETTINGS.multiStatementDefaultView;
}
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 normalizeDefaultTransactionMode(value: unknown): DefaultTransactionMode {
return value === "manual" ? value : DEFAULT_EDITOR_SETTINGS.defaultTransactionMode;
}
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 normalizeAppCloseUnsavedTabsMode(value: unknown): AppCloseUnsavedTabsMode {
return value === "prompt" || value === "keep-drafts" ? value : DEFAULT_EDITOR_SETTINGS.appCloseUnsavedTabsMode;
}
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<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, ColumnFormatterConfig> = {};
for (const [key, formatter] of Object.entries(value as Record<string, unknown>)) {
const normalized = normalizeColumnFormatter(formatter);
if (normalized) formatters[key] = normalized;
}
return formatters;
}
function normalizeCustomColumnFormatters(value: unknown): Record<string, CustomColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, CustomColumnFormatterConfig> = {};
for (const formatter of Object.values(value as Record<string, unknown>)) {
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<string>();
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 normalizeSqlShortcuts(value: unknown, existing?: SqlShortcutAction[]): SqlShortcutAction[] {
if (!Array.isArray(value)) return existing ?? DEFAULT_EDITOR_SETTINGS.sqlShortcuts;
const valid: SqlShortcutAction[] = [];
for (const item of value) {
if (!item || typeof item !== "object" || typeof item.id !== "string" || !item.id || typeof item.label !== "string" || !item.label || typeof item.shortcut !== "string" || typeof item.sql !== "string") {
continue;
}
const shortcut = item.shortcut.trim();
// SQL 快捷键走 createQueryEditorSqlShortcutDomHandler:匹配后 preventDefault,
// 与普通动作一样会重新劫持 macOS 的 ⌘H。此处直接丢弃保留组合——SQL 快捷键
// 没有“平台默认值”这一概念(它是用户自定义模板的专属触发键),清空即视为未绑定。
const normalizedShortcut = isReservedShortcut(shortcut) ? "" : shortcut;
valid.push({
id: item.id,
label: item.label,
shortcut: normalizedShortcut,
sql: item.sql,
enabled: item.enabled !== false,
});
}
return valid;
}
function normalizeToolbarItems(items: Partial<ToolbarItems> | 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,
pluginCenter: items.pluginCenter ?? defaults.pluginCenter,
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<TableInfoTab>(["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<EditorSettings>, existing?: EditorSettings): EditorSettings {
const legacyTimeoutSettings = settings as Partial<EditorSettings> & { queryTimeoutSecs?: unknown; queryTimeoutInheritanceMigrationVersion?: unknown };
const legacySidebarOpenDatabaseOnSingleClick = (settings as Partial<EditorSettings> & { sidebarOpenDatabaseOnSingleClick?: unknown }).sidebarOpenDatabaseOnSingleClick;
const legacyDataGridAutoHideFilterBuilder = (settings as Partial<EditorSettings> & { dataGridAutoHideFilterBuilder?: unknown }).dataGridAutoHideFilterBuilder;
const hasDataGridKeepFilterEditorExpanded = Object.prototype.hasOwnProperty.call(settings, "dataGridKeepFilterEditorExpanded");
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);
const savedExtractorMigrationVersion = settings.dataGridExtractorOptionsMigrationVersion;
const normalizedExtractorOptions = normalizeDataGridExtractorOptions(settings.dataGridExtractorOptions);
const isLegacyExtractorOptions = typeof savedExtractorMigrationVersion !== "number" || savedExtractorMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION;
const dataGridExtractorOptions = isLegacyExtractorOptions && normalizedExtractorOptions.dsv.nullText === "NULL" ? { ...normalizedExtractorOptions, dsv: { ...normalizedExtractorOptions.dsv, nullText: "" } } : normalizedExtractorOptions;
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,
blockDangerousRedisCommands: typeof settings.blockDangerousRedisCommands === "boolean" ? settings.blockDangerousRedisCommands : DEFAULT_EDITOR_SETTINGS.blockDangerousRedisCommands,
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,
showLineNumbers: typeof settings.showLineNumbers === "boolean" ? settings.showLineNumbers : DEFAULT_EDITOR_SETTINGS.showLineNumbers,
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,
selectFirstCompletionOnOpen: typeof settings.selectFirstCompletionOnOpen === "boolean" ? settings.selectFirstCompletionOnOpen : DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
tableDdlWordWrap: typeof settings.tableDdlWordWrap === "boolean" ? settings.tableDdlWordWrap : DEFAULT_EDITOR_SETTINGS.tableDdlWordWrap,
refreshDdlOnOpen: typeof settings.refreshDdlOnOpen === "boolean" ? settings.refreshDdlOnOpen : DEFAULT_EDITOR_SETTINGS.refreshDdlOnOpen,
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,
appCloseUnsavedTabsMode: normalizeAppCloseUnsavedTabsMode(settings.appCloseUnsavedTabsMode),
savedSqlOpenTargetMode: settings.savedSqlOpenTargetMode === "current" ? "current" : DEFAULT_EDITOR_SETTINGS.savedSqlOpenTargetMode,
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
tabLayout: normalizeTabLayout(settings.tabLayout),
tabPlacement: normalizeTabPlacement(settings.tabPlacement),
tabGroupMode: normalizeTabGroupMode(settings.tabGroupMode),
tabGroupCustomizations: normalizeTabGroupCustomizations(settings.tabGroupCustomizations),
tabSortMode: normalizeTabSortMode(settings.tabSortMode),
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),
externalSqlEditorMaxMb: normalizeExternalSqlEditorMaxMb(settings.externalSqlEditorMaxMb),
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,
dataGridShowWhitespace: settings.dataGridShowWhitespace ?? DEFAULT_EDITOR_SETTINGS.dataGridShowWhitespace,
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),
dataGridKeepFilterEditorExpanded: typeof settings.dataGridKeepFilterEditorExpanded === "boolean" ? settings.dataGridKeepFilterEditorExpanded : hasDataGridKeepFilterEditorExpanded ? false : legacyDataGridAutoHideFilterBuilder === false,
dataGridTextFilterPanelHeight: normalizeDataGridTextFilterPanelHeight(settings.dataGridTextFilterPanelHeight),
localFilterPopoverWidth: normalizeDrawerWidth(settings.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth),
dataGridRenderMode: normalizeDataGridRenderMode(settings.dataGridRenderMode),
dataGridSearchMode: normalizeDataGridSearchMode(settings.dataGridSearchMode),
dataGridCopyExtractor: normalizeDataGridCopyPreference(settings.dataGridCopyExtractor),
dataGridExtractorOptions,
dataGridExtractorOptionsMigrationVersion: DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION,
resultRunDisplayMode: normalizeResultRunDisplayMode(settings.resultRunDisplayMode),
defaultAutoKeepResults: settings.defaultAutoKeepResults === true,
multiStatementDefaultView: normalizeMultiStatementDefaultView(settings.multiStatementDefaultView),
dataGridAutoTransposeSingleRow: settings.dataGridAutoTransposeSingleRow === true,
dataGridCellDetailButtonVisible: typeof settings.dataGridCellDetailButtonVisible === "boolean" ? settings.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible,
dataGridCrosshairHighlight: typeof settings.dataGridCrosshairHighlight === "boolean" ? settings.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight,
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),
tableInfoDrawerPinned: settings.tableInfoDrawerPinned === true,
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,
sidebarBrowseObjectsOnDatabaseActivation:
typeof settings.sidebarBrowseObjectsOnDatabaseActivation === "boolean"
? settings.sidebarBrowseObjectsOnDatabaseActivation
: typeof legacySidebarOpenDatabaseOnSingleClick === "boolean"
? // The legacy false value only disabled single-click browsing; double-click still opened the browser.
true
: DEFAULT_EDITOR_SETTINGS.sidebarBrowseObjectsOnDatabaseActivation,
sidebarBrowseObjectsOnDatabaseActivationMigrationVersion:
typeof settings.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion === "number" && settings.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion >= SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION
? Math.floor(settings.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion)
: SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION,
openTabsRestoreMode: normalizeOpenTabsRestoreMode(
(settings as Partial<EditorSettings>).openTabsRestoreMode,
(
settings as Partial<EditorSettings> & {
restoreOpenTabsOnLaunch?: boolean;
}
).restoreOpenTabsOnLaunch,
),
disconnectTabHandlingMode: normalizeDisconnectTabHandlingMode(
(settings as Partial<EditorSettings>).disconnectTabHandlingMode,
(
settings as Partial<EditorSettings> & {
closeQueryTabsOnDisconnect?: boolean;
}
).closeQueryTabsOnDisconnect,
),
dataTabReuseMode: normalizeDataTabReuseMode(
settings.dataTabReuseMode,
(
settings as Partial<EditorSettings> & {
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,
generateSqlIncludeDatabaseName: settings.generateSqlIncludeDatabaseName === true,
generateSqlQuoteIdentifiers: typeof settings.generateSqlQuoteIdentifiers === "boolean" ? settings.generateSqlQuoteIdentifiers : DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers,
formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true,
updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled,
autoDownloadUpdates: settings.autoDownloadUpdates === true,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
sidebarCopyTableNameSeparator: normalizeSidebarCopyTableNameSeparator(settings.sidebarCopyTableNameSeparator),
sidebarCopyTableNameIncludeSchema: settings.sidebarCopyTableNameIncludeSchema === true,
sidebarObjectInfoMode: normalizeSidebarObjectInfoMode(
settings.sidebarObjectInfoMode,
(
settings as Partial<EditorSettings> & {
sidebarTableCommentLayout?: string;
}
).sidebarTableCommentLayout,
(
settings as Partial<EditorSettings> & {
sidebarHideTableComments?: boolean;
}
).sidebarHideTableComments,
(
settings as Partial<EditorSettings> & {
sidebarShowDatabaseSizes?: boolean;
}
).sidebarShowDatabaseSizes,
),
sidebarShowConnectionNotes: settings.sidebarShowConnectionNotes === true,
sidebarShowTooltips: settings.sidebarShowTooltips ?? DEFAULT_EDITOR_SETTINGS.sidebarShowTooltips,
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),
sqlShortcuts: normalizeSqlShortcuts(settings.sqlShortcuts, existing?.sqlShortcuts),
tableColumnTemplateFields: normalizeTableColumnTemplateFields(settings.tableColumnTemplateFields),
exportBatchSize: typeof settings.exportBatchSize === "number" && settings.exportBatchSize >= 100 && settings.exportBatchSize <= 100000 ? Math.round(settings.exportBatchSize) : DEFAULT_EDITOR_SETTINGS.exportBatchSize,
csvQuoteMode: normalizeCsvQuoteMode(settings.csvQuoteMode),
redisKeyTemplates: normalizeRedisKeyTemplates(settings.redisKeyTemplates),
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,
showTableDdlHoverPreview: typeof settings.showTableDdlHoverPreview === "boolean" ? settings.showTableDdlHoverPreview : DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview,
clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget),
completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode),
defaultTransactionMode: normalizeDefaultTransactionMode(settings.defaultTransactionMode),
backgroundImage: normalizeBackgroundImageSettings(settings.backgroundImage),
};
}
/**
* Cloud snapshots are applied as partial settings updates rather than loaded
* through `normalizeEditorSettings`. Translate the removed inverse flag here
* so older snapshots retain their behavior on every incremental update path.
*/
function migrateLegacyFilterEditorExpansionPatch(partial: Partial<EditorSettings>): Partial<EditorSettings> {
const patch = partial as Partial<EditorSettings> & { dataGridAutoHideFilterBuilder?: unknown };
const { dataGridAutoHideFilterBuilder: legacyAutoHide, ...current } = patch;
if (Object.prototype.hasOwnProperty.call(patch, "dataGridKeepFilterEditorExpanded") || typeof legacyAutoHide !== "boolean") {
return current;
}
return { ...current, dataGridKeepFilterEditorExpanded: !legacyAutoHide };
}
function loadLegacyEditorSettings(): EditorSettings | null {
const raw = safeLocalStorageGet(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as Partial<EditorSettings>;
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<EditorSettings>): Partial<EditorSettings> {
return JSON.parse(JSON.stringify(settings)) as Partial<EditorSettings>;
}
/** Keep only well-formed, non-empty template id lists keyed by db_type. */
function normalizeTemplateIdsByDbType(value?: Record<string, string[]>): Record<string, string[]> {
if (!value) return {};
const out: Record<string, string[]> = {};
for (const [dbType, ids] of Object.entries(value)) {
if (!Array.isArray(ids)) continue;
const cleaned = [...new Set(ids.filter((id): id is string => typeof id === "string" && id.trim() !== "").map((id) => id.trim()))];
if (cleaned.length > 0) out[dbType] = cleaned;
}
return out;
}
export interface SettingsNavigationRequest {
id: number;
tab: string;
section?: string;
}
export const useSettingsStore = defineStore("settings", () => {
const settingsPageActive = ref(false);
const settingsNavigationRequest = ref<SettingsNavigationRequest | null>(null);
const activeModel = ref<{ configId: string; modelId: string } | null>(null);
const effortPreferences = ref<AiModelEffortPreference[]>([]);
const defaultAiMode = ref<AiAssistantMode>("ask");
const restoreLastConversation = ref(false);
// Per-db_type prompt template defaults (explicit opt-in) and last-used
// fallback; both resolved when an AI panel mounts or its namespace changes.
const aiDefaultTemplatesByDbType = ref<Record<string, string[]>>({});
const aiLastUsedTemplatesByDbType = ref<Record<string, string[]>>({});
const isAiConfigLoaded = ref(false);
const aiConfigs = ref<AiConfigItem[]>([]);
const desktopSettings = ref<DesktopSettings>({ ...DEFAULT_DESKTOP_SETTINGS });
const mcpGlobalPolicy = ref<McpGlobalPolicy>({
...DEFAULT_MCP_GLOBAL_POLICY,
});
const isDesktopSettingsLoaded = ref(false);
const isMcpGlobalPolicyLoaded = ref(false);
const isEditorSettingsLoaded = ref(false);
let initEditorSettingsPromise: Promise<void> | null = null;
let pendingEditorSettingsPatches: Partial<EditorSettings>[] = [];
let editorSettingsOperationQueue: Promise<void> | null = null;
let editorSettingsPatchRevision = 0;
const editorSettingsFieldRevisions = new Map<keyof EditorSettings, number>();
const customColumnFormatterDeleteVersions = new Map<string, number>();
let pendingAiChatSelection: AiChatSelectionState | null = null;
let aiChatSelectionSaveRunning = false;
const editorSettings = ref<EditorSettings>(normalizeEditorSettings({}));
function enqueueEditorSettingsOperation<T>(operation: () => Promise<T>): Promise<T> {
const queuedOperation = editorSettingsOperationQueue ? editorSettingsOperationQueue.then(operation) : operation();
const trackedOperation = queuedOperation.then(
() => undefined,
() => undefined,
);
editorSettingsOperationQueue = trackedOperation;
void trackedOperation.finally(() => {
if (editorSettingsOperationQueue === trackedOperation) editorSettingsOperationQueue = null;
});
return queuedOperation;
}
function persistCurrentEditorSettings(): Promise<void> {
return api.saveEditorSettings(editorSettingsSnapshot(editorSettings.value));
}
function enqueueEditorSettingsSave(): Promise<void> {
return enqueueEditorSettingsOperation(persistCurrentEditorSettings);
}
function saveEditorSettings() {
void enqueueEditorSettingsSave().catch(() => {});
}
function markEditorSettingsPatch(partial: Partial<EditorSettings>): 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<EditorSettings>;
const normalized = normalizeEditorSettings(savedSettings);
const needsSidebarBrowseObjectsMigration = typeof savedSettings.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion !== "number" || savedSettings.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion < SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION;
if (needsSidebarBrowseObjectsMigration) {
// Before this migration, a false value still preserved double-click browsing.
normalized.sidebarBrowseObjectsOnDatabaseActivation = true;
normalized.sidebarBrowseObjectsOnDatabaseActivationMigrationVersion = SIDEBAR_BROWSE_OBJECTS_MIGRATION_VERSION;
}
editorSettings.value = normalized;
const needsExecuteModeDefaultMigration = typeof savedSettings.executeModeDefaultVersion !== "number" || savedSettings.executeModeDefaultVersion < EXECUTE_MODE_CURRENT_DEFAULT_VERSION;
const needsTabNavigationShortcutMigration = needsTabNavigationHistoryShortcutMigration(savedSettings.shortcuts);
const savedNullText = (savedSettings.dataGridExtractorOptions as Partial<DataGridExtractorOptions> | undefined)?.dsv?.nullText;
const needsDataGridExtractorOptionsMigration = (typeof savedSettings.dataGridExtractorOptionsMigrationVersion !== "number" || savedSettings.dataGridExtractorOptionsMigrationVersion < DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION) && savedNullText === "NULL";
const savedUpdateDownloadSource = (saved as { updateDownloadSource?: unknown }).updateDownloadSource;
if (savedUpdateDownloadSource === "atomgit" || needsExecuteModeDefaultMigration || needsTabNavigationShortcutMigration || needsSidebarBrowseObjectsMigration || needsDataGridExtractorOptionsMigration) {
// 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<DesktopSettings>) {
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<Omit<McpGlobalPolicy, "configured">>) {
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,
allowedGroupIds: next.allowedGroupIds,
allowedToolNames: next.allowedToolNames,
connectionPolicies: next.connectionPolicies,
groupPolicies: next.groupPolicies,
queryTimeoutSecs: next.queryTimeoutSecs,
});
} catch (error) {
mcpGlobalPolicy.value = previous;
throw error;
}
}
async function initAiConfigs(): Promise<void> {
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";
restoreLastConversation.value = savedSelection?.restoreLastConversation ?? false;
aiDefaultTemplatesByDbType.value = normalizeTemplateIdsByDbType(savedSelection?.defaultTemplatesByDbType);
aiLastUsedTemplatesByDbType.value = normalizeTemplateIdsByDbType(savedSelection?.lastUsedTemplatesByDbType);
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<void> {
isAiConfigLoaded.value = false;
await initAiConfigs();
}
async function migrateToMultiConfig(): Promise<void> {
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<string>();
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<void> {
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<AiConfigItem>): Promise<void> {
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<void> {
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<void> {
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<AiEffortSelection | null>(() => {
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 setRestoreLastConversation(value: boolean) {
if (value === restoreLastConversation.value) return;
restoreLastConversation.value = value;
persistAiChatSelection();
}
/** Empty id list clears the db_type entry so unsetting a default is expressible. */
function setDefaultTemplatesForDbType(dbType: string, templateIds: string[]) {
if (!dbType) return;
const next = { ...aiDefaultTemplatesByDbType.value };
const cleaned = [...new Set(templateIds.map((id) => id.trim()).filter((id) => id !== ""))];
if (cleaned.length === 0) delete next[dbType];
else next[dbType] = cleaned;
aiDefaultTemplatesByDbType.value = next;
persistAiChatSelection();
}
/**
* Called on send: a non-empty selection is remembered for the db_type, while
* sending with every template deselected is an explicit choice that clears
* the remembered selection — otherwise the stale entry would resurrect the
* old templates the next time a panel opens.
*/
function recordLastUsedTemplates(dbType: string, templateIds: string[]) {
if (!dbType) return;
if (templateIds.length === 0) {
if (!(dbType in aiLastUsedTemplatesByDbType.value)) return;
const next = { ...aiLastUsedTemplatesByDbType.value };
delete next[dbType];
aiLastUsedTemplatesByDbType.value = next;
persistAiChatSelection();
return;
}
aiLastUsedTemplatesByDbType.value = { ...aiLastUsedTemplatesByDbType.value, [dbType]: [...templateIds] };
persistAiChatSelection();
}
function removeTemplateFromDefaultAndLastUsed(templateId: string) {
const strip = (record: Record<string, string[]>): Record<string, string[]> => {
let changed = false;
const next: Record<string, string[]> = {};
for (const [dbType, ids] of Object.entries(record)) {
const filtered = ids.filter((id) => id !== templateId);
if (filtered.length !== ids.length) changed = true;
if (filtered.length > 0) next[dbType] = filtered;
}
// Identity-preserving no-op lets the caller skip a needless persist.
return changed ? next : record;
};
const nextDefaults = strip(aiDefaultTemplatesByDbType.value);
const nextLastUsed = strip(aiLastUsedTemplatesByDbType.value);
if (nextDefaults === aiDefaultTemplatesByDbType.value && nextLastUsed === aiLastUsedTemplatesByDbType.value) return;
aiDefaultTemplatesByDbType.value = nextDefaults;
aiLastUsedTemplatesByDbType.value = nextLastUsed;
persistAiChatSelection();
}
function persistAiChatSelection() {
pendingAiChatSelection = {
version: 1,
active: activeModel.value ? { ...activeModel.value } : undefined,
effortPreferences: effortPreferences.value.map((preference) => ({
...preference,
selection: { ...preference.selection },
})),
defaultMode: defaultAiMode.value,
restoreLastConversation: restoreLastConversation.value,
// Match the backend's skip_serializing_if(empty): omit the per-db_type
// records entirely while nothing is configured so the payload stays
// identical to the pre-defaults format for users without template picks.
...(Object.keys(aiDefaultTemplatesByDbType.value).length > 0 ? { defaultTemplatesByDbType: aiDefaultTemplatesByDbType.value } : {}),
...(Object.keys(aiLastUsedTemplatesByDbType.value).length > 0 ? { lastUsedTemplatesByDbType: aiLastUsedTemplatesByDbType.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 = getAiProviderPreset(config.provider, config.endpoint);
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<EditorSettings>) {
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.backgroundImage !== undefined) editorSettings.value.backgroundImage = normalizeBackgroundImageSettings(partial.backgroundImage);
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.blockDangerousRedisCommands !== undefined) editorSettings.value.blockDangerousRedisCommands = partial.blockDangerousRedisCommands === 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.showLineNumbers !== undefined) editorSettings.value.showLineNumbers = partial.showLineNumbers === 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.selectFirstCompletionOnOpen !== undefined) editorSettings.value.selectFirstCompletionOnOpen = partial.selectFirstCompletionOnOpen === true;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
if (partial.tableDdlWordWrap !== undefined) editorSettings.value.tableDdlWordWrap = partial.tableDdlWordWrap === true;
if (partial.refreshDdlOnOpen !== undefined) editorSettings.value.refreshDdlOnOpen = partial.refreshDdlOnOpen === 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.appCloseUnsavedTabsMode !== undefined) editorSettings.value.appCloseUnsavedTabsMode = normalizeAppCloseUnsavedTabsMode(partial.appCloseUnsavedTabsMode);
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.tabPlacement !== undefined) editorSettings.value.tabPlacement = normalizeTabPlacement(partial.tabPlacement);
if (partial.tabGroupMode !== undefined) editorSettings.value.tabGroupMode = normalizeTabGroupMode(partial.tabGroupMode);
if (partial.tabGroupCustomizations !== undefined) editorSettings.value.tabGroupCustomizations = normalizeTabGroupCustomizations(partial.tabGroupCustomizations);
if (partial.tabSortMode !== undefined) editorSettings.value.tabSortMode = normalizeTabSortMode(partial.tabSortMode);
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.externalSqlEditorMaxMb !== undefined) editorSettings.value.externalSqlEditorMaxMb = normalizeExternalSqlEditorMaxMb(partial.externalSqlEditorMaxMb);
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.dataGridKeepFilterEditorExpanded !== undefined) editorSettings.value.dataGridKeepFilterEditorExpanded = partial.dataGridKeepFilterEditorExpanded === true;
if (partial.dataGridTextFilterPanelHeight !== undefined) editorSettings.value.dataGridTextFilterPanelHeight = normalizeDataGridTextFilterPanelHeight(partial.dataGridTextFilterPanelHeight);
if (partial.localFilterPopoverWidth !== undefined) editorSettings.value.localFilterPopoverWidth = normalizeDrawerWidth(partial.localFilterPopoverWidth, 240, DEFAULT_EDITOR_SETTINGS.localFilterPopoverWidth);
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.dataGridExtractorOptionsMigrationVersion !== undefined) editorSettings.value.dataGridExtractorOptionsMigrationVersion = DATA_GRID_EXTRACTOR_OPTIONS_MIGRATION_VERSION;
if (partial.resultRunDisplayMode !== undefined) editorSettings.value.resultRunDisplayMode = normalizeResultRunDisplayMode(partial.resultRunDisplayMode);
if (partial.defaultAutoKeepResults !== undefined) editorSettings.value.defaultAutoKeepResults = partial.defaultAutoKeepResults === true;
if (partial.multiStatementDefaultView !== undefined) editorSettings.value.multiStatementDefaultView = normalizeMultiStatementDefaultView(partial.multiStatementDefaultView);
if (partial.dataGridAutoTransposeSingleRow !== undefined) editorSettings.value.dataGridAutoTransposeSingleRow = partial.dataGridAutoTransposeSingleRow === true;
if (partial.dataGridCellDetailButtonVisible !== undefined) editorSettings.value.dataGridCellDetailButtonVisible = typeof partial.dataGridCellDetailButtonVisible === "boolean" ? partial.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
if (partial.dataGridCrosshairHighlight !== undefined) editorSettings.value.dataGridCrosshairHighlight = typeof partial.dataGridCrosshairHighlight === "boolean" ? partial.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
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.tableInfoDrawerPinned !== undefined) editorSettings.value.tableInfoDrawerPinned = partial.tableInfoDrawerPinned === true;
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.sidebarBrowseObjectsOnDatabaseActivation !== undefined) editorSettings.value.sidebarBrowseObjectsOnDatabaseActivation = partial.sidebarBrowseObjectsOnDatabaseActivation === 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.generateSqlIncludeDatabaseName !== undefined) editorSettings.value.generateSqlIncludeDatabaseName = partial.generateSqlIncludeDatabaseName === true;
if (partial.generateSqlQuoteIdentifiers !== undefined) editorSettings.value.generateSqlQuoteIdentifiers = partial.generateSqlQuoteIdentifiers === true;
if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true;
if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled;
if (partial.autoDownloadUpdates !== undefined) editorSettings.value.autoDownloadUpdates = partial.autoDownloadUpdates === true;
if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);
if (partial.sidebarCopyTableNameSeparator !== undefined) editorSettings.value.sidebarCopyTableNameSeparator = normalizeSidebarCopyTableNameSeparator(partial.sidebarCopyTableNameSeparator);
if (partial.sidebarCopyTableNameIncludeSchema !== undefined) editorSettings.value.sidebarCopyTableNameIncludeSchema = partial.sidebarCopyTableNameIncludeSchema === true;
if (partial.sidebarObjectInfoMode !== undefined) editorSettings.value.sidebarObjectInfoMode = normalizeSidebarObjectInfoMode(partial.sidebarObjectInfoMode);
if (partial.sidebarShowConnectionNotes !== undefined) editorSettings.value.sidebarShowConnectionNotes = partial.sidebarShowConnectionNotes === true;
if (partial.sidebarShowTooltips !== undefined) editorSettings.value.sidebarShowTooltips = partial.sidebarShowTooltips;
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.sqlShortcuts !== undefined) editorSettings.value.sqlShortcuts = normalizeSqlShortcuts(partial.sqlShortcuts);
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.csvQuoteMode !== undefined) editorSettings.value.csvQuoteMode = normalizeCsvQuoteMode(partial.csvQuoteMode);
if (partial.redisKeyTemplates !== undefined) editorSettings.value.redisKeyTemplates = normalizeRedisKeyTemplates(partial.redisKeyTemplates);
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.showTableDdlHoverPreview !== undefined) editorSettings.value.showTableDdlHoverPreview = partial.showTableDdlHoverPreview === true;
if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);
if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode);
if (partial.defaultTransactionMode !== undefined) editorSettings.value.defaultTransactionMode = normalizeDefaultTransactionMode(partial.defaultTransactionMode);
if (partial.flatteningMultiLineText !== undefined) editorSettings.value.flatteningMultiLineText = partial.flatteningMultiLineText;
if (partial.dataGridShowWhitespace !== undefined) editorSettings.value.dataGridShowWhitespace = partial.dataGridShowWhitespace;
}
function updateEditorSettings(partial: Partial<EditorSettings>) {
const patch = migrateLegacyFilterEditorExpansionPatch(partial);
applyEditorSettingsPatch(patch);
markEditorSettingsPatch(patch);
if (!isEditorSettingsLoaded.value) {
pendingEditorSettingsPatches.push(editorSettingsPatchSnapshot(patch));
return;
}
saveEditorSettings();
}
async function persistEditorSettings(): Promise<void> {
await initEditorSettings();
await enqueueEditorSettingsSave();
}
async function enqueueEditorSettingsAtomicMutation<T>(mutation: () => Promise<T>): Promise<T> {
await initEditorSettings();
return enqueueEditorSettingsOperation(mutation);
}
function updateEditorSettingsAndPersist(partial: Partial<EditorSettings>): Promise<void> {
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
const previous = editorSettingsSnapshot(editorSettings.value);
const patch = migrateLegacyFilterEditorExpansionPatch(partial);
applyEditorSettingsPatch(patch);
const revision = markEditorSettingsPatch(patch);
try {
await persistCurrentEditorSettings();
} catch (error) {
const restored = editorSettingsSnapshot(editorSettings.value) as unknown as Record<string, unknown>;
const previousSettings = previous as unknown as Record<string, unknown>;
let changed = false;
for (const key of Object.keys(patch) as (keyof EditorSettings)[]) {
if (patch[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;
}
});
}
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 customColumnFormatterDeleteVersion(id: string): number {
return customColumnFormatterDeleteVersions.get(id) ?? 0;
}
async function upsertCustomColumnFormatter(formatter: CustomColumnFormatterConfig, expectedDeleteVersion?: number): Promise<CustomColumnFormatterConfig | undefined> {
const normalized = normalizeCustomColumnFormatter(formatter);
if (!normalized) return undefined;
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
if (expectedDeleteVersion !== undefined && customColumnFormatterDeleteVersion(normalized.id) !== expectedDeleteVersion) return undefined;
const previous = editorSettings.value.customColumnFormatters[normalized.id];
const partial = {
customColumnFormatters: {
...editorSettings.value.customColumnFormatters,
[normalized.id]: normalized,
},
} satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(partial);
markEditorSettingsPatch(partial);
try {
await persistCurrentEditorSettings();
} catch (error) {
const customColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
const current = customColumnFormatters[normalized.id];
if (current?.name === normalized.name && current.template === normalized.template) {
if (previous) {
customColumnFormatters[normalized.id] = previous;
} else {
delete customColumnFormatters[normalized.id];
}
const rollback = { customColumnFormatters } satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(rollback);
markEditorSettingsPatch(rollback);
}
throw error;
}
return normalized;
});
}
async function deleteCustomColumnFormatter(id: string): Promise<void> {
return enqueueEditorSettingsAtomicMutation(async () => {
await initEditorSettings();
const previousDeleteVersion = customColumnFormatterDeleteVersion(id);
const deleteVersion = previousDeleteVersion + 1;
customColumnFormatterDeleteVersions.set(id, deleteVersion);
const previousFormatter = editorSettings.value.customColumnFormatters[id];
const customColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
delete customColumnFormatters[id];
const removedColumnFormatters = Object.fromEntries(
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
return formatter.kind === "custom-ref" && formatter.formatterId === id;
}),
);
const columnFormatters = Object.fromEntries(
Object.entries(editorSettings.value.columnFormatters).filter(([, formatter]) => {
return formatter.kind !== "custom-ref" || formatter.formatterId !== id;
}),
);
const partial = { customColumnFormatters, columnFormatters } satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(partial);
markEditorSettingsPatch(partial);
try {
await persistCurrentEditorSettings();
} catch (error) {
if (customColumnFormatterDeleteVersion(id) === deleteVersion) {
if (previousDeleteVersion === 0) {
customColumnFormatterDeleteVersions.delete(id);
} else {
customColumnFormatterDeleteVersions.set(id, previousDeleteVersion);
}
}
const restoredCustomColumnFormatters = {
...editorSettings.value.customColumnFormatters,
};
if (previousFormatter && !restoredCustomColumnFormatters[id]) restoredCustomColumnFormatters[id] = previousFormatter;
const restoredColumnFormatters = {
...editorSettings.value.columnFormatters,
};
for (const [key, formatter] of Object.entries(removedColumnFormatters)) {
if (!restoredColumnFormatters[key]) restoredColumnFormatters[key] = formatter;
}
const rollback = {
customColumnFormatters: restoredCustomColumnFormatters,
columnFormatters: restoredColumnFormatters,
} satisfies Partial<EditorSettings>;
applyEditorSettingsPatch(rollback);
markEditorSettingsPatch(rollback);
throw error;
}
});
}
return {
settingsPageActive,
settingsNavigationRequest,
requestSettingsNavigation,
clearSettingsNavigationRequest,
activeModel,
activeEffort,
defaultAiMode,
setDefaultAiMode,
restoreLastConversation,
setRestoreLastConversation,
aiDefaultTemplatesByDbType,
aiLastUsedTemplatesByDbType,
setDefaultTemplatesForDbType,
recordLastUsedTemplates,
removeTemplateFromDefaultAndLastUsed,
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,
customColumnFormatterDeleteVersion,
upsertCustomColumnFormatter,
deleteCustomColumnFormatter,
};
});