+
@@ -4958,7 +5094,7 @@ onUnmounted(cleanupPreviewEditor);
-
{{ t("common.cancel") }}
- {{ t("settings.apply") }}
+ {{ t("settings.apply") }}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index c6e2038ce..9367196c6 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1466,6 +1466,18 @@ export default {
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "Invalid environment variable name: {name}",
codexCliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
+ cliMcpRequiredTitle: "DBX MCP Server required",
+ cliMcpRequiredDescription: "{provider} uses the DBX MCP Server to access database schema and query tools. Install it before using this provider in the AI assistant.",
+ cliPath: "{provider} Path",
+ cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
+ cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
+ cliEnv: "Environment variables",
+ cliEnvAdd: "Add variable",
+ cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "Invalid environment variable name: {name}",
+ cliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
codexMcpRequiredTitle: "DBX MCP Server required",
codexMcpRequiredDescription: "Codex CLI uses the DBX MCP Server to access database schema and query tools. Install it before using Codex in the AI assistant.",
reasoningLevel: "Reasoning level",
@@ -1474,7 +1486,11 @@ export default {
reasoningLevelLow: "Low",
reasoningLevelMedium: "Medium",
reasoningLevelHigh: "High",
+ reasoningLevelXhigh: "Extra high",
+ reasoningLevelMax: "Maximum",
reasoningLevelHint: "Controls Codex CLI model_reasoning_effort. Default uses your Codex config.",
+ claudeCodeEffortHint: "Available levels are reported by the selected model through your local Claude Code CLI.",
+ claudeCodeEffortUnavailable: "The selected model does not report configurable effort levels. Claude Code will use its default.",
actions: {
general: "General",
generate: "Generate SQL",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 502f1c472..be7578e31 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1325,13 +1325,29 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "Nombre de variable de entorno no válido: {name}",
codexCliEnvReservedName: "{name} lo administra DBX para MCP Server y no se puede configurar aquí.",
+ cliMcpRequiredTitle: "DBX MCP Server required",
+ cliMcpRequiredDescription: "{provider} uses the DBX MCP Server to access database schema and query tools. Install it before using this provider in the AI assistant.",
+ cliPath: "{provider} Path",
+ cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
+ cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
+ cliEnv: "Environment variables",
+ cliEnvAdd: "Add variable",
+ cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "Invalid environment variable name: {name}",
+ cliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
reasoningLevel: "Nivel de razonamiento",
reasoningLevelDefault: "Predeterminado",
reasoningLevelMinimal: "Mínimo",
reasoningLevelLow: "Bajo",
reasoningLevelMedium: "Medio",
reasoningLevelHigh: "Alto",
+ reasoningLevelXhigh: "Muy alto",
+ reasoningLevelMax: "Máximo",
reasoningLevelHint: "Controla model_reasoning_effort de Codex CLI. Predeterminado usa tu configuración de Codex.",
+ claudeCodeEffortHint: "Los niveles disponibles los proporciona dinámicamente tu Claude Code CLI local para el modelo seleccionado.",
+ claudeCodeEffortUnavailable: "El modelo seleccionado no informa niveles de esfuerzo configurables. Claude Code usará su valor predeterminado.",
run: "Ejecutar",
readingSchema: "Leyendo esquema",
noConnection: "No hay una conexión disponible para esta pestaña",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 847fa5a4e..98fe931d3 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -1406,6 +1406,18 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "Nome variabile d'ambiente non valido: {name}",
codexCliEnvReservedName: "{name} è gestita da DBX per MCP Server e non può essere impostata qui.",
+ cliMcpRequiredTitle: "DBX MCP Server required",
+ cliMcpRequiredDescription: "{provider} uses the DBX MCP Server to access database schema and query tools. Install it before using this provider in the AI assistant.",
+ cliPath: "{provider} Path",
+ cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
+ cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
+ cliEnv: "Environment variables",
+ cliEnvAdd: "Add variable",
+ cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "Invalid environment variable name: {name}",
+ cliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
codexMcpRequiredTitle: "DBX MCP Server richiesto",
codexMcpRequiredDescription: "Codex CLI usa il DBX MCP Server per accedere allo schema del database e agli strumenti di query. Installalo prima di usare Codex nell'assistente AI.",
reasoningLevel: "Livello di ragionamento",
@@ -1414,7 +1426,11 @@ export default withEnglishFallback({
reasoningLevelLow: "Basso",
reasoningLevelMedium: "Medio",
reasoningLevelHigh: "Alto",
+ reasoningLevelXhigh: "Molto alto",
+ reasoningLevelMax: "Massimo",
reasoningLevelHint: "Controlla model_reasoning_effort di Codex CLI. Predefinito usa la configurazione Codex.",
+ claudeCodeEffortHint: "I livelli disponibili vengono forniti dinamicamente dal Claude Code CLI locale per il modello selezionato.",
+ claudeCodeEffortUnavailable: "Il modello selezionato non segnala livelli di effort configurabili. Claude Code userà il valore predefinito.",
actions: {
general: "Generale",
generate: "Genera SQL",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index 5b5499c3b..7851868f3 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -1324,6 +1324,18 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "無効な環境変数名: {name}",
codexCliEnvReservedName: "{name}はMCP Server用にDBXが管理しているため、ここでは設定できません。",
+ cliMcpRequiredTitle: "DBX MCP Server required",
+ cliMcpRequiredDescription: "{provider} uses the DBX MCP Server to access database schema and query tools. Install it before using this provider in the AI assistant.",
+ cliPath: "{provider} Path",
+ cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
+ cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
+ cliEnv: "Environment variables",
+ cliEnvAdd: "Add variable",
+ cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "Invalid environment variable name: {name}",
+ cliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
contextWindow: "コンテキストウィンドウ",
contextWindowAuto: "自動(モデル名から検出)",
contextWindowHint: "トークン数。空欄で自動検出。ローカル/カスタムモデルは手動設定してください。",
@@ -1335,7 +1347,11 @@ export default withEnglishFallback({
reasoningLevelLow: "低",
reasoningLevelMedium: "中",
reasoningLevelHigh: "高",
+ reasoningLevelXhigh: "非常に高い",
+ reasoningLevelMax: "最大",
reasoningLevelHint: "Codex CLIのmodel_reasoning_effortを制御します。デフォルトではCodex設定を使用します。",
+ claudeCodeEffortHint: "利用可能なレベルは、選択したモデルについてローカルのClaude Code CLIから動的に取得されます。",
+ claudeCodeEffortUnavailable: "選択したモデルは設定可能なeffortレベルを返していません。Claude Codeのデフォルトを使用します。",
run: "実行",
readingSchema: "スキーマを読み取り中",
noConnection: "このタブに利用可能な接続がありません",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 2f53450ab..145dd8aae 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -1327,13 +1327,29 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "Nome de variável de ambiente inválido: {name}",
codexCliEnvReservedName: "{name} é gerenciada pelo DBX para o MCP Server e não pode ser definida aqui.",
+ cliMcpRequiredTitle: "DBX MCP Server required",
+ cliMcpRequiredDescription: "{provider} uses the DBX MCP Server to access database schema and query tools. Install it before using this provider in the AI assistant.",
+ cliPath: "{provider} Path",
+ cliPathHint: "Leave empty to use {command} from PATH. Sign in separately with {loginCommand}.",
+ cliPathEnvError: "{provider} path should contain only the executable path. Add environment variables below.",
+ cliEnv: "Environment variables",
+ cliEnvAdd: "Add variable",
+ cliEnvHint: "Optional variables passed to {provider}, such as HTTPS_PROXY, NO_PROXY, or SSL_CERT_FILE.",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "Invalid environment variable name: {name}",
+ cliEnvReservedName: "{name} is managed by DBX for the MCP server and cannot be set here.",
reasoningLevel: "Nível de raciocínio",
reasoningLevelDefault: "Padrão",
reasoningLevelMinimal: "Mínimo",
reasoningLevelLow: "Baixo",
reasoningLevelMedium: "Médio",
reasoningLevelHigh: "Alto",
+ reasoningLevelXhigh: "Muito alto",
+ reasoningLevelMax: "Máximo",
reasoningLevelHint: "Controla model_reasoning_effort do Codex CLI. Padrão usa sua configuração do Codex.",
+ claudeCodeEffortHint: "Os níveis disponíveis são informados dinamicamente pelo Claude Code CLI local para o modelo selecionado.",
+ claudeCodeEffortUnavailable: "O modelo selecionado não informa níveis de effort configuráveis. O Claude Code usará o padrão.",
run: "Executar",
readingSchema: "Lendo schema",
noConnection: "Nenhuma conexão disponível para esta aba",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index fa573b15b..2af3a3490 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1466,6 +1466,18 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "环境变量名称无效:{name}",
codexCliEnvReservedName: "{name} 由 DBX 用于 MCP Server,不能在这里设置。",
+ cliMcpRequiredTitle: "需要 DBX MCP Server",
+ cliMcpRequiredDescription: "{provider} 会通过 DBX MCP Server 访问数据库结构和查询工具。请先安装后再在 AI 助手中使用该提供商。",
+ cliPath: "{provider} 路径",
+ cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
+ cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
+ cliEnv: "环境变量",
+ cliEnvAdd: "添加变量",
+ cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "环境变量名称无效:{name}",
+ cliEnvReservedName: "{name} 由 DBX 用于 MCP Server,不能在这里设置。",
codexMcpRequiredTitle: "需要 DBX MCP Server",
codexMcpRequiredDescription: "Codex CLI 通过 DBX MCP Server 访问数据库结构和查询工具。请先安装后再在 AI 助手中使用 Codex。",
reasoningLevel: "推理级别",
@@ -1474,7 +1486,11 @@ export default withEnglishFallback({
reasoningLevelLow: "低",
reasoningLevelMedium: "中",
reasoningLevelHigh: "高",
+ reasoningLevelXhigh: "极高",
+ reasoningLevelMax: "最高",
reasoningLevelHint: "控制 Codex CLI 的 model_reasoning_effort。默认使用你的 Codex 配置。",
+ claudeCodeEffortHint: "可用级别由本地 Claude Code CLI 根据所选模型动态提供。",
+ claudeCodeEffortUnavailable: "所选模型未提供可配置的推理级别,将使用 Claude Code 的默认设置。",
actions: {
general: "通用问答",
generate: "生成 SQL",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 934ab84cc..0a5288039 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -1410,13 +1410,29 @@ export default withEnglishFallback({
codexCliEnvValuePlaceholder: "http://proxy:9800",
codexCliEnvInvalidName: "環境變數名稱無效:{name}",
codexCliEnvReservedName: "{name} 由 DBX 用於 MCP Server,不能在這裡設定。",
+ cliMcpRequiredTitle: "需要 DBX MCP Server",
+ cliMcpRequiredDescription: "{provider} 会通过 DBX MCP Server 访问数据库结构和查询工具。请先安装后再在 AI 助手中使用该提供商。",
+ cliPath: "{provider} 路径",
+ cliPathHint: "留空则使用 PATH 中的 {command}。请先在终端执行 {loginCommand} 登录。",
+ cliPathEnvError: "{provider} 路径只能填写可执行文件路径。请在下方添加环境变量。",
+ cliEnv: "环境变量",
+ cliEnvAdd: "添加变量",
+ cliEnvHint: "传递给 {provider} 的可选变量,例如 HTTPS_PROXY、NO_PROXY 或 SSL_CERT_FILE。",
+ cliEnvKeyPlaceholder: "HTTPS_PROXY",
+ cliEnvValuePlaceholder: "http://proxy:9800",
+ cliEnvInvalidName: "环境变量名称无效:{name}",
+ cliEnvReservedName: "{name} 由 DBX 用于 MCP Server,不能在这里设置。",
reasoningLevel: "推理層級",
reasoningLevelDefault: "預設",
reasoningLevelMinimal: "最小",
reasoningLevelLow: "低",
reasoningLevelMedium: "中",
reasoningLevelHigh: "高",
+ reasoningLevelXhigh: "極高",
+ reasoningLevelMax: "最高",
reasoningLevelHint: "控制 Codex CLI 的 model_reasoning_effort。預設會使用你的 Codex 設定。",
+ claudeCodeEffortHint: "可用層級由本機 Claude Code CLI 根據所選模型動態提供。",
+ claudeCodeEffortUnavailable: "所選模型未提供可設定的推理層級,將使用 Claude Code 的預設設定。",
actions: {
general: "通用問答",
generate: "產生 SQL",
diff --git a/apps/desktop/src/lib/__tests__/ai/aiModelEffort.spec.ts b/apps/desktop/src/lib/__tests__/ai/aiModelEffort.spec.ts
new file mode 100644
index 000000000..da2dc9abc
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/ai/aiModelEffort.spec.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from "vitest";
+import { normalizeAiModelEffortLevels, normalizeClaudeCodeReasoningLevel } from "@/lib/ai/aiModelEffort";
+import type { AiModelInfo } from "@/lib/backend/tauri";
+
+describe("normalizeAiModelEffortLevels", () => {
+ it("preserves the CLI order while removing duplicates and unknown levels", () => {
+ expect(normalizeAiModelEffortLevels(["low", "high", "xhigh", "high", "future", null, "max"])).toEqual(["low", "high", "xhigh", "max"]);
+ });
+
+ it("returns no configurable levels when metadata is unavailable", () => {
+ expect(normalizeAiModelEffortLevels(undefined)).toEqual([]);
+ });
+});
+
+describe("normalizeClaudeCodeReasoningLevel", () => {
+ const model: AiModelInfo = {
+ id: "claude-sonnet-5",
+ supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
+ };
+
+ it("keeps an effort level reported by the selected model", () => {
+ expect(normalizeClaudeCodeReasoningLevel("xhigh", model)).toBe("xhigh");
+ });
+
+ it("falls back to default for unsupported or missing model metadata", () => {
+ expect(normalizeClaudeCodeReasoningLevel("minimal", model)).toBe("default");
+ expect(normalizeClaudeCodeReasoningLevel("max", { id: "default" })).toBe("default");
+ });
+});
diff --git a/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts b/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts
new file mode 100644
index 000000000..4fc71f620
--- /dev/null
+++ b/apps/desktop/src/lib/ai/__tests__/aiConfigOrdering.spec.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from "vitest";
+import { orderAiConfigsForDisplay } from "@/lib/ai/aiConfigOrdering";
+import type { AiProvider } from "@/types/ai";
+
+interface TestConfig {
+ id: string;
+ provider: AiProvider;
+}
+
+describe("orderAiConfigsForDisplay", () => {
+ it("matches the canonical provider order", () => {
+ const configs: TestConfig[] = [
+ { id: "claude-code-1", provider: "claude-code-cli" },
+ { id: "claude", provider: "claude" },
+ { id: "openai", provider: "openai" },
+ { id: "codex", provider: "codex-cli" },
+ { id: "custom", provider: "custom" },
+ ];
+
+ expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude", "openai", "claude-code-1", "codex", "custom"]);
+ });
+
+ it("preserves creation order for configs from the same provider", () => {
+ const configs: TestConfig[] = [
+ { id: "codex-1", provider: "codex-cli" },
+ { id: "claude-code-1", provider: "claude-code-cli" },
+ { id: "codex-2", provider: "codex-cli" },
+ { id: "claude-code-2", provider: "claude-code-cli" },
+ ];
+
+ expect(orderAiConfigsForDisplay(configs).map((config) => config.id)).toEqual(["claude-code-1", "claude-code-2", "codex-1", "codex-2"]);
+ });
+});
diff --git a/apps/desktop/src/lib/ai/aiConfigOrdering.ts b/apps/desktop/src/lib/ai/aiConfigOrdering.ts
new file mode 100644
index 000000000..1ee6198e2
--- /dev/null
+++ b/apps/desktop/src/lib/ai/aiConfigOrdering.ts
@@ -0,0 +1,15 @@
+import { AI_PROVIDER_PRESETS } from "@/stores/settingsStore";
+import type { AiProvider } from "@/types/ai";
+
+const AI_PROVIDER_DISPLAY_RANK = new Map
((Object.keys(AI_PROVIDER_PRESETS) as AiProvider[]).map((provider, index) => [provider, index]));
+
+/**
+ * Match configured-provider lists to the canonical provider picker order while
+ * preserving creation order between configs that use the same provider.
+ */
+export function orderAiConfigsForDisplay(configs: readonly T[]): T[] {
+ return configs
+ .map((config, index) => ({ config, index }))
+ .sort((a, b) => (AI_PROVIDER_DISPLAY_RANK.get(a.config.provider) ?? Number.MAX_SAFE_INTEGER) - (AI_PROVIDER_DISPLAY_RANK.get(b.config.provider) ?? Number.MAX_SAFE_INTEGER) || a.index - b.index)
+ .map(({ config }) => config);
+}
diff --git a/apps/desktop/src/lib/ai/aiModelEffort.ts b/apps/desktop/src/lib/ai/aiModelEffort.ts
new file mode 100644
index 000000000..2016de91d
--- /dev/null
+++ b/apps/desktop/src/lib/ai/aiModelEffort.ts
@@ -0,0 +1,28 @@
+import type { AiEffortLevel, AiReasoningLevel } from "@/types/ai";
+
+interface AiModelEffortMetadata {
+ supportedEffortLevels?: unknown;
+}
+
+const AI_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
+
+export function normalizeAiModelEffortLevels(value: unknown): AiEffortLevel[] {
+ if (!Array.isArray(value)) return [];
+
+ const seen = new Set();
+ const levels: AiEffortLevel[] = [];
+ for (const level of value) {
+ if (typeof level !== "string" || !AI_EFFORT_LEVELS.has(level as AiEffortLevel)) continue;
+ const normalized = level as AiEffortLevel;
+ if (seen.has(normalized)) continue;
+ seen.add(normalized);
+ levels.push(normalized);
+ }
+ return levels;
+}
+
+export function normalizeClaudeCodeReasoningLevel(reasoningLevel: AiReasoningLevel | undefined, model: AiModelEffortMetadata | undefined): AiReasoningLevel {
+ if (!reasoningLevel || reasoningLevel === "default") return "default";
+ const supported = normalizeAiModelEffortLevels(model?.supportedEffortLevels);
+ return supported.includes(reasoningLevel as AiEffortLevel) ? reasoningLevel : "default";
+}
diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts
index 310fd5cac..63b69a479 100644
--- a/apps/desktop/src/lib/backend/tauri.ts
+++ b/apps/desktop/src/lib/backend/tauri.ts
@@ -41,8 +41,7 @@ import type {
import { isTauriCommandUnavailable, normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
import type { CollectionInfo } from "@/types/database";
import type { SidebarObjectKind } from "@/lib/database/databaseObjectCapabilities";
-import type { AiConfig, AiTestConnectionResult } from "@/stores/settingsStore";
-import type { AiConfigItem } from "@/types/ai";
+import type { AiConfig, AiConfigItem, AiEffortLevel, AiTestConnectionResult } from "@/types/ai";
import type { QueryEditability } from "@/lib/sql/sqlAnalysis";
import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
import type {
@@ -336,6 +335,7 @@ export interface AiCompletionRequest {
export interface AiModelInfo {
id: string;
displayName?: string;
+ supportedEffortLevels?: AiEffortLevel[];
}
export async function aiComplete(request: AiCompletionRequest): Promise {
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index dee6e3731..0c938f82b 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -15,9 +15,9 @@ import { setDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateFields } from "@/lib/table/tableColumnTemplates";
import { DEFAULT_UI_FONT_FAMILY } from "@/lib/app/appFonts";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
-import type { AiProvider, AiApiStyle, AiAuthMethod, AiReasoningLevel, AiConfig, AiTestConnectionResult, AiConfigItem } from "@/types/ai";
+import type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem } from "@/types/ai";
-export type { AiProvider, AiApiStyle, AiAuthMethod, AiReasoningLevel, AiConfig, AiTestConnectionResult, AiConfigItem };
+export type { AiProvider, AiApiStyle, AiAuthMethod, AiEffortLevel, AiReasoningLevel, AiConfiguredModel, AiConfig, AiTestConnectionResult, AiConfigItem };
export interface DesktopSettings {
show_tray_icon: boolean;
@@ -164,6 +164,16 @@ export const AI_PROVIDER_PRESETS: Record = {
authMethod: "bearer",
requiresApiKey: true,
},
+ "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",
@@ -192,7 +202,7 @@ const defaultConfigs: Record> = Object.from
}),
) as Record>;
-const AI_REASONING_LEVELS: AiReasoningLevel[] = ["default", "minimal", "low", "medium", "high"];
+const AI_REASONING_LEVELS: AiReasoningLevel[] = ["default", "minimal", "low", "medium", "high", "xhigh", "max"];
const AI_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
function normalizeAiReasoningLevel(value: unknown): AiReasoningLevel {
@@ -226,6 +236,8 @@ export function normalizeAiConfig(config: Partial | null | undefined):
contextWindow: config?.contextWindow ?? undefined,
codexCliPath: config?.codexCliPath?.trim() || undefined,
codexCliEnv: normalizeAiEnv(config?.codexCliEnv),
+ claudeCodeCliPath: config?.claudeCodeCliPath?.trim() || undefined,
+ claudeCodeCliEnv: normalizeAiEnv(config?.claudeCodeCliEnv),
};
}
@@ -1022,7 +1034,7 @@ export const useSettingsStore = defineStore("settings", () => {
const config = aiConfigs.value.find((c) => c.id === activeModel.value!.configId);
if (!config) return false;
const preset = AI_PROVIDER_PRESETS[config.provider];
- if (config.provider === "codex-cli") return true;
+ if (config.provider === "codex-cli" || config.provider === "claude-code-cli") return true;
return !!config.endpoint && !!activeModel.value!.modelId && (!preset.requiresApiKey || !!config.apiKey);
});
diff --git a/apps/desktop/src/types/ai.ts b/apps/desktop/src/types/ai.ts
index 5db8f4c0b..9b2e4ae4b 100644
--- a/apps/desktop/src/types/ai.ts
+++ b/apps/desktop/src/types/ai.ts
@@ -1,7 +1,14 @@
-export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "ollama" | "openai-compatible" | "codex-cli" | "custom";
+export type AiProvider = "claude" | "openai" | "gemini" | "deepseek" | "qwen" | "ollama" | "openai-compatible" | "claude-code-cli" | "codex-cli" | "custom";
export type AiApiStyle = "completions" | "responses" | "anthropic-messages";
export type AiAuthMethod = "api-key" | "bearer";
-export type AiReasoningLevel = "default" | "minimal" | "low" | "medium" | "high";
+export type AiEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
+export type AiReasoningLevel = "default" | "minimal" | AiEffortLevel;
+
+export interface AiConfiguredModel {
+ name: string;
+ label?: string;
+ supportedEffortLevels?: AiEffortLevel[];
+}
export interface AiConfig {
provider: AiProvider;
@@ -9,7 +16,7 @@ export interface AiConfig {
authMethod: AiAuthMethod;
endpoint: string;
model: string;
- models?: Array<{ name: string; label?: string }>;
+ models?: AiConfiguredModel[];
apiStyle: AiApiStyle;
proxyEnabled?: boolean;
proxyUrl?: string;
@@ -18,6 +25,8 @@ export interface AiConfig {
contextWindow?: number;
codexCliPath?: string | null;
codexCliEnv?: Record;
+ claudeCodeCliPath?: string | null;
+ claudeCodeCliEnv?: Record;
}
export interface AiTestConnectionResult {
diff --git a/crates/dbx-core/src/agent_loop.rs b/crates/dbx-core/src/agent_loop.rs
index fe408d898..f6e957b1d 100644
--- a/crates/dbx-core/src/agent_loop.rs
+++ b/crates/dbx-core/src/agent_loop.rs
@@ -101,7 +101,7 @@ pub async fn run_agent_loop(
let contract_system_prompt = augment_system_prompt_with_task_contract(system_prompt, task_contract, is_agent_mode);
let system_prompt = contract_system_prompt.as_str();
- if matches!(config.provider, AiProvider::CodexCli) {
+ if matches!(config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
let connection_name = {
let configs = agent_ctx.state.configs.read().await;
configs
@@ -109,24 +109,27 @@ pub async fn run_agent_loop(
.map(|config| config.name.clone())
.unwrap_or_else(|| agent_ctx.connection_id.clone())
};
+ let options = crate::ai_cli_agent::CliAgentRunOptions {
+ connection_id: agent_ctx.connection_id.clone(),
+ connection_name,
+ database: agent_ctx.database.clone(),
+ agent_mode: is_agent_mode,
+ allow_writes: agent_ctx.sql_permissions.allow_writes,
+ allow_dangerous: agent_ctx.sql_permissions.allow_dangerous,
+ mcp_server_command: agent_ctx.cli_mcp_server_command.clone(),
+ };
+ if matches!(config.provider, AiProvider::ClaudeCodeCli) {
+ let prompt = crate::ai_claude_code_cli::build_claude_code_prompt(
+ system_prompt,
+ messages,
+ agent_ctx.sql_permissions.allow_writes,
+ );
+ return crate::ai_claude_code_cli::run_claude_code_agent(config, &prompt, options, cancelled, on_event)
+ .await;
+ }
let prompt =
crate::ai_codex_cli::build_codex_prompt(system_prompt, messages, agent_ctx.sql_permissions.allow_writes);
- return crate::ai_codex_cli::run_codex_agent(
- config,
- &prompt,
- crate::ai_codex_cli::CodexRunOptions {
- connection_id: agent_ctx.connection_id.clone(),
- connection_name,
- database: agent_ctx.database.clone(),
- agent_mode: is_agent_mode,
- allow_writes: agent_ctx.sql_permissions.allow_writes,
- allow_dangerous: agent_ctx.sql_permissions.allow_dangerous,
- mcp_server_command: agent_ctx.cli_mcp_server_command.clone(),
- },
- cancelled,
- on_event,
- )
- .await;
+ return crate::ai_codex_cli::run_codex_agent(config, &prompt, options, cancelled, on_event).await;
}
// Auto-degrade: providers without function calling fall back to text-only completion.
diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs
index e032625f8..631c98a87 100644
--- a/crates/dbx-core/src/ai.rs
+++ b/crates/dbx-core/src/ai.rs
@@ -61,6 +61,8 @@ pub enum AiProvider {
OpenaiCompatible,
#[serde(rename = "codex-cli")]
CodexCli,
+ #[serde(rename = "claude-code-cli")]
+ ClaudeCodeCli,
Custom,
}
@@ -74,6 +76,7 @@ impl AiProvider {
AiProvider::Qwen => "qwen",
AiProvider::Ollama => "ollama",
AiProvider::OpenaiCompatible => "openai-compatible",
+ AiProvider::ClaudeCodeCli => "claude-code-cli",
AiProvider::CodexCli => "codex-cli",
AiProvider::Custom => "custom",
}
@@ -107,6 +110,8 @@ pub enum AiReasoningLevel {
Low,
Medium,
High,
+ Xhigh,
+ Max,
}
impl AiReasoningLevel {
@@ -117,6 +122,43 @@ impl AiReasoningLevel {
AiReasoningLevel::Low => Some("low"),
AiReasoningLevel::Medium => Some("medium"),
AiReasoningLevel::High => Some("high"),
+ AiReasoningLevel::Xhigh | AiReasoningLevel::Max => None,
+ }
+ }
+
+ pub fn as_claude_code_effort(&self) -> Option<&'static str> {
+ match self {
+ AiReasoningLevel::Default | AiReasoningLevel::Minimal => None,
+ AiReasoningLevel::Low => Some("low"),
+ AiReasoningLevel::Medium => Some("medium"),
+ AiReasoningLevel::High => Some("high"),
+ AiReasoningLevel::Xhigh => Some("xhigh"),
+ AiReasoningLevel::Max => Some("max"),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(rename_all = "lowercase")]
+pub enum AiEffortLevel {
+ Low,
+ Medium,
+ High,
+ Xhigh,
+ Max,
+}
+
+impl std::str::FromStr for AiEffortLevel {
+ type Err = ();
+
+ fn from_str(value: &str) -> Result {
+ match value {
+ "low" => Ok(Self::Low),
+ "medium" => Ok(Self::Medium),
+ "high" => Ok(Self::High),
+ "xhigh" => Ok(Self::Xhigh),
+ "max" => Ok(Self::Max),
+ _ => Err(()),
}
}
}
@@ -144,6 +186,8 @@ pub struct AiModelListItem {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub supported_effort_levels: Vec,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -176,6 +220,10 @@ pub struct AiConfig {
pub codex_cli_path: Option,
#[serde(default)]
pub codex_cli_env: HashMap,
+ #[serde(default)]
+ pub claude_code_cli_path: Option,
+ #[serde(default)]
+ pub claude_code_cli_env: HashMap,
}
fn default_enable_thinking() -> bool {
@@ -273,6 +321,14 @@ pub struct AiModelInfo {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub supported_effort_levels: Vec,
+}
+
+impl AiModelInfo {
+ pub fn new(id: impl Into, display_name: Option) -> Self {
+ Self { id: id.into(), display_name, supported_effort_levels: Vec::new() }
+ }
}
/// Result of an AI connection test (mirrors CC-Switch's StreamCheckResult).
@@ -358,7 +414,7 @@ pub fn resolve_endpoint(config: &AiConfig) -> String {
format!("{base}/chat/completions")
}
}
- AiProvider::Claude | AiProvider::CodexCli | AiProvider::Gemini => unreachable!(),
+ AiProvider::Claude | AiProvider::CodexCli | AiProvider::ClaudeCodeCli | AiProvider::Gemini => unreachable!(),
}
}
@@ -746,7 +802,7 @@ fn emit_responses_function_call_item(
// ---------------------------------------------------------------------------
fn validate_config(config: &AiConfig) -> Result<(), String> {
- if matches!(config.provider, AiProvider::CodexCli) {
+ if matches!(config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
return Ok(());
}
if !matches!(config.provider, AiProvider::Ollama) && config.api_key.trim().is_empty() {
@@ -762,7 +818,7 @@ fn validate_config(config: &AiConfig) -> Result<(), String> {
}
fn validate_model_list_config(config: &AiConfig) -> Result<(), String> {
- if matches!(config.provider, AiProvider::CodexCli) {
+ if matches!(config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
return Ok(());
}
if !matches!(config.provider, AiProvider::Ollama) && config.api_key.trim().is_empty() {
@@ -854,7 +910,7 @@ fn parse_model_list_response(data: &serde_json::Value) -> Result Result, Str
if matches!(config.provider, AiProvider::CodexCli) {
return crate::ai_codex_cli::list_codex_models(config).await;
}
+ if matches!(config.provider, AiProvider::ClaudeCodeCli) {
+ return crate::ai_claude_code_cli::list_claude_code_models(config).await;
+ }
validate_model_list_config(config)?;
let client = build_ai_http_client(config, 30)?;
@@ -919,7 +978,7 @@ pub async fn list_models_core(config: &AiConfig) -> Result, Str
list_openai_compatible_models(&client, config).await
}
}
- AiProvider::CodexCli => unreachable!(),
+ AiProvider::CodexCli | AiProvider::ClaudeCodeCli => unreachable!(),
AiProvider::Gemini => {
Err("Model listing is only supported for OpenAI-compatible and Claude providers".to_string())
}
@@ -1133,6 +1192,9 @@ pub async fn test_connection_core(config: &AiConfig) -> Result &'static str {
pub async fn complete(request: &AiCompletionRequest) -> Result {
validate_config(&request.config)?;
- if matches!(request.config.provider, AiProvider::CodexCli) {
- return Err("Codex CLI provider is only supported in DBX AI agent mode".to_string());
+ if matches!(request.config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
+ return Err("CLI providers are only supported in DBX AI agent mode".to_string());
}
let client = build_ai_http_client(&request.config, 60)?;
@@ -1317,7 +1379,7 @@ pub async fn complete(request: &AiCompletionRequest) -> Result {
match request.config.provider {
AiProvider::Claude => call_claude(&client, request.clone()).await,
AiProvider::Gemini => call_gemini(&client, request.clone()).await,
- AiProvider::CodexCli => unreachable!(),
+ AiProvider::CodexCli | AiProvider::ClaudeCodeCli => unreachable!(),
AiProvider::Openai
| AiProvider::Deepseek
| AiProvider::Qwen
@@ -1353,8 +1415,8 @@ pub async fn stream(
) -> Result<(), String> {
validate_config(&request.config)?;
- if matches!(request.config.provider, AiProvider::CodexCli) {
- return Err("Codex CLI provider is only supported in DBX AI agent mode".to_string());
+ if matches!(request.config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
+ return Err("CLI providers are only supported in DBX AI agent mode".to_string());
}
let stream_timeout = if request.config.enable_thinking { 600 } else { 120 };
@@ -1363,7 +1425,7 @@ pub async fn stream(
match request.config.provider {
AiProvider::Claude => stream_claude(&client, session_id, request, cancelled, &on_chunk).await,
AiProvider::Gemini => stream_gemini(&client, session_id, request, cancelled, &on_chunk).await,
- AiProvider::CodexCli => unreachable!(),
+ AiProvider::CodexCli | AiProvider::ClaudeCodeCli => unreachable!(),
AiProvider::Openai
| AiProvider::Deepseek
| AiProvider::Qwen
@@ -2422,6 +2484,9 @@ pub async fn stream_with_tools(
on_chunk: impl Fn(AiStreamChunk),
) -> Result<(Vec, Option), String> {
validate_config(config)?;
+ if matches!(config.provider, AiProvider::CodexCli | AiProvider::ClaudeCodeCli) {
+ return Err("CLI providers are only supported through the DBX AI agent loop".to_string());
+ }
let stream_timeout = if config.enable_thinking { 600 } else { 120 };
let client = build_ai_http_client(config, stream_timeout)?;
@@ -2606,6 +2671,8 @@ mod tests {
assert_eq!(config.proxy_url, "");
assert!(config.enable_thinking);
assert_eq!(config.auth_method, AiAuthMethod::ApiKey);
+ assert!(config.claude_code_cli_path.is_none());
+ assert!(config.claude_code_cli_env.is_empty());
assert!(config.codex_cli_env.is_empty());
}
@@ -2626,6 +2693,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let err = build_ai_http_client(&config, 1).unwrap_err();
@@ -2650,6 +2719,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
build_ai_http_client(&config, 1).unwrap();
@@ -2672,6 +2743,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
build_ai_http_client(&config, 1).unwrap();
@@ -2694,6 +2767,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert_eq!(
@@ -2720,6 +2795,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert_eq!(resolve_endpoint(&ollama), "http://localhost:11434/v1/chat/completions");
@@ -2743,6 +2820,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert_eq!(resolve_model_list_endpoint(&openai).unwrap(), "https://api.openai.com/v1/models");
@@ -2761,6 +2840,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert_eq!(resolve_model_list_endpoint(&claude).unwrap(), "https://api.anthropic.com/v1/models");
}
@@ -2782,6 +2863,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert!(uses_anthropic_messages_api(&config));
@@ -2827,6 +2910,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
assert_eq!(resolve_endpoint(&config), "https://api.example.com/v1/chat/completions");
assert_eq!(resolve_model_list_endpoint(&config).unwrap(), "https://api.example.com/v1/models");
@@ -2884,6 +2969,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let api_key_headers = claude_headers(&config).unwrap();
@@ -2923,11 +3010,8 @@ mod tests {
assert_eq!(
parse_model_list_response(&data).unwrap(),
vec![
- AiModelInfo { id: "gpt-4o-mini".to_string(), display_name: None },
- AiModelInfo {
- id: "claude-sonnet-4-20250514".to_string(),
- display_name: Some("Claude Sonnet 4".to_string())
- },
+ AiModelInfo::new("gpt-4o-mini", None),
+ AiModelInfo::new("claude-sonnet-4-20250514", Some("Claude Sonnet 4".to_string())),
]
);
}
@@ -3173,6 +3257,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let mut body = serde_json::json!({
@@ -3239,6 +3325,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let mut body = serde_json::json!({
"model": &config.model,
@@ -3270,6 +3358,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -3307,6 +3397,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let mut body = serde_json::json!({ "model": &config.model });
@@ -3338,6 +3430,8 @@ mod tests {
context_window: None,
codex_cli_path: None,
codex_cli_env: Default::default(),
+ claude_code_cli_path: None,
+ claude_code_cli_env: Default::default(),
};
let mut body = serde_json::json!({
"model": &config.model,
diff --git a/crates/dbx-core/src/ai_claude_code_cli.rs b/crates/dbx-core/src/ai_claude_code_cli.rs
new file mode 100644
index 000000000..2747989f2
--- /dev/null
+++ b/crates/dbx-core/src/ai_claude_code_cli.rs
@@ -0,0 +1,855 @@
+use crate::agent_events::AgentEvent;
+use crate::ai::{AiConfig, AiEffortLevel, AiModelInfo, AiTestConnectionResult};
+use crate::ai_cli_agent::{
+ build_cli_agent_prompt, cli_command, dbx_mcp_enabled_tools, dbx_mcp_scope_env, model_infos, parse_cli_jsonl_event,
+ run_cli_jsonl_agent, CliAgentCommandSpec, CliAgentJsonlDialect, CliAgentProcessSpec, CliAgentRunOptions,
+};
+use serde_json::{json, Map, Value};
+use std::collections::{BTreeMap, BTreeSet};
+use std::env;
+use std::path::{Path, PathBuf};
+use std::process::Stdio;
+use std::time::{Duration, Instant};
+use tokio::io::AsyncWriteExt;
+use tokio::sync::Notify;
+
+const DEFAULT_CLAUDE_CODE_MODELS: &[&str] = &["default", "sonnet", "opus", "fable"];
+const CLAUDE_CODE_MODEL_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(10);
+const CLAUDE_CODE_SETTING_SOURCES: &str = "user";
+
+pub type ClaudeCodeRunOptions = CliAgentRunOptions;
+pub type ClaudeCodeCommandSpec = CliAgentCommandSpec;
+
+struct ClaudeCodeIsolatedCwd {
+ path: PathBuf,
+}
+
+impl ClaudeCodeIsolatedCwd {
+ fn create() -> Result {
+ let path = env::temp_dir().join(format!("dbx-claude-code-{}", uuid::Uuid::new_v4()));
+ std::fs::create_dir(&path).map_err(|error| {
+ format!("[claudeCodeRunFailed] Failed to create isolated Claude Code directory: {error}")
+ })?;
+ Ok(Self { path })
+ }
+}
+
+impl Drop for ClaudeCodeIsolatedCwd {
+ fn drop(&mut self) {
+ let _ = std::fs::remove_dir_all(&self.path);
+ }
+}
+
+fn append_claude_code_isolation_args(args: &mut Vec) {
+ // User settings retain authentication and preferences without loading project hooks or local overrides.
+ args.extend(["--setting-sources".to_string(), CLAUDE_CODE_SETTING_SOURCES.to_string()]);
+}
+
+fn claude_code_program(config: &AiConfig) -> String {
+ config
+ .claude_code_cli_path
+ .as_deref()
+ .map(str::trim)
+ .filter(|path| !path.is_empty())
+ .unwrap_or("claude")
+ .to_string()
+}
+
+fn claude_code_process_env(
+ config: &AiConfig,
+ command: &ClaudeCodeCommandSpec,
+) -> Result, String> {
+ let mut env = BTreeMap::from_iter(claude_code_cli_env(config)?);
+ if let Some(dir) = command_parent_dir(command) {
+ let user_path = env.get("PATH").map(String::as_str);
+ env.insert("PATH".to_string(), merged_path_with_dir(&dir, user_path));
+ }
+ Ok(env.into_iter().collect())
+}
+
+fn command_parent_dir(command: &ClaudeCodeCommandSpec) -> Option {
+ Path::new(&command.program)
+ .parent()
+ .filter(|parent| !parent.as_os_str().is_empty())
+ .map(|parent| parent.to_string_lossy().to_string())
+}
+
+fn merged_path_with_dir(dir: &str, user_path: Option<&str>) -> String {
+ let mut seen = BTreeSet::new();
+ let mut dirs = vec![PathBuf::from(dir)];
+ if let Some(path) = user_path {
+ dirs.extend(env::split_paths(path));
+ }
+ dirs.extend(common_executable_dirs());
+ let paths = dirs.into_iter().filter(|path| seen.insert(path.clone())).collect::>();
+ env::join_paths(paths).unwrap_or_default().to_string_lossy().to_string()
+}
+
+fn common_executable_dirs() -> Vec {
+ let mut dirs = Vec::new();
+ if let Ok(path) = env::var("PATH") {
+ dirs.extend(env::split_paths(&path));
+ }
+ #[cfg(windows)]
+ {
+ if let Ok(app_data) = env::var("APPDATA") {
+ dirs.push(PathBuf::from(app_data).join("npm"));
+ }
+ }
+ #[cfg(not(windows))]
+ {
+ dirs.extend([
+ PathBuf::from("/opt/homebrew/bin"),
+ PathBuf::from("/usr/local/bin"),
+ PathBuf::from("/usr/bin"),
+ PathBuf::from("/bin"),
+ PathBuf::from("/usr/sbin"),
+ PathBuf::from("/sbin"),
+ ]);
+ }
+ dirs
+}
+
+fn validate_claude_code_program(config: &AiConfig) -> Result {
+ let program = claude_code_program(config);
+ if starts_with_env_assignment(&program) {
+ return Err("[claudeCodeCliPathInvalid] Claude Code CLI path should contain only the executable path. Add environment variables in the Claude Code CLI environment variables section.".to_string());
+ }
+ if is_path_like_program(&program) {
+ let expanded = crate::path_utils::expand_tilde(&program);
+ let path = Path::new(&expanded);
+ if path.is_dir() {
+ return launchable_program_in_dir(path, "claude").ok_or_else(|| {
+ "[claudeCodeCliPathInvalid] Claude Code CLI path should point to the claude executable or a directory containing claude."
+ .to_string()
+ });
+ }
+ return Ok(expanded);
+ }
+ Ok(program)
+}
+
+fn launchable_program_in_dir(dir: &Path, program: &str) -> Option {
+ program_path_candidates(dir, program)
+ .into_iter()
+ .find(|candidate| is_launchable_program_path(candidate) && candidate.is_file())
+ .map(|path| path.to_string_lossy().to_string())
+}
+
+#[cfg(not(windows))]
+fn program_path_candidates(dir: &Path, program: &str) -> Vec {
+ vec![dir.join(program)]
+}
+
+#[cfg(windows)]
+fn program_path_candidates(dir: &Path, program: &str) -> Vec {
+ let path = Path::new(program);
+ if path.extension().is_some() {
+ return vec![dir.join(program)];
+ }
+ [".cmd", ".exe", ".bat", ".com", ""].iter().map(|extension| dir.join(format!("{program}{extension}"))).collect()
+}
+
+#[cfg(not(windows))]
+fn is_launchable_program_path(_path: &Path) -> bool {
+ true
+}
+
+#[cfg(windows)]
+fn is_launchable_program_path(path: &Path) -> bool {
+ matches!(
+ path.extension().and_then(|extension| extension.to_str()).map(str::to_ascii_lowercase).as_deref(),
+ Some("exe" | "cmd" | "bat" | "com")
+ )
+}
+
+fn is_path_like_program(program: &str) -> bool {
+ program.contains('/') || program.contains('\\') || program.starts_with('~')
+}
+
+fn starts_with_env_assignment(program: &str) -> bool {
+ let Some(first_token) = program.split_whitespace().next() else {
+ return false;
+ };
+ let Some((key, _)) = first_token.split_once('=') else {
+ return false;
+ };
+ is_env_var_name(key)
+}
+
+fn is_env_var_name(name: &str) -> bool {
+ let mut chars = name.chars();
+ let Some(first) = chars.next() else {
+ return false;
+ };
+ (first == '_' || first.is_ascii_alphabetic()) && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
+}
+
+fn is_reserved_dbx_mcp_env_name(name: &str) -> bool {
+ name.to_ascii_uppercase().starts_with("DBX_MCP_")
+}
+
+pub fn claude_code_cli_env(config: &AiConfig) -> Result, String> {
+ let mut env = BTreeMap::new();
+ for (key, value) in &config.claude_code_cli_env {
+ let key = key.trim();
+ if key.is_empty() {
+ continue;
+ }
+ if !is_env_var_name(key) {
+ return Err(format!(
+ "[claudeCodeEnvInvalid] Invalid Claude Code CLI environment variable name `{key}`. Use names like HTTPS_PROXY."
+ ));
+ }
+ if is_reserved_dbx_mcp_env_name(key) {
+ return Err(format!(
+ "[claudeCodeEnvReserved] `{key}` is managed by DBX for the scoped MCP server and cannot be set here."
+ ));
+ }
+ env.insert(key.to_string(), value.clone());
+ }
+ Ok(env.into_iter().collect())
+}
+
+pub fn claude_code_enabled_tools(agent_mode: bool) -> Vec {
+ dbx_mcp_enabled_tools(agent_mode).into_iter().map(|tool| format!("mcp__dbx__{tool}")).collect()
+}
+
+fn claude_code_mcp_config(options: &ClaudeCodeRunOptions) -> String {
+ let mcp_command =
+ options.mcp_server_command.as_ref().map(|command| command.program.as_str()).unwrap_or("dbx-mcp-server");
+ let mut server = Map::new();
+ server.insert("command".to_string(), Value::String(mcp_command.to_string()));
+ if let Some(command) = options.mcp_server_command.as_ref().filter(|command| !command.args.is_empty()) {
+ server.insert("args".to_string(), json!(command.args));
+ }
+
+ let env = dbx_mcp_scope_env(options)
+ .into_iter()
+ .map(|(name, value)| (name.to_string(), Value::String(value)))
+ .collect::