From cb4b29f8308668b30a070a607940dcb989e52c45 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 21 Nov 2025 10:16:08 +0800 Subject: [PATCH 1/5] fix: windows acp spawn --- .../agent/acpProcessManager.ts | 153 ++++++++++-------- 1 file changed, 90 insertions(+), 63 deletions(-) diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index 520643204..93150552c 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcessWithoutNullStreams, execSync } from 'child_process' +import { spawn, type ChildProcessWithoutNullStreams } from 'child_process' import { Readable, Writable } from 'node:stream' import { app } from 'electron' import * as fs from 'fs' @@ -262,72 +262,63 @@ export class AcpProcessManager implements AgentProcessManager this.replaceWithRuntimeCommand(arg)) + // Keep args as-is, do not replace them + const processedArgs = agent.args ?? [] // Prepare environment variables const mergedEnv = agent.env ? { ...process.env, ...agent.env } : { ...process.env } @@ -398,8 +421,12 @@ export class AcpProcessManager implements AgentProcessManager Date: Fri, 21 Nov 2025 10:20:08 +0800 Subject: [PATCH 2/5] fix: add more logs --- .../agent/acpProcessManager.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index 93150552c..7a25c1932 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -382,8 +382,28 @@ export class AcpProcessManager implements AgentProcessManager empty` + ) + } + + // Log command processing for debugging + if (processedCommand !== agent.command) { + console.info( + `[ACP] Command replaced for agent ${agent.id}: "${agent.command}" -> "${processedCommand}"` + ) + } + // Keep args as-is, do not replace them const processedArgs = agent.args ?? [] @@ -422,7 +442,16 @@ export class AcpProcessManager implements AgentProcessManager Date: Fri, 21 Nov 2025 10:32:24 +0800 Subject: [PATCH 3/5] feat: add cross-spawn --- package.json | 1 + .../agent/acpProcessManager.ts | 128 +++++++++++++++--- 2 files changed, 110 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index a019d96eb..218e99287 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "better-sqlite3-multiple-ciphers": "12.4.1", "cheerio": "^1.1.2", "compare-versions": "^6.1.1", + "cross-spawn": "^7.0.6", "diff": "^7.0.0", "electron-log": "^5.4.3", "electron-store": "^8.2.0", diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index 7a25c1932..f29f8c858 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -1,4 +1,5 @@ -import { spawn, type ChildProcessWithoutNullStreams } from 'child_process' +import spawn from 'cross-spawn' +import type { ChildProcessWithoutNullStreams } from 'child_process' import { Readable, Writable } from 'node:stream' import { app } from 'electron' import * as fs from 'fs' @@ -35,6 +36,61 @@ interface SessionListenerEntry { handlers: Set } +/** + * Environment variables to inherit by default, if an environment is not explicitly given. + * Reference: @modelcontextprotocol/sdk/client/stdio.js + */ +const DEFAULT_INHERITED_ENV_VARS = + process.platform === 'win32' + ? [ + 'APPDATA', + 'HOMEDRIVE', + 'HOMEPATH', + 'LOCALAPPDATA', + 'PATH', + 'PROCESSOR_ARCHITECTURE', + 'SYSTEMDRIVE', + 'SYSTEMROOT', + 'TEMP', + 'USERNAME', + 'USERPROFILE', + 'PROGRAMFILES' + ] + : /* list inspired by the default env inheritance of sudo */ + ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'] + +/** + * Returns a default environment object including only environment variables deemed safe to inherit. + * Reference: @modelcontextprotocol/sdk/client/stdio.js + */ +function getDefaultEnvironment(): Record { + const env: Record = {} + + for (const key of DEFAULT_INHERITED_ENV_VARS) { + const value = process.env[key] + if (value === undefined) { + continue + } + + if (value.startsWith('()')) { + // Skip functions, which are a security risk. + continue + } + + env[key] = value + } + + return env +} + +/** + * Check if running in Electron environment. + * Reference: @modelcontextprotocol/sdk/client/stdio.js + */ +function isElectron(): boolean { + return 'type' in process +} + interface PermissionResolverEntry { agentId: string resolver: PermissionResolver @@ -398,6 +454,12 @@ export class AcpProcessManager implements AgentProcessManager "${processedCommand}"` @@ -407,57 +469,85 @@ export class AcpProcessManager implements AgentProcessManager { - if (value !== undefined && ['PATH', 'Path', 'path'].includes(key)) { - existingPaths.push(value) - } - }) + const pathKey = process.platform === 'win32' ? 'Path' : 'PATH' + if (defaultEnv[pathKey]) { + existingPaths.push(defaultEnv[pathKey]) + } const allPaths = [...existingPaths] if (process.platform === 'win32') { if (this.uvRuntimePath) { allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) } if (this.nodeRuntimePath) { allPaths.unshift(this.nodeRuntimePath) + console.info(`[ACP] Added Node runtime path to PATH: ${this.nodeRuntimePath}`) } } else { if (this.uvRuntimePath) { allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) } if (this.nodeRuntimePath) { - allPaths.unshift(path.join(this.nodeRuntimePath, 'bin')) + const nodeBinPath = path.join(this.nodeRuntimePath, 'bin') + allPaths.unshift(nodeBinPath) + console.info(`[ACP] Added Node bin path to PATH: ${nodeBinPath}`) } if (this.bunRuntimePath) { allPaths.unshift(this.bunRuntimePath) + console.info(`[ACP] Added Bun runtime path to PATH: ${this.bunRuntimePath}`) } } const { key, value } = this.normalizePathEnv(allPaths) - mergedEnv[key] = value + defaultEnv[key] = value + + // Merge default env with agent env (agent env takes precedence) + // Reference: @modelcontextprotocol/sdk/client/stdio.js + const mergedEnv = { + ...defaultEnv, + ...agent.env + } + + console.info(`[ACP] Environment variables for agent ${agent.id}:`, { + pathKey: key, + pathValue: value, + hasCustomEnv: !!agent.env, + customEnvKeys: agent.env ? Object.keys(agent.env) : [] + }) // Determine working directory (default to current working directory) let cwd = process.cwd() // Validate cwd exists if (!fs.existsSync(cwd)) { - console.warn(`[ACP] Working directory does not exist: ${cwd}, using process.cwd()`) - cwd = process.cwd() - // If still doesn't exist, use a safe fallback - if (!fs.existsSync(cwd)) { - cwd = process.platform === 'win32' ? 'C:\\' : '/' - } + console.warn(`[ACP] Working directory does not exist: ${cwd}, using fallback`) + cwd = process.platform === 'win32' ? 'C:\\' : '/' } - return spawn(processedCommand, processedArgs, { - env: mergedEnv, + console.info(`[ACP] Spawning process with options:`, { + command: processedCommand, + args: processedArgs, cwd, - stdio: ['pipe', 'pipe', 'pipe'] + platform: process.platform }) + + const child = spawn(processedCommand, processedArgs, { + env: mergedEnv, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + shell: false, + windowsHide: process.platform === 'win32' && isElectron() + }) as ChildProcessWithoutNullStreams + + console.info(`[ACP] Process spawned successfully for agent ${agent.id}, PID: ${child.pid}`) + + return child } private createAgentStream(child: ChildProcessWithoutNullStreams): Stream { From caaf1e5d24c1def8589c0b7fcf18b5f7efa18bb1 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 21 Nov 2025 10:51:39 +0800 Subject: [PATCH 4/5] feat: add toggle for buildin env --- .../configPresenter/acpConfHelper.ts | 14 +- src/main/presenter/configPresenter/index.ts | 8 + .../agent/acpProcessManager.ts | 276 ++++++++++++------ .../providers/acpProvider.ts | 5 +- .../settings/components/AcpSettings.vue | 40 +++ src/renderer/src/i18n/en-US/settings.json | 2 + src/renderer/src/i18n/fa-IR/settings.json | 2 + src/renderer/src/i18n/fr-FR/settings.json | 2 + src/renderer/src/i18n/ja-JP/settings.json | 2 + src/renderer/src/i18n/ko-KR/settings.json | 2 + src/renderer/src/i18n/pt-BR/settings.json | 2 + src/renderer/src/i18n/ru-RU/settings.json | 2 + src/renderer/src/i18n/zh-CN/settings.json | 2 + src/renderer/src/i18n/zh-HK/settings.json | 2 + src/renderer/src/i18n/zh-TW/settings.json | 2 + .../types/presenters/legacy.presenters.d.ts | 2 + 16 files changed, 273 insertions(+), 92 deletions(-) diff --git a/src/main/presenter/configPresenter/acpConfHelper.ts b/src/main/presenter/configPresenter/acpConfHelper.ts index f56d1eb86..f1b24edaf 100644 --- a/src/main/presenter/configPresenter/acpConfHelper.ts +++ b/src/main/presenter/configPresenter/acpConfHelper.ts @@ -35,7 +35,7 @@ const BUILTIN_TEMPLATES: Record = { name: DEFAULT_PROFILE_NAME, command: 'npx', args: ['-y', '@zed-industries/claude-code-acp'], - env: { ANTHROPIC_API_KEY: '' } + env: {} }) }, 'codex-acp': { @@ -44,7 +44,7 @@ const BUILTIN_TEMPLATES: Record = { name: DEFAULT_PROFILE_NAME, command: 'npx', args: ['-y', '@zed-industries/codex-acp'], - env: { OPENAI_API_KEY: '' } + env: {} }) } } @@ -52,6 +52,7 @@ const BUILTIN_TEMPLATES: Record = { type InternalStore = Partial & { agents?: AcpAgentConfig[] builtinsVersion?: string + useBuiltinRuntime?: boolean } const deepClone = (value: T): T => { @@ -71,6 +72,7 @@ export class AcpConfHelper { builtins: [], customs: [], enabled: false, + useBuiltinRuntime: false, version: ACP_STORE_VERSION } }) @@ -92,6 +94,14 @@ export class AcpConfHelper { return true } + getUseBuiltinRuntime(): boolean { + return Boolean(this.store.get('useBuiltinRuntime')) + } + + setUseBuiltinRuntime(enabled: boolean): void { + this.store.set('useBuiltinRuntime', enabled) + } + getEnabledAgents(): AcpAgentConfig[] { const data = this.getData() if (!data.enabled) { diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 8d8baba3a..87ea7a72d 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -947,6 +947,14 @@ export class ConfigPresenter implements IConfigPresenter { this.notifyAcpAgentsChanged() } + async getAcpUseBuiltinRuntime(): Promise { + return this.acpConfHelper.getUseBuiltinRuntime() + } + + async setAcpUseBuiltinRuntime(enabled: boolean): Promise { + this.acpConfHelper.setUseBuiltinRuntime(enabled) + } + // ===================== ACP configuration methods ===================== async getAcpAgents(): Promise { return this.acpConfHelper.getEnabledAgents() diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index f29f8c858..1005e7cd3 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -23,6 +23,7 @@ export interface AcpProcessHandle extends AgentProcessHandle { interface AcpProcessManagerOptions { providerId: string + getUseBuiltinRuntime: () => Promise } export type SessionNotificationHandler = (notification: schema.SessionNotification) => void @@ -36,53 +37,6 @@ interface SessionListenerEntry { handlers: Set } -/** - * Environment variables to inherit by default, if an environment is not explicitly given. - * Reference: @modelcontextprotocol/sdk/client/stdio.js - */ -const DEFAULT_INHERITED_ENV_VARS = - process.platform === 'win32' - ? [ - 'APPDATA', - 'HOMEDRIVE', - 'HOMEPATH', - 'LOCALAPPDATA', - 'PATH', - 'PROCESSOR_ARCHITECTURE', - 'SYSTEMDRIVE', - 'SYSTEMROOT', - 'TEMP', - 'USERNAME', - 'USERPROFILE', - 'PROGRAMFILES' - ] - : /* list inspired by the default env inheritance of sudo */ - ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'] - -/** - * Returns a default environment object including only environment variables deemed safe to inherit. - * Reference: @modelcontextprotocol/sdk/client/stdio.js - */ -function getDefaultEnvironment(): Record { - const env: Record = {} - - for (const key of DEFAULT_INHERITED_ENV_VARS) { - const value = process.env[key] - if (value === undefined) { - continue - } - - if (value.startsWith('()')) { - // Skip functions, which are a security risk. - continue - } - - env[key] = value - } - - return env -} - /** * Check if running in Electron environment. * Reference: @modelcontextprotocol/sdk/client/stdio.js @@ -98,6 +52,7 @@ interface PermissionResolverEntry { export class AcpProcessManager implements AgentProcessManager { private readonly providerId: string + private readonly getUseBuiltinRuntime: () => Promise private readonly handles = new Map() private readonly pendingHandles = new Map>() private readonly sessionListeners = new Map() @@ -109,6 +64,7 @@ export class AcpProcessManager implements AgentProcessManager { @@ -208,7 +164,7 @@ export class AcpProcessManager implements AgentProcessManager { - const child = this.spawnAgentProcess(agent) + const child = await this.spawnAgentProcess(agent) const stream = this.createAgentStream(child) const client = this.createClientProxy() const connection = new ClientSideConnection(() => client, stream) @@ -318,7 +274,12 @@ export class AcpProcessManager implements AgentProcessManager { // Initialize runtime paths if not already done this.setupRuntimes() + // Get useBuiltinRuntime configuration + const useBuiltinRuntime = await this.getUseBuiltinRuntime() + // Validate command if (!agent.command || agent.command.trim().length === 0) { throw new Error(`[ACP] Invalid command for agent ${agent.id}: command is empty`) } // Replace command with runtime version if needed - const processedCommand = this.replaceWithRuntimeCommand(agent.command) + const processedCommand = this.replaceWithRuntimeCommand(agent.command, useBuiltinRuntime) // Validate processed command if (!processedCommand || processedCommand.trim().length === 0) { @@ -469,55 +454,168 @@ export class AcpProcessManager implements AgentProcessManager + processedCommand.includes(cmd) || + processedArgs.some((arg) => typeof arg === 'string' && arg.includes(cmd)) + ) + + // Define allowed environment variables whitelist for Node.js/Bun/UV commands + const allowedEnvVars = [ + 'PATH', + 'path', + 'Path', + 'npm_config_registry', + 'npm_config_cache', + 'npm_config_prefix', + 'npm_config_tmp', + 'NPM_CONFIG_REGISTRY', + 'NPM_CONFIG_CACHE', + 'NPM_CONFIG_PREFIX', + 'NPM_CONFIG_TMP' + ] + + const HOME_DIR = app.getPath('home') + const env: Record = {} + let pathKey = process.platform === 'win32' ? 'Path' : 'PATH' + let pathValue = '' + + if (isNodeCommand) { + // Node.js/Bun/UV commands use whitelist processing + if (process.env) { + const existingPaths: string[] = [] + + // Collect all PATH-related values + Object.entries(process.env).forEach(([key, value]) => { + if (value !== undefined) { + if (['PATH', 'Path', 'path'].includes(key)) { + existingPaths.push(value) + } else if (allowedEnvVars.includes(key) && !['PATH', 'Path', 'path'].includes(key)) { + env[key] = value + } + } + }) + + // Get default paths + const defaultPaths = this.getDefaultPaths(HOME_DIR) + + // Merge all paths + const allPaths = [...existingPaths, ...defaultPaths] + // Add runtime paths + if (process.platform === 'win32') { + // Windows platform only adds node and uv paths + if (this.uvRuntimePath) { + allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) + } + if (this.nodeRuntimePath) { + allPaths.unshift(this.nodeRuntimePath) + console.info(`[ACP] Added Node runtime path to PATH: ${this.nodeRuntimePath}`) + } + } else { + // Other platforms priority: bun > node > uv + if (this.uvRuntimePath) { + allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) + } + if (this.nodeRuntimePath) { + const nodeBinPath = path.join(this.nodeRuntimePath, 'bin') + allPaths.unshift(nodeBinPath) + console.info(`[ACP] Added Node bin path to PATH: ${nodeBinPath}`) + } + if (this.bunRuntimePath) { + allPaths.unshift(this.bunRuntimePath) + console.info(`[ACP] Added Bun runtime path to PATH: ${this.bunRuntimePath}`) + } + } - const allPaths = [...existingPaths] - if (process.platform === 'win32') { - if (this.uvRuntimePath) { - allPaths.unshift(this.uvRuntimePath) - console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) - } - if (this.nodeRuntimePath) { - allPaths.unshift(this.nodeRuntimePath) - console.info(`[ACP] Added Node runtime path to PATH: ${this.nodeRuntimePath}`) + // Normalize and set PATH + const normalized = this.normalizePathEnv(allPaths) + pathKey = normalized.key + pathValue = normalized.value + env[pathKey] = pathValue } } else { - if (this.uvRuntimePath) { - allPaths.unshift(this.uvRuntimePath) - console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) + // Non Node.js/Bun/UV commands, preserve all system environment variables, only supplement PATH + Object.entries(process.env).forEach(([key, value]) => { + if (value !== undefined) { + env[key] = value + } + }) + + // Supplement PATH + const existingPaths: string[] = [] + if (env.PATH) { + existingPaths.push(env.PATH) } - if (this.nodeRuntimePath) { - const nodeBinPath = path.join(this.nodeRuntimePath, 'bin') - allPaths.unshift(nodeBinPath) - console.info(`[ACP] Added Node bin path to PATH: ${nodeBinPath}`) + if (env.Path) { + existingPaths.push(env.Path) } - if (this.bunRuntimePath) { - allPaths.unshift(this.bunRuntimePath) - console.info(`[ACP] Added Bun runtime path to PATH: ${this.bunRuntimePath}`) + + // Get default paths + const defaultPaths = this.getDefaultPaths(HOME_DIR) + + // Merge all paths + const allPaths = [...existingPaths, ...defaultPaths] + // Add runtime paths + if (process.platform === 'win32') { + // Windows platform only adds node and uv paths + if (this.uvRuntimePath) { + allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) + } + if (this.nodeRuntimePath) { + allPaths.unshift(this.nodeRuntimePath) + console.info(`[ACP] Added Node runtime path to PATH: ${this.nodeRuntimePath}`) + } + } else { + // Other platforms priority: bun > node > uv + if (this.uvRuntimePath) { + allPaths.unshift(this.uvRuntimePath) + console.info(`[ACP] Added UV runtime path to PATH: ${this.uvRuntimePath}`) + } + if (this.nodeRuntimePath) { + const nodeBinPath = path.join(this.nodeRuntimePath, 'bin') + allPaths.unshift(nodeBinPath) + console.info(`[ACP] Added Node bin path to PATH: ${nodeBinPath}`) + } + if (this.bunRuntimePath) { + allPaths.unshift(this.bunRuntimePath) + console.info(`[ACP] Added Bun runtime path to PATH: ${this.bunRuntimePath}`) + } } - } - const { key, value } = this.normalizePathEnv(allPaths) - defaultEnv[key] = value + // Normalize and set PATH + const normalized = this.normalizePathEnv(allPaths) + pathKey = normalized.key + pathValue = normalized.value + env[pathKey] = pathValue + } - // Merge default env with agent env (agent env takes precedence) - // Reference: @modelcontextprotocol/sdk/client/stdio.js - const mergedEnv = { - ...defaultEnv, - ...agent.env + // Add custom environment variables + if (agent.env) { + Object.entries(agent.env).forEach(([key, value]) => { + if (value !== undefined) { + // If it's a PATH-related variable, merge into main PATH + if (['PATH', 'Path', 'path'].includes(key)) { + const currentPathKey = process.platform === 'win32' ? 'Path' : 'PATH' + const separator = process.platform === 'win32' ? ';' : ':' + env[currentPathKey] = env[currentPathKey] + ? `${value}${separator}${env[currentPathKey]}` + : value + } else { + env[key] = value + } + } + }) } + const mergedEnv = env + console.info(`[ACP] Environment variables for agent ${agent.id}:`, { - pathKey: key, - pathValue: value, + pathKey, + pathValue, hasCustomEnv: !!agent.env, customEnvKeys: agent.env ? Object.keys(agent.env) : [] }) diff --git a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts index 9a6f9dff3..7887f9f33 100644 --- a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts @@ -68,7 +68,10 @@ export class AcpProvider extends BaseAgentProvider< ) { super(provider, configPresenter) this.sessionPersistence = sessionPersistence - this.processManager = new AcpProcessManager({ providerId: provider.id }) + this.processManager = new AcpProcessManager({ + providerId: provider.id, + getUseBuiltinRuntime: () => this.configPresenter.getAcpUseBuiltinRuntime() + }) this.sessionManager = new AcpSessionManager({ providerId: provider.id, processManager: this.processManager, diff --git a/src/renderer/settings/components/AcpSettings.vue b/src/renderer/settings/components/AcpSettings.vue index 176c27556..067b6aa92 100644 --- a/src/renderer/settings/components/AcpSettings.vue +++ b/src/renderer/settings/components/AcpSettings.vue @@ -16,6 +16,21 @@ @update:model-value="handleToggle" /> +
+
+
{{ t('settings.acp.useBuiltinRuntimeTitle') }}
+

+ {{ t('settings.acp.useBuiltinRuntimeDescription') }} +

+
+ +
@@ -260,6 +275,8 @@ const llmProviderPresenter = usePresenter('llmproviderPresenter') const acpEnabled = ref(false) const toggling = ref(false) +const useBuiltinRuntime = ref(false) +const togglingUseBuiltinRuntime = ref(false) const loading = ref(false) const builtins = ref([]) const customAgents = ref([]) @@ -355,6 +372,28 @@ const loadAcpEnabled = async () => { } } +const loadAcpUseBuiltinRuntime = async () => { + try { + useBuiltinRuntime.value = await configPresenter.getAcpUseBuiltinRuntime() + } catch (error) { + handleError(error) + } +} + +const handleUseBuiltinRuntimeToggle = async (enabled: boolean) => { + if (togglingUseBuiltinRuntime.value) return + togglingUseBuiltinRuntime.value = true + try { + await configPresenter.setAcpUseBuiltinRuntime(enabled) + useBuiltinRuntime.value = enabled + } catch (error) { + handleError(error) + useBuiltinRuntime.value = !enabled + } finally { + togglingUseBuiltinRuntime.value = false + } +} + const loadAcpData = async () => { if (!acpEnabled.value) { builtins.value = [] @@ -617,6 +656,7 @@ watch( onMounted(async () => { await loadAcpEnabled() + await loadAcpUseBuiltinRuntime() await loadAcpData() }) diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index e19f029d0..a20cf5e02 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -800,6 +800,8 @@ "description": "Manage local Agent Client Protocol agents started by DeepChat.", "enabledTitle": "Enable ACP", "enabledDescription": "When enabled, configured ACP agents will appear as models in the selector.", + "useBuiltinRuntimeTitle": "Use DeepChat Built-in Runtime", + "useBuiltinRuntimeDescription": "When enabled, bypasses system node and uv commands, using DeepChat's bundled versions.", "enableToAccess": "Enable ACP to configure agents.", "addCustomAgent": "Add Custom Agent", "customEmpty": "No custom agents yet.", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 5e2b29d9c..082459b77 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "لطفاً ابتدا ACP را فعال کنید تا به پیکربندی پروکسی دسترسی پیدا کنید", "enabledDescription": "پس از فعال‌سازی، پروکسی ACP پیکربندی شده به عنوان یک مدل در انتخاب‌گر مدل ظاهر خواهد شد.", "enabledTitle": "فعال کردن ACP", + "useBuiltinRuntimeTitle": "استفاده از محیط اجرای داخلی DeepChat", + "useBuiltinRuntimeDescription": "هنگام فعال‌سازی، دستورات node و uv سیستم را دور می‌زند و از نسخه‌های همراه DeepChat استفاده می‌کند.", "env": "متغیر محیطی", "envKeyPlaceholder": "کلید", "envValuePlaceholder": "مقدار", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 669ce4511..ad6c641e8 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "Veuillez d'abord activer l'ACP pour accéder à la configuration du proxy.", "enabledDescription": "Une fois activé, le proxy ACP configuré apparaîtra comme modèle dans le sélecteur de modèles.", "enabledTitle": "Activer ACP", + "useBuiltinRuntimeTitle": "Utiliser l'environnement d'exécution intégré de DeepChat", + "useBuiltinRuntimeDescription": "Lorsqu'il est activé, contourne les commandes node et uv du système, en utilisant les versions fournies avec DeepChat.", "env": "Variable d'environnement", "envKeyPlaceholder": "CLÉ", "envValuePlaceholder": "VALEUR", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 278fab2d8..ded9c7df1 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "まずACPを有効にしてプロキシ設定にアクセスしてください", "enabledDescription": "有効にすると、設定されたACPプロキシがモデルセレクターにモデルとして表示されます。", "enabledTitle": "ACPを有効にする", + "useBuiltinRuntimeTitle": "DeepChat内蔵ランタイムを使用", + "useBuiltinRuntimeDescription": "有効にすると、システムのnodeとuvコマンドをバイパスし、DeepChatにバンドルされたバージョンを使用します。", "env": "環境変数", "envKeyPlaceholder": "キー", "envValuePlaceholder": "価値", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index 7e3841ed4..c4d0ef0ce 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "프록시 구성에 액세스하려면 먼저 ACP를 활성화하세요.", "enabledDescription": "활성화되면 구성된 ACP 에이전트가 모델 선택기에서 모델로 나타납니다.", "enabledTitle": "ACP 활성화", + "useBuiltinRuntimeTitle": "DeepChat 내장 런타임 사용", + "useBuiltinRuntimeDescription": "활성화하면 시스템의 node 및 uv 명령을 우회하여 DeepChat에 번들로 제공되는 버전을 사용합니다.", "env": "환경 변수", "envKeyPlaceholder": "열쇠", "envValuePlaceholder": "값", diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index 4008057cd..1a421f7e5 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "Ative primeiro o ACP para acessar a configuração do proxy", "enabledDescription": "Quando habilitado, o agente ACP configurado aparecerá como modelo no seletor de modelo.", "enabledTitle": "Habilitar ACP", + "useBuiltinRuntimeTitle": "Usar ambiente de execução integrado do DeepChat", + "useBuiltinRuntimeDescription": "Quando habilitado, ignora os comandos node e uv do sistema, usando as versões incluídas com o DeepChat.", "env": "variáveis ​​de ambiente", "envKeyPlaceholder": "CHAVE", "envValuePlaceholder": "VALOR", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 44185e61b..984f7da9f 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -832,6 +832,8 @@ "enableToAccess": "Пожалуйста, сначала включите ACP для доступа к настройкам прокси.", "enabledDescription": "После включения настроенный прокси ACP будет отображаться в селекторе моделей как модель.", "enabledTitle": "Включить ACP", + "useBuiltinRuntimeTitle": "Использовать встроенную среду выполнения DeepChat", + "useBuiltinRuntimeDescription": "При включении обходит системные команды node и uv, используя версии, входящие в состав DeepChat.", "env": "Переменная окружения", "envKeyPlaceholder": "КЛЮЧ", "envValuePlaceholder": "ЗНАЧЕНИЕ", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 985899832..92cf2b642 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -800,6 +800,8 @@ "description": "管理由 DeepChat 启动的本地 ACP Agent。", "enabledTitle": "启用 ACP", "enabledDescription": "启用后,配置好的 ACP Agent 会出现在模型列表中。", + "useBuiltinRuntimeTitle": "使用 DeepChat 内置环境", + "useBuiltinRuntimeDescription": "勾选后会绕过系统的 node 和 uv 命令,使用 DeepChat 自带的版本。", "enableToAccess": "启用 ACP 后才能配置 Agent。", "addCustomAgent": "新增自定义 Agent", "customEmpty": "还没有自定义 Agent。", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 8e7d6b8b3..1a8f94611 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -826,6 +826,8 @@ "description": "管理由 DeepChat 啟動嘅本地 ACP Agent。", "enabledTitle": "啟用 ACP", "enabledDescription": "啟用後,設定好嘅 ACP Agent 會出現在模型清單。", + "useBuiltinRuntimeTitle": "使用 DeepChat 內建環境", + "useBuiltinRuntimeDescription": "勾選後會繞過系統嘅 node 同 uv 命令,使用 DeepChat 自帶嘅版本。", "enableToAccess": "啟用 ACP 後先可以設定 Agent。", "addCustomAgent": "新增自訂 Agent", "customEmpty": "仲未有自訂 Agent。", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 30ae213a8..b50f41f7f 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -826,6 +826,8 @@ "description": "管理由 DeepChat 啟動的本地 ACP Agent。", "enabledTitle": "啟用 ACP", "enabledDescription": "啟用後,設定好的 ACP Agent 會出現在模型列表。", + "useBuiltinRuntimeTitle": "使用 DeepChat 內建環境", + "useBuiltinRuntimeDescription": "勾選後會繞過系統的 node 和 uv 命令,使用 DeepChat 自帶的版本。", "enableToAccess": "啟用 ACP 後才能設定 Agent。", "addCustomAgent": "新增自訂 Agent", "customEmpty": "還沒有自訂 Agent。", diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index a91e5d035..b3ead4801 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -498,6 +498,8 @@ export interface IConfigPresenter { // ACP configuration methods getAcpEnabled(): Promise setAcpEnabled(enabled: boolean): Promise + getAcpUseBuiltinRuntime(): Promise + setAcpUseBuiltinRuntime(enabled: boolean): Promise setAcpAgents(agents: AcpAgentConfig[]): Promise getAcpAgents(): Promise addAcpAgent(agent: Omit & { id?: string }): Promise From 703576a68c3902431451c49b29bb1d629f3f6a42 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 21 Nov 2025 11:01:34 +0800 Subject: [PATCH 5/5] fix: env with api config --- .../agent/acpProcessManager.ts | 66 +++++++++++++++++-- .../providers/acpProvider.ts | 13 +++- src/main/presenter/mcpPresenter/index.ts | 10 +++ .../types/presenters/legacy.presenters.d.ts | 3 + 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts index 1005e7cd3..94150cd0a 100644 --- a/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts +++ b/src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts @@ -24,6 +24,8 @@ export interface AcpProcessHandle extends AgentProcessHandle { interface AcpProcessManagerOptions { providerId: string getUseBuiltinRuntime: () => Promise + getNpmRegistry?: () => Promise + getUvRegistry?: () => Promise } export type SessionNotificationHandler = (notification: schema.SessionNotification) => void @@ -53,6 +55,8 @@ interface PermissionResolverEntry { export class AcpProcessManager implements AgentProcessManager { private readonly providerId: string private readonly getUseBuiltinRuntime: () => Promise + private readonly getNpmRegistry?: () => Promise + private readonly getUvRegistry?: () => Promise private readonly handles = new Map() private readonly pendingHandles = new Map>() private readonly sessionListeners = new Map() @@ -65,6 +69,8 @@ export class AcpProcessManager implements AgentProcessManager { @@ -416,6 +422,29 @@ export class AcpProcessManager implements AgentProcessManager { + return process.env[varName] || match + }) + + // Handle simple $VAR format (without braces) + expandedPath = expandedPath.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => { + return process.env[varName] || match + }) + + return expandedPath + } + private async spawnAgentProcess(agent: AcpAgentConfig): Promise { // Initialize runtime paths if not already done this.setupRuntimes() @@ -428,8 +457,14 @@ export class AcpProcessManager implements AgentProcessManager + typeof arg === 'string' ? this.expandPath(arg) : arg + ) + // Replace command with runtime version if needed - const processedCommand = this.replaceWithRuntimeCommand(agent.command, useBuiltinRuntime) + const processedCommand = this.replaceWithRuntimeCommand(expandedCommand, useBuiltinRuntime) // Validate processed command if (!processedCommand || processedCommand.trim().length === 0) { @@ -451,8 +486,8 @@ export class AcpProcessManager implements AgentProcessManager void @@ -70,7 +71,17 @@ export class AcpProvider extends BaseAgentProvider< this.sessionPersistence = sessionPersistence this.processManager = new AcpProcessManager({ providerId: provider.id, - getUseBuiltinRuntime: () => this.configPresenter.getAcpUseBuiltinRuntime() + getUseBuiltinRuntime: () => this.configPresenter.getAcpUseBuiltinRuntime(), + getNpmRegistry: async () => { + // Get npm registry from MCP presenter's server manager + // This will use the fastest registry from speed test + return presenter.mcpPresenter.getNpmRegistry?.() ?? null + }, + getUvRegistry: async () => { + // Get uv registry from MCP presenter's server manager + // This will use the fastest registry from speed test + return presenter.mcpPresenter.getUvRegistry?.() ?? null + } }) this.sessionManager = new AcpSessionManager({ providerId: provider.id, diff --git a/src/main/presenter/mcpPresenter/index.ts b/src/main/presenter/mcpPresenter/index.ts index d56711713..19f6a0707 100644 --- a/src/main/presenter/mcpPresenter/index.ts +++ b/src/main/presenter/mcpPresenter/index.ts @@ -1301,4 +1301,14 @@ export class McpPresenter implements IMCPPresenter { this.configPresenter.clearNpmRegistryCache?.() console.log('[MCP] NPM Registry cache cleared') } + + // Get npm registry (for ACP and other internal use) + getNpmRegistry(): string | null { + return this.serverManager.getNpmRegistry() + } + + // Get uv registry (for ACP and other internal use) + getUvRegistry(): string | null { + return this.serverManager.getUvRegistry() + } } diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index b3ead4801..4cad317aa 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -1422,6 +1422,9 @@ export interface IMCPPresenter { setCustomNpmRegistry?(registry: string | undefined): Promise setAutoDetectNpmRegistry?(enabled: boolean): Promise clearNpmRegistryCache?(): Promise + // Get npm/uv registry for internal use (ACP, etc.) + getNpmRegistry?(): string | null + getUvRegistry?(): string | null // McpRouter marketplace listMcpRouterServers?(