diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index bb8ef0ea37..bc57c04e61 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -5026,6 +5026,10 @@ onUnmounted(() => { closeSettingsPage(); openPluginCenterPage(); " + @open-cc-switch-plugin-center=" + closeSettingsPage(); + openPluginCenterPage({ pluginId: 'cc-switch' }); + " @open-mcp-settings="openSettings('mcp')" @ai-config-deep-link-handled="settingsAiConfigDraft = null" @logout="handleWebLogout" diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 802ef44ca1..0b1fae0f2f 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -64,6 +64,8 @@ import { useSettingsStore, AI_PROVIDER_PRESETS, AI_PROVIDER_PARTNER_PRESETS, + CC_SWITCH_PROVIDER_ID, + CC_SWITCH_PROVIDER_PRESET, AI_OUTPUT_TOKENS_MAX, AI_OUTPUT_TOKENS_MIN, EDITOR_THEMES, @@ -148,6 +150,7 @@ import { importClipboardApiKeyAfterConfirmation, type AiConfigDeepLinkDraft } fr import { clearDebugLogs as clearStoredDebugLogs, downloadDebugLogs, getDebugLogBundleText } from "@/lib/backend/debugLog"; import { aiTestConnection, + loadCcSwitchAiConfigs, checkMcpServerStatus, installMcpServer, installNativeMcpServer, @@ -196,6 +199,7 @@ import { webdavSyncTest, webdavSyncUpload, listInstalledAgentsLocal, + listPlugins, type AppSupportInfo, type McpHttpServerSettings, type McpHttpServerStatus, @@ -235,7 +239,7 @@ import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame"; import { currentStatementFrameLayer } from "@/lib/editor/codemirrorCurrentStatementFrameLayer"; import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers"; import { normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig"; -import { validateConfigName, generateId, type AiConfigItem, type ConfigNameValidationResult } from "@/lib/ai/aiConfigList"; +import { prepareImportedAiConfigs, validateConfigName, generateId, type AiConfigItem, type ConfigNameValidationResult } from "@/lib/ai/aiConfigList"; import { currentExecutableStatementRange, type SqlTextRange } from "@/lib/sql/sqlStatementRanges"; import { executableStatementRangeCacheForDoc, executableStatementRangeStartingAt, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache"; import { EMPTY_TABLE_COLUMN_TEMPLATE_DATA_TYPE, parseTableColumnTemplateFields, TABLE_COLUMN_TEMPLATE_DATABASE_TYPES, tableColumnTemplateRowsToSettings } from "@/lib/table/tableColumnTemplates"; @@ -281,7 +285,8 @@ import { import { isMacOS, isWindows } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; import { useToast } from "@/composables/useToast"; -import type { DatabaseType, SqlShortcutAction, SqlSnippet } from "@/types/database"; +import type { InstalledPlugin, DatabaseType, SqlShortcutAction, SqlSnippet } from "@/types/database"; +import { getCcSwitchPluginStatus, isCcSwitchPluginAvailable } from "@/lib/ai/ccSwitch"; import { uuid } from "@/lib/common/utils"; import { DEFAULT_SQL_SHORTCUT_SELECT_LIMIT } from "@/lib/sql/sqlDialectSelectLimit"; import { @@ -536,6 +541,7 @@ const emit = defineEmits<{ "update-all": []; "open-driver-store": [target?: DriverStoreTab]; "open-plugin-center": []; + "open-cc-switch-plugin-center": []; "open-mcp-settings": []; "open-update-center": []; "ai-config-deep-link-handled": []; @@ -5357,6 +5363,7 @@ watch(activeSettingsTab, async (tab) => { if (tab !== "shortcuts") applyShortcutRecording(false); void resetSettingsContentScroll(); if (tab === "mcp" && !mcpStatus.value && !mcpStatusLoading.value) void refreshMcpStatus(); + if (tab === "ai" && !isWeb) void refreshCcSwitchPluginAvailability(); if (tab === "ai" && aiIsCliProvider.value) void ensureCliMcpStatus(); if (tab === "ai") { void loadMaxAgentTurnsSetting(); @@ -5405,6 +5412,7 @@ onMounted(() => { window.addEventListener("focus", onShortcutRecordingWindowFocus); window.addEventListener("blur", onShortcutRecordingWindowBlur); void refreshWebDavPasswordStatus(); + if (!isWeb) void refreshCcSwitchPluginAvailability(); checkLayoutDescTruncation(); initTruncationObservers(); void checkBackgroundImageFileExists(); @@ -5739,6 +5747,15 @@ async function saveWebSqlFileUploadMaxMbSetting() { // AI Config Delete Confirmation const aiDeleteConfirmOpen = ref(false); const aiDeleteConfigId = ref(null); +const aiCcSwitchImporting = ref(false); +const aiCcSwitchPlugins = ref([]); +const aiCcSwitchPluginLoading = ref(false); +const aiCcSwitchPluginStatus = computed(() => getCcSwitchPluginStatus(aiCcSwitchPlugins.value)); +const aiCcSwitchPluginAvailable = computed(() => isCcSwitchPluginAvailable(aiCcSwitchPlugins.value)); +const aiCcSwitchPluginStatusText = computed(() => { + if (aiCcSwitchPluginLoading.value) return "ai.ccSwitchPluginChecking"; + return aiCcSwitchPluginStatus.value === "incompatible" ? "ai.ccSwitchPluginIncompatible" : "ai.ccSwitchPluginNotInstalledStatus"; +}); const CLI_AI_PROVIDERS = new Set(["claude-code-cli", "codex-cli", "opencode-cli", "pi-agent-cli", "cursor-cli", "grok-cli", "codebuddy-cli", "qoder-cli"]); const OPENCODE_CONTROL_ENV = new Set(["OPENCODE_CONFIG", "OPENCODE_CONFIG_CONTENT", "OPENCODE_CONFIG_DIR", "OPENCODE_DB", "OPENCODE_PERMISSION", "OPENCODE_DISABLE_PROJECT_CONFIG"]); @@ -5746,6 +5763,18 @@ const CURSOR_CONTROL_ENV = new Set(["CURSOR_CONFIG_DIR", "CURSOR_DATA_DIR"]); const builtinAiProviderOptions = computed(() => Object.values(AI_PROVIDER_PRESETS).filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider))); const partnerAiProviderOptions = computed(() => AI_PROVIDER_PARTNER_PRESETS.filter((provider) => !isWeb || !CLI_AI_PROVIDERS.has(provider.provider))); +async function refreshCcSwitchPluginAvailability() { + if (isWeb || aiCcSwitchPluginLoading.value) return; + aiCcSwitchPluginLoading.value = true; + try { + aiCcSwitchPlugins.value = await listPlugins(); + } catch { + aiCcSwitchPlugins.value = []; + } finally { + aiCcSwitchPluginLoading.value = false; + } +} + const aiEditProvider = ref("claude"); const aiEditProviderPresetId = ref("claude"); const aiEditApiKey = ref(""); @@ -5782,6 +5811,7 @@ const aiEditQoderCliEnvRows = ref([]); const aiAnthropicMessagesMode = computed(() => aiEditApiStyle.value === "anthropic-messages"); const selectedAiProviderPreset = computed(() => getAiProviderPresetOption(aiEditProviderPresetId.value)); const selectedAiPartnerPreset = computed(() => (isAiPartnerProviderPreset(selectedAiProviderPreset.value) ? selectedAiProviderPreset.value : null)); +const aiIsCcSwitchProvider = computed(() => aiEditProviderPresetId.value === CC_SWITCH_PROVIDER_ID); const aiTesting = ref(false); const aiTestResult = ref<"" | "success" | "error">(""); @@ -6081,12 +6111,12 @@ function syncAiEditState() { function aiSelectProvider(presetId: string) { const preset = getAiProviderPresetOption(presetId); const provider = preset.provider; - if (isWeb && CLI_AI_PROVIDERS.has(provider)) return; + if (isWeb && (CLI_AI_PROVIDERS.has(provider) || presetId === CC_SWITCH_PROVIDER_ID)) return; if (presetId === aiEditProviderPresetId.value) return; syncAiEditState(); - // Apply new provider's preset defaults to edit state + // CC-SWITCH is a UI-only preset; installation is initiated by its action buttons. aiEditProviderPresetId.value = presetId; aiEditProvider.value = provider; aiEditApiKey.value = ""; @@ -6298,6 +6328,51 @@ async function aiSetDefaultConfig(id: string) { } } +function ccSwitchImportErrorMessage(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + if (message === "ccSwitchNotInstalled") return t("ai.ccSwitchNotInstalled"); + if (message === "ccSwitchPluginNotInstalled") return t("ai.ccSwitchPluginNotInstalled"); + if (message === "ccSwitchInvalidDatabase") return t("ai.ccSwitchInvalidDatabase"); + if (message === "ccSwitchImportDesktopOnly") return t("ai.ccSwitchImportDesktopOnly"); + if (message.startsWith("ccSwitchHomeDirectoryFailed:")) return t("ai.ccSwitchHomeDirectoryFailed"); + if (message.startsWith("ccSwitchOpenFailed:")) return t("ai.ccSwitchOpenFailed"); + if (message.startsWith("ccSwitchReadFailed:")) return t("ai.ccSwitchReadFailed"); + return message; +} + +async function aiImportCcSwitchConfigs() { + if (isWeb || aiCcSwitchImporting.value) return; + aiCcSwitchImporting.value = true; + try { + const result = await loadCcSwitchAiConfigs(); + const prepared = prepareImportedAiConfigs(settingsStore.aiConfigs, result.configs); + if (prepared.length === 0) { + toast(t("ai.ccSwitchNoNewConfigs"), 4000); + return; + } + + const hadConfigs = settingsStore.aiConfigs.length > 0; + for (const [index, config] of prepared.entries()) { + await settingsStore.createAiConfig({ + ...config, + id: generateId(), + isDefault: !hadConfigs && index === 0, + }); + } + toast( + t(result.skipped.length > 0 ? "ai.ccSwitchImportPartial" : "ai.ccSwitchImportSuccess", { + count: prepared.length, + skipped: result.skipped.length, + }), + 4000, + ); + } catch (error) { + toast(ccSwitchImportErrorMessage(error), 5000); + } finally { + aiCcSwitchImporting.value = false; + } +} + async function aiTestConn() { if ((aiRequiresApiKey.value && !aiEditApiKey.value.trim()) || (!aiIsCliProvider.value && !aiEditEndpoint.value.trim())) return; if (aiHeadersValidationError.value) { @@ -10422,12 +10497,14 @@ LIMIT 100;
-
+

{{ t("ai.configList") }}

- +
+ +
@@ -10545,6 +10622,20 @@ LIMIT 100;
+
+
+ +
+

{{ t("ai.ccSwitchProvider") }}

+

{{ t(aiCcSwitchPluginStatusText) }}

+
+
+ +
+
@@ -10850,7 +10941,7 @@ LIMIT 100;
-
+
@@ -10863,8 +10954,8 @@ LIMIT 100; - - {{ selectedAiProviderPreset.label }} + + {{ aiIsCcSwitchProvider ? t("ai.ccSwitchProvider") : selectedAiProviderPreset.label }} @@ -10878,6 +10969,12 @@ LIMIT 100; {{ provider.label }} + + + + {{ t("ai.ccSwitchProvider") }} + + {{ t("ai.partnerProviders") }} @@ -10903,8 +11000,44 @@ LIMIT 100;
+
+
+ +
+

{{ t("ai.ccSwitchProvider") }}

+

{{ t("ai.ccSwitchProviderDescription") }}

+
+
+
+
+
+ + + + {{ aiCcSwitchPluginLoading ? t("ai.ccSwitchPluginChecking") : t("ai.ccSwitchPluginReady") }} + +
+ +
+
+ +
+
+
+ -
+
@@ -10942,7 +11075,7 @@ LIMIT 100;
-
+
@@ -10979,7 +11112,7 @@ LIMIT 100;
-
+
@@ -11001,7 +11134,7 @@ LIMIT 100;
-
+
@@ -11020,7 +11153,7 @@ LIMIT 100;
-
+
@@ -11046,7 +11179,7 @@ LIMIT 100;
-
+
-
+
-
+
@@ -11100,7 +11233,7 @@ LIMIT 100;
-
+
@@ -11111,7 +11244,7 @@ LIMIT 100;
-
+
-
+
-
+