From f569b5933c270eae7d46590dc0a51c974b32f2ee Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 17:44:26 +0800 Subject: [PATCH 1/9] fix: add bypass to powershell --- src/main/presenter/configPresenter/acpInitHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/presenter/configPresenter/acpInitHelper.ts b/src/main/presenter/configPresenter/acpInitHelper.ts index 92005b095..a0a0b7a8c 100644 --- a/src/main/presenter/configPresenter/acpInitHelper.ts +++ b/src/main/presenter/configPresenter/acpInitHelper.ts @@ -177,7 +177,7 @@ class AcpInitHelper { if (platform === 'win32') { shell = 'powershell.exe' - shellArgs = ['-NoLogo'] + shellArgs = ['-NoLogo', '-ExecutionPolicy', 'Bypass'] } else { // Use user's default shell or bash/zsh shell = process.env.SHELL || '/bin/bash' From 4c48f25feb43c695b5e7cbc67fa2f30951eb00e7 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 18:43:01 +0800 Subject: [PATCH 2/9] feat: add deps check --- .../configPresenter/acpInitHelper.ts | 148 ++++++++++++++++++ src/preload/index.d.ts | 1 + src/preload/index.ts | 3 + .../settings/components/AcpTerminalDialog.vue | 134 +++++++++++++++- src/renderer/src/i18n/en-US/settings.json | 3 + src/renderer/src/i18n/zh-CN/settings.json | 3 + src/renderer/src/i18n/zh-HK/settings.json | 3 + src/renderer/src/i18n/zh-TW/settings.json | 3 + 8 files changed, 290 insertions(+), 8 deletions(-) diff --git a/src/main/presenter/configPresenter/acpInitHelper.ts b/src/main/presenter/configPresenter/acpInitHelper.ts index a0a0b7a8c..6412896dc 100644 --- a/src/main/presenter/configPresenter/acpInitHelper.ts +++ b/src/main/presenter/configPresenter/acpInitHelper.ts @@ -1,15 +1,55 @@ import * as path from 'path' +import * as fs from 'fs' +import { exec } from 'child_process' +import { promisify } from 'util' import { type WebContents } from 'electron' import type { AcpBuiltinAgentId, AcpAgentConfig, AcpAgentProfile } from '@shared/presenter' import { spawn } from '@homebridge/node-pty-prebuilt-multiarch' import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch' import { RuntimeHelper } from '@/lib/runtimeHelper' +const execAsync = promisify(exec) + interface InitCommandConfig { commands: string[] description: string } +interface ExternalDependency { + name: string + description: string + platform?: string[] + checkCommand?: string + checkPaths?: string[] + installCommands?: { + winget?: string + chocolatey?: string + scoop?: string + } + downloadUrl?: string + requiredFor?: string[] +} + +const EXTERNAL_DEPENDENCIES: ExternalDependency[] = [ + { + name: 'Git Bash', + description: 'Git for Windows includes Git Bash', + platform: ['win32'], + checkCommand: 'git --version', + checkPaths: [ + 'C:\\Program Files\\Git\\bin\\bash.exe', + 'C:\\Program Files (x86)\\Git\\bin\\bash.exe' + ], + installCommands: { + winget: 'winget install Git.Git', + chocolatey: 'choco install git', + scoop: 'scoop install git' + }, + downloadUrl: 'https://git-scm.com/download/win', + requiredFor: ['claude-code-acp'] + } +] + const BUILTIN_INIT_COMMANDS: Record = { 'kimi-cli': { commands: ['uv tool run --from kimi-cli kimi'], @@ -37,6 +77,100 @@ class AcpInitHelper { this.runtimeHelper.initializeRuntimes() } + /** + * Check if an external dependency is available + */ + private async checkExternalDependency(dep: ExternalDependency): Promise { + const platform = process.platform + + // Check if dependency supports current platform + if (dep.platform && !dep.platform.includes(platform)) { + console.log(`[ACP Init] Dependency ${dep.name} not required on platform ${platform}`) + return true // Not required on this platform, consider it available + } + + // Method 1: Check via command + if (dep.checkCommand) { + try { + const { stdout } = await execAsync(dep.checkCommand, { timeout: 5000 }) + if (stdout && stdout.trim().length > 0) { + console.log(`[ACP Init] Dependency ${dep.name} found via command: ${dep.checkCommand}`) + return true + } + } catch { + console.log(`[ACP Init] Dependency ${dep.name} not found via command: ${dep.checkCommand}`) + } + } + + // Method 2: Check via paths + if (dep.checkPaths && dep.checkPaths.length > 0) { + for (const checkPath of dep.checkPaths) { + try { + if (fs.existsSync(checkPath)) { + console.log(`[ACP Init] Dependency ${dep.name} found at path: ${checkPath}`) + return true + } + } catch { + // Continue checking other paths + } + } + } + + // Method 3: Use system tools to find command + if (dep.checkCommand) { + try { + const commandName = dep.checkCommand.split(' ')[0] + let findCommand: string + + if (platform === 'win32') { + findCommand = `where.exe ${commandName}` + } else { + findCommand = `which ${commandName}` + } + + const { stdout } = await execAsync(findCommand, { timeout: 5000 }) + if (stdout && stdout.trim().length > 0) { + console.log(`[ACP Init] Dependency ${dep.name} found via system tool: ${findCommand}`) + return true + } + } catch { + // Command not found + } + } + + console.log(`[ACP Init] Dependency ${dep.name} not found`) + return false + } + + /** + * Check required dependencies for an agent + */ + private async checkRequiredDependencies(agentId: string): Promise { + const platform = process.platform + const missingDeps: ExternalDependency[] = [] + + // Find dependencies required for this agent + const requiredDeps = EXTERNAL_DEPENDENCIES.filter( + (dep) => dep.requiredFor && dep.requiredFor.includes(agentId) + ) + + console.log(`[ACP Init] Checking dependencies for agent ${agentId}:`, { + totalDeps: requiredDeps.length, + platform + }) + + // Check each dependency + for (const dep of requiredDeps) { + const isAvailable = await this.checkExternalDependency(dep) + if (!isAvailable) { + missingDeps.push(dep) + console.log(`[ACP Init] Missing dependency: ${dep.name}`) + } + } + + return missingDeps + } + /** * Initialize a builtin ACP agent with terminal output streaming */ @@ -57,6 +191,20 @@ class AcpInitHelper { profileName: profile.name }) + // Check external dependencies before initialization + const missingDeps = await this.checkRequiredDependencies(agentId) + if (missingDeps.length > 0 && webContents && !webContents.isDestroyed()) { + console.log('[ACP Init] Missing dependencies detected, sending notification:', { + agentId, + missingCount: missingDeps.length + }) + webContents.send('external-deps-required', { + agentId, + missingDeps + }) + // Continue with initialization anyway - user can install dependencies manually + } + const initConfig = BUILTIN_INIT_COMMANDS[agentId] if (!initConfig) { console.error('[ACP Init] Unknown builtin agent:', agentId) diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 5fb4d773a..195757e54 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -6,6 +6,7 @@ declare global { api: { copyText(text: string): void copyImage(image: string): void + readClipboardText(): string getPathForFile(file: File): string getWindowId(): number | null getWebContentsId(): number diff --git a/src/preload/index.ts b/src/preload/index.ts index 92e072b86..f3d34a69b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -22,6 +22,9 @@ const api = { const img = nativeImage.createFromDataURL(image) clipboard.writeImage(img) }, + readClipboardText: () => { + return clipboard.readText() + }, getPathForFile: (file: File) => { return webUtils.getPathForFile(file) }, diff --git a/src/renderer/settings/components/AcpTerminalDialog.vue b/src/renderer/settings/components/AcpTerminalDialog.vue index a8e87e27d..b25ecdad8 100644 --- a/src/renderer/settings/components/AcpTerminalDialog.vue +++ b/src/renderer/settings/components/AcpTerminalDialog.vue @@ -23,14 +23,26 @@ {{ statusText }} - +
+ + +
@@ -54,6 +66,8 @@ import { } from '@shadcn/components/ui/dialog' import { Button } from '@shadcn/components/ui/button' import { X } from 'lucide-vue-next' +import { Icon } from '@iconify/vue' +import { useToast } from '@/components/use-toast' const props = defineProps<{ open: boolean @@ -65,6 +79,7 @@ const emit = defineEmits<{ }>() const { t } = useI18n() +const { toast } = useToast() const terminalContainer = ref(null) let terminal: Terminal | null = null @@ -72,6 +87,21 @@ let terminal: Terminal | null = null const isRunning = ref(false) const status = ref<'idle' | 'running' | 'completed' | 'error'>('idle') +interface ExternalDependency { + name: string + description: string + platform?: string[] + checkCommand?: string + checkPaths?: string[] + installCommands?: { + winget?: string + chocolatey?: string + scoop?: string + } + downloadUrl?: string + requiredFor?: string[] +} + const statusColor = computed(() => { switch (status.value) { case 'running': @@ -248,6 +278,92 @@ const handleError = (_event: unknown, data: { message: string }) => { } } +const handlePaste = async () => { + try { + if (!window.api || typeof window.api.readClipboardText !== 'function') { + console.warn('[AcpTerminal] readClipboardText API not available') + return + } + + const text = window.api.readClipboardText() + if (text && window.electron) { + window.electron.ipcRenderer.send('acp-terminal:input', text) + console.log('[AcpTerminal] Pasted text to terminal:', text.length, 'characters') + } + } catch (error) { + console.error('[AcpTerminal] Failed to paste from clipboard:', error) + toast({ + title: t('settings.acp.terminal.pasteError'), + description: error instanceof Error ? error.message : String(error), + variant: 'destructive' + }) + } +} + +const handleExternalDepsRequired = ( + _event: unknown, + data: { agentId: string; missingDeps: ExternalDependency[] } +) => { + console.log('[AcpTerminal] External dependencies required:', data) + + if (!data.missingDeps || data.missingDeps.length === 0) { + return + } + + // Build dependency information message + const depMessages = data.missingDeps.map((dep) => { + let message = `${dep.name}: ${dep.description}\n` + + // Add installation commands + if (dep.installCommands) { + const commands: string[] = [] + if (dep.installCommands.winget) { + commands.push(`winget: ${dep.installCommands.winget}`) + } + if (dep.installCommands.chocolatey) { + commands.push(`chocolatey: ${dep.installCommands.chocolatey}`) + } + if (dep.installCommands.scoop) { + commands.push(`scoop: ${dep.installCommands.scoop}`) + } + if (commands.length > 0) { + message += `\nInstall commands:\n${commands.join('\n')}` + } + } + + // Add download URL + if (dep.downloadUrl) { + message += `\nDownload: ${dep.downloadUrl}` + } + + return message + }) + + const fullMessage = `Missing external dependencies:\n\n${depMessages.join('\n\n')}` + + // Show toast notification with dependency information + toast({ + title: t('settings.acp.terminal.missingDependencies'), + description: fullMessage, + duration: 10000, // Show for 10 seconds + variant: 'default' + }) + + // Also write to terminal + if (terminal) { + terminal.writeln(`\r\n\x1b[33m⚠ Missing external dependencies detected:\x1b[0m`) + data.missingDeps.forEach((dep) => { + terminal?.writeln(`\r\n\x1b[33m${dep.name}\x1b[0m: ${dep.description}`) + if (dep.installCommands?.winget) { + terminal?.writeln(` Install: ${dep.installCommands.winget}`) + } + if (dep.downloadUrl) { + terminal?.writeln(` Download: ${dep.downloadUrl}`) + } + }) + } +} + const setupIpcListeners = () => { if (typeof window === 'undefined' || !window.electron) { console.warn('[AcpTerminal] Cannot setup IPC listeners - window.electron not available') @@ -259,6 +375,7 @@ const setupIpcListeners = () => { window.electron.ipcRenderer.on('acp-init:output', handleOutput) window.electron.ipcRenderer.on('acp-init:exit', handleExit) window.electron.ipcRenderer.on('acp-init:error', handleError) + window.electron.ipcRenderer.on('external-deps-required', handleExternalDepsRequired) console.log('[AcpTerminal] IPC listeners set up successfully') } @@ -271,6 +388,7 @@ const removeIpcListeners = () => { window.electron.ipcRenderer.removeAllListeners('acp-init:output') window.electron.ipcRenderer.removeAllListeners('acp-init:exit') window.electron.ipcRenderer.removeAllListeners('acp-init:error') + window.electron.ipcRenderer.removeAllListeners('external-deps-required') } watch( diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index a05ddbf18..2b46204e2 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -873,6 +873,9 @@ "exitSuccess": "Process completed successfully (exit code: {code})", "exitError": "Process exited with error (exit code: {code})", "processError": "Process error", + "paste": "Paste", + "pasteError": "Failed to paste from clipboard", + "missingDependencies": "Missing External Dependencies", "status": { "idle": "Idle", "running": "Running", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 22d38a996..9010cb9dd 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -873,6 +873,9 @@ "exitSuccess": "进程成功完成(退出码:{code})", "exitError": "进程退出时出错(退出码:{code})", "processError": "进程错误", + "paste": "粘贴", + "pasteError": "从剪贴板粘贴失败", + "missingDependencies": "缺少外部依赖", "status": { "idle": "空闲", "running": "运行中", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 5cd5ef2fd..caefc6ef7 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -897,6 +897,9 @@ "exitSuccess": "進程成功完成(退出碼:{code})", "processError": "進程錯誤", "starting": "正在啟動初始化...", + "paste": "貼上", + "pasteError": "從剪貼簿貼上失敗", + "missingDependencies": "缺少外部依賴", "status": { "completed": "已完成", "error": "錯誤", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index cbeb00984..67ae49411 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -897,6 +897,9 @@ "exitSuccess": "進程成功完成(退出碼:{code})", "processError": "進程錯誤", "starting": "正在啟動初始化...", + "paste": "貼上", + "pasteError": "從剪貼簿貼上失敗", + "missingDependencies": "缺少外部依賴", "status": { "completed": "已完成", "error": "錯誤", From 023103f71dec854994af9b0bb97511a81487e712 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 19:10:46 +0800 Subject: [PATCH 3/9] fix: add missing i18n --- src/renderer/src/i18n/fa-IR/settings.json | 5 ++++- src/renderer/src/i18n/fr-FR/settings.json | 5 ++++- src/renderer/src/i18n/ja-JP/settings.json | 5 ++++- src/renderer/src/i18n/ko-KR/settings.json | 5 ++++- src/renderer/src/i18n/pt-BR/settings.json | 5 ++++- src/renderer/src/i18n/ru-RU/settings.json | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 0f224cbb8..cea2ee466 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -904,7 +904,10 @@ "running": "در حال دویدن" }, "title": "ترمینال را راه اندازی کنید", - "waiting": "در انتظار شروع اولیه سازی..." + "waiting": "در انتظار شروع اولیه سازی...", + "missingDependencies": "عدم وابستگی خارجی", + "paste": "چسباندن", + "pasteError": "جای‌گذاری از کلیپ بورد انجام نشد" } } } diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 2e8035ec8..ad3a36f47 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -904,7 +904,10 @@ "running": "En cours d'exécution" }, "title": "Initialiser le terminal", - "waiting": "En attente du début de l'initialisation..." + "waiting": "En attente du début de l'initialisation...", + "missingDependencies": "Dépendances externes manquantes", + "paste": "Coller", + "pasteError": "Le collage depuis le presse-papiers a échoué" } } } diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 10dc90f20..74c08d717 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -904,7 +904,10 @@ "running": "ランニング" }, "title": "端末の初期化", - "waiting": "初期化が始まるのを待っています..." + "waiting": "初期化が始まるのを待っています...", + "missingDependencies": "外部依存関係が欠落している", + "paste": "ペースト", + "pasteError": "クリップボードからの貼り付けに失敗しました" } } } diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index a7f177f7d..eea2316f3 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -904,7 +904,10 @@ "running": "달리기" }, "title": "터미널 초기화", - "waiting": "초기화가 시작되기를 기다리는 중..." + "waiting": "초기화가 시작되기를 기다리는 중...", + "missingDependencies": "외부 종속성 누락", + "paste": "반죽", + "pasteError": "클립보드에서 붙여넣기 실패" } } } diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index 93bba7b9e..4c65e252c 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -904,7 +904,10 @@ "running": "Correndo" }, "title": "Inicializar terminal", - "waiting": "Aguardando o início da inicialização..." + "waiting": "Aguardando o início da inicialização...", + "missingDependencies": "Dependências externas ausentes", + "paste": "Colar", + "pasteError": "Falha ao colar da área de transferência" } } } diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 2bafa767c..75ff63af6 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -904,7 +904,10 @@ "running": "Бег" }, "title": "Инициализировать терминал", - "waiting": "Ожидание начала инициализации..." + "waiting": "Ожидание начала инициализации...", + "missingDependencies": "Отсутствуют внешние зависимости", + "paste": "Вставить", + "pasteError": "Не удалось вставить из буфера обмена." } } } From 7801ce75fc052614cbbe1318d624c1665f6cfdcc Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 19:48:00 +0800 Subject: [PATCH 4/9] feat: add dialog for windows deps --- .../configPresenter/acpInitHelper.ts | 17 +- src/main/presenter/configPresenter/index.ts | 7 +- .../components/AcpDependencyDialog.vue | 161 ++++++++++++++++++ .../settings/components/AcpSettings.vue | 32 ++++ .../settings/components/AcpTerminalDialog.vue | 62 ++----- src/renderer/src/i18n/en-US/settings.json | 10 +- src/renderer/src/i18n/fa-IR/settings.json | 10 +- src/renderer/src/i18n/fr-FR/settings.json | 10 +- src/renderer/src/i18n/ja-JP/settings.json | 10 +- src/renderer/src/i18n/ko-KR/settings.json | 10 +- src/renderer/src/i18n/pt-BR/settings.json | 10 +- src/renderer/src/i18n/ru-RU/settings.json | 10 +- src/renderer/src/i18n/zh-CN/settings.json | 10 +- src/renderer/src/i18n/zh-HK/settings.json | 10 +- src/renderer/src/i18n/zh-TW/settings.json | 10 +- 15 files changed, 310 insertions(+), 69 deletions(-) create mode 100644 src/renderer/settings/components/AcpDependencyDialog.vue diff --git a/src/main/presenter/configPresenter/acpInitHelper.ts b/src/main/presenter/configPresenter/acpInitHelper.ts index 6412896dc..5c05ba14c 100644 --- a/src/main/presenter/configPresenter/acpInitHelper.ts +++ b/src/main/presenter/configPresenter/acpInitHelper.ts @@ -193,16 +193,19 @@ class AcpInitHelper { // Check external dependencies before initialization const missingDeps = await this.checkRequiredDependencies(agentId) - if (missingDeps.length > 0 && webContents && !webContents.isDestroyed()) { - console.log('[ACP Init] Missing dependencies detected, sending notification:', { + if (missingDeps.length > 0) { + console.log('[ACP Init] Missing dependencies detected, blocking initialization:', { agentId, missingCount: missingDeps.length }) - webContents.send('external-deps-required', { - agentId, - missingDeps - }) - // Continue with initialization anyway - user can install dependencies manually + if (webContents && !webContents.isDestroyed()) { + webContents.send('external-deps-required', { + agentId, + missingDeps + }) + } + // Stop initialization - user must install dependencies first + return null } const initConfig = BUILTIN_INIT_COMMANDS[agentId] diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index 69871cb47..a5345866b 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -1057,7 +1057,7 @@ export class ConfigPresenter implements IConfigPresenter { throw new Error(`No active profile found for agent: ${agentId}`) } - await initializeBuiltinAgent( + const result = await initializeBuiltinAgent( agentId as AcpBuiltinAgentId, activeProfile, useBuiltinRuntime, @@ -1065,6 +1065,11 @@ export class ConfigPresenter implements IConfigPresenter { uvRegistry, webContents ) + // If initialization returns null, it means dependencies are missing + // The event has already been sent to frontend, just return without error + if (result === null) { + return + } } else { // Get custom agent const customs = await this.getAcpCustomAgents() diff --git a/src/renderer/settings/components/AcpDependencyDialog.vue b/src/renderer/settings/components/AcpDependencyDialog.vue new file mode 100644 index 000000000..486cf52c6 --- /dev/null +++ b/src/renderer/settings/components/AcpDependencyDialog.vue @@ -0,0 +1,161 @@ + + + diff --git a/src/renderer/settings/components/AcpSettings.vue b/src/renderer/settings/components/AcpSettings.vue index 2c3527333..adf530f39 100644 --- a/src/renderer/settings/components/AcpSettings.vue +++ b/src/renderer/settings/components/AcpSettings.vue @@ -260,6 +260,13 @@ + +
@@ -296,6 +303,7 @@ import { import AcpProfileDialog from './AcpProfileDialog.vue' import AcpProfileManagerDialog from './AcpProfileManagerDialog.vue' import AcpTerminalDialog from './AcpTerminalDialog.vue' +import AcpDependencyDialog from './AcpDependencyDialog.vue' const { t } = useI18n() const { toast } = useToast() @@ -316,6 +324,23 @@ const builtinPending = reactive>({}) const customPending = reactive>({}) const initializing = reactive>({}) const terminalDialogOpen = ref(false) +const dependencyDialogOpen = ref(false) +const missingDependencies = ref< + Array<{ + name: string + description: string + platform?: string[] + checkCommand?: string + checkPaths?: string[] + installCommands?: { + winget?: string + chocolatey?: string + scoop?: string + } + downloadUrl?: string + requiredFor?: string[] + }> +>([]) const profileDialogState = reactive({ open: false, @@ -679,6 +704,13 @@ const setInitializing = (agentId: string, isBuiltin: boolean, state: boolean) => } } +const handleDependenciesRequired = (dependencies: typeof missingDependencies.value) => { + console.log('[AcpSettings] Dependencies required:', dependencies) + missingDependencies.value = dependencies + dependencyDialogOpen.value = true + terminalDialogOpen.value = false +} + const handleInitializeAgent = async (agentId: string, isBuiltin: boolean) => { if (isInitializing(agentId, isBuiltin)) return diff --git a/src/renderer/settings/components/AcpTerminalDialog.vue b/src/renderer/settings/components/AcpTerminalDialog.vue index b25ecdad8..eddc027e8 100644 --- a/src/renderer/settings/components/AcpTerminalDialog.vue +++ b/src/renderer/settings/components/AcpTerminalDialog.vue @@ -76,6 +76,7 @@ const props = defineProps<{ const emit = defineEmits<{ (e: 'update:open', value: boolean): void (e: 'close'): void + (e: 'dependencies-required', dependencies: ExternalDependency[]): void }>() const { t } = useI18n() @@ -310,58 +311,12 @@ const handleExternalDepsRequired = ( return } - // Build dependency information message - const depMessages = data.missingDeps.map((dep) => { - let message = `${dep.name}: ${dep.description}\n` + // Emit event to parent to show dependency dialog + emit('dependencies-required', data.missingDeps) - // Add installation commands - if (dep.installCommands) { - const commands: string[] = [] - if (dep.installCommands.winget) { - commands.push(`winget: ${dep.installCommands.winget}`) - } - if (dep.installCommands.chocolatey) { - commands.push(`chocolatey: ${dep.installCommands.chocolatey}`) - } - if (dep.installCommands.scoop) { - commands.push(`scoop: ${dep.installCommands.scoop}`) - } - if (commands.length > 0) { - message += `\nInstall commands:\n${commands.join('\n')}` - } - } - - // Add download URL - if (dep.downloadUrl) { - message += `\nDownload: ${dep.downloadUrl}` - } - - return message - }) - - const fullMessage = `Missing external dependencies:\n\n${depMessages.join('\n\n')}` - - // Show toast notification with dependency information - toast({ - title: t('settings.acp.terminal.missingDependencies'), - description: fullMessage, - duration: 10000, // Show for 10 seconds - variant: 'default' - }) - - // Also write to terminal - if (terminal) { - terminal.writeln(`\r\n\x1b[33m⚠ Missing external dependencies detected:\x1b[0m`) - data.missingDeps.forEach((dep) => { - terminal?.writeln(`\r\n\x1b[33m${dep.name}\x1b[0m: ${dep.description}`) - if (dep.installCommands?.winget) { - terminal?.writeln(` Install: ${dep.installCommands.winget}`) - } - if (dep.downloadUrl) { - terminal?.writeln(` Download: ${dep.downloadUrl}`) - } - }) - } + // Close terminal dialog since initialization is blocked + emit('update:open', false) + emit('close') } const setupIpcListeners = () => { @@ -451,6 +406,11 @@ onBeforeUnmount(() => { width: 100%; } +/* Hide default DialogContent close button */ +:deep([data-slot='dialog-close']) { + display: none !important; +} + /* Xterm.js styles */ :deep(.xterm) { height: 100% !important; diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 2b46204e2..f43553451 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -875,13 +875,21 @@ "processError": "Process error", "paste": "Paste", "pasteError": "Failed to paste from clipboard", - "missingDependencies": "Missing External Dependencies", "status": { "idle": "Idle", "running": "Running", "completed": "Completed", "error": "Error" } + }, + "dependency": { + "title": "Missing External Dependencies", + "description": "The following dependencies need to be installed before initialization.", + "installCommands": "Installation Commands", + "downloadUrl": "Download Link", + "copy": "Copy", + "copied": "Copied to clipboard", + "copyFailed": "Failed to copy" } }, "rateLimit": { diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index cea2ee466..473107534 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -905,9 +905,17 @@ }, "title": "ترمینال را راه اندازی کنید", "waiting": "در انتظار شروع اولیه سازی...", - "missingDependencies": "عدم وابستگی خارجی", "paste": "چسباندن", "pasteError": "جای‌گذاری از کلیپ بورد انجام نشد" + }, + "dependency": { + "copied": "در کلیپ بورد کپی شد", + "copy": "کپی کنید", + "copyFailed": "کپی ناموفق بود", + "description": "وابستگی های زیر باید قبل از مقداردهی اولیه نصب شوند.", + "downloadUrl": "لینک دانلود", + "installCommands": "دستور نصب", + "title": "عدم وابستگی خارجی" } } } diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index ad3a36f47..23cfde7d6 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -905,9 +905,17 @@ }, "title": "Initialiser le terminal", "waiting": "En attente du début de l'initialisation...", - "missingDependencies": "Dépendances externes manquantes", "paste": "Coller", "pasteError": "Le collage depuis le presse-papiers a échoué" + }, + "dependency": { + "copied": "Copié dans le presse-papiers", + "copy": "copie", + "copyFailed": "Échec de la copie", + "description": "Les dépendances suivantes doivent être installées avant l'initialisation.", + "downloadUrl": "Lien de téléchargement", + "installCommands": "Commande d'installation", + "title": "Dépendances externes manquantes" } } } diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 74c08d717..e6a7ca7bd 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -905,9 +905,17 @@ }, "title": "端末の初期化", "waiting": "初期化が始まるのを待っています...", - "missingDependencies": "外部依存関係が欠落している", "paste": "ペースト", "pasteError": "クリップボードからの貼り付けに失敗しました" + }, + "dependency": { + "copied": "クリップボードにコピーされました", + "copy": "コピー", + "copyFailed": "コピーに失敗しました", + "description": "初期化の前に、次の依存関係をインストールする必要があります。", + "downloadUrl": "ダウンロードリンク", + "installCommands": "インストールコマンド", + "title": "外部依存関係が欠落している" } } } diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index eea2316f3..93246c969 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -905,9 +905,17 @@ }, "title": "터미널 초기화", "waiting": "초기화가 시작되기를 기다리는 중...", - "missingDependencies": "외부 종속성 누락", "paste": "반죽", "pasteError": "클립보드에서 붙여넣기 실패" + }, + "dependency": { + "copied": "클립보드에 복사됨", + "copy": "복사", + "copyFailed": "복사 실패", + "description": "초기화하기 전에 다음 종속성을 설치해야 합니다.", + "downloadUrl": "다운로드 링크", + "installCommands": "설치 명령", + "title": "외부 종속성 누락" } } } diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index 4c65e252c..0fffce452 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -905,9 +905,17 @@ }, "title": "Inicializar terminal", "waiting": "Aguardando o início da inicialização...", - "missingDependencies": "Dependências externas ausentes", "paste": "Colar", "pasteError": "Falha ao colar da área de transferência" + }, + "dependency": { + "copied": "Copiado para a área de transferência", + "copy": "cópia", + "copyFailed": "Falha na cópia", + "description": "As seguintes dependências precisam ser instaladas antes da inicialização.", + "downloadUrl": "Baixar link", + "installCommands": "Comando de instalação", + "title": "Dependências externas ausentes" } } } diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 75ff63af6..999b270f0 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -905,9 +905,17 @@ }, "title": "Инициализировать терминал", "waiting": "Ожидание начала инициализации...", - "missingDependencies": "Отсутствуют внешние зависимости", "paste": "Вставить", "pasteError": "Не удалось вставить из буфера обмена." + }, + "dependency": { + "copied": "Скопировано в буфер обмена", + "copy": "копировать", + "copyFailed": "Не удалось скопировать", + "description": "Перед инициализацией необходимо установить следующие зависимости.", + "downloadUrl": "Ссылка для скачивания", + "installCommands": "Команда установки", + "title": "Отсутствуют внешние зависимости" } } } diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 9010cb9dd..d54a78081 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -875,13 +875,21 @@ "processError": "进程错误", "paste": "粘贴", "pasteError": "从剪贴板粘贴失败", - "missingDependencies": "缺少外部依赖", "status": { "idle": "空闲", "running": "运行中", "completed": "已完成", "error": "错误" } + }, + "dependency": { + "title": "缺少外部依赖", + "description": "初始化前需要安装以下依赖项。", + "installCommands": "安装命令", + "downloadUrl": "下载链接", + "copy": "复制", + "copied": "已复制到剪贴板", + "copyFailed": "复制失败" } }, "rateLimit": { diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index caefc6ef7..e8a950fa3 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -899,7 +899,6 @@ "starting": "正在啟動初始化...", "paste": "貼上", "pasteError": "從剪貼簿貼上失敗", - "missingDependencies": "缺少外部依賴", "status": { "completed": "已完成", "error": "錯誤", @@ -908,6 +907,15 @@ }, "title": "初始化終端", "waiting": "等待初始化開始..." + }, + "dependency": { + "copied": "已復製到剪貼板", + "copy": "複製", + "copyFailed": "複製失敗", + "description": "初始化前需要安裝以下依賴項。", + "downloadUrl": "下載鏈接", + "installCommands": "安裝命令", + "title": "缺少外部依賴" } } } diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 67ae49411..e3879f957 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -889,6 +889,15 @@ "initializeFailed": "初始化失敗", "initializeSuccess": "初始化已啟動", "initializing": "正在初始化...", + "dependency": { + "title": "缺少外部依賴", + "description": "初始化前需要安裝以下依賴項。", + "installCommands": "安裝命令", + "downloadUrl": "下載連結", + "copy": "複製", + "copied": "已複製到剪貼板", + "copyFailed": "複製失敗" + }, "terminal": { "close": "關閉", "closing": "正在關閉...", @@ -899,7 +908,6 @@ "starting": "正在啟動初始化...", "paste": "貼上", "pasteError": "從剪貼簿貼上失敗", - "missingDependencies": "缺少外部依賴", "status": { "completed": "已完成", "error": "錯誤", From 8b9d6ee9a1ad85995c8bd17506114ad15c3122c0 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 20:42:00 +0800 Subject: [PATCH 5/9] feat: add support for windows system permission --- src/main/lib/runtimeHelper.ts | 38 +++++++++++++++++++ .../configPresenter/acpInitHelper.ts | 30 +++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/main/lib/runtimeHelper.ts b/src/main/lib/runtimeHelper.ts index 8ab087bfb..6ac255731 100644 --- a/src/main/lib/runtimeHelper.ts +++ b/src/main/lib/runtimeHelper.ts @@ -319,4 +319,42 @@ export class RuntimeHelper { return [`${homeDir}\\.cargo\\bin`, `${homeDir}\\.local\\bin`] } } + + /** + * Check if the application is installed in a Windows system directory + * System directories include Program Files and Program Files (x86) + * @returns true if installed in system directory, false otherwise + */ + public isInstalledInSystemDirectory(): boolean { + if (process.platform !== 'win32') { + return false + } + + const appPath = app.getAppPath() + const normalizedPath = appPath.toLowerCase() + + // Check if app is installed in Program Files or Program Files (x86) + const isSystemDir = + normalizedPath.includes('program files') || normalizedPath.includes('program files (x86)') + + if (isSystemDir) { + console.log('[RuntimeHelper] Application is installed in system directory:', appPath) + } + + return isSystemDir + } + + /** + * Get user npm prefix path for Windows + * Returns the path where npm should install global packages when app is in system directory + * @returns User npm prefix path or null if not applicable + */ + public getUserNpmPrefix(): string | null { + if (process.platform !== 'win32') { + return null + } + + const appDataPath = app.getPath('appData') + return path.join(appDataPath, 'npm') + } } diff --git a/src/main/presenter/configPresenter/acpInitHelper.ts b/src/main/presenter/configPresenter/acpInitHelper.ts index 5c05ba14c..18d9b5486 100644 --- a/src/main/presenter/configPresenter/acpInitHelper.ts +++ b/src/main/presenter/configPresenter/acpInitHelper.ts @@ -545,6 +545,36 @@ class AcpInitHelper { env.PIP_INDEX_URL = uvRegistry console.log('[ACP Init] Set UV registry:', uvRegistry) } + + // On Windows, if app is installed in system directory, set npm prefix to user directory + // to avoid permission issues when installing global packages + if (process.platform === 'win32' && this.runtimeHelper.isInstalledInSystemDirectory()) { + const userNpmPrefix = this.runtimeHelper.getUserNpmPrefix() + + if (userNpmPrefix) { + env.npm_config_prefix = userNpmPrefix + env.NPM_CONFIG_PREFIX = userNpmPrefix + console.log( + '[ACP Init] Set NPM prefix to user directory (system install detected):', + userNpmPrefix + ) + + // Add user npm bin directory to PATH + const pathKey = 'Path' + const separator = ';' + const existingPath = env[pathKey] || '' + const userNpmBinPath = userNpmPrefix + + // Ensure the user npm bin path is at the beginning of PATH + if (existingPath) { + env[pathKey] = [userNpmBinPath, existingPath].filter(Boolean).join(separator) + } else { + env[pathKey] = userNpmBinPath + } + + console.log('[ACP Init] Added user npm bin directory to PATH:', userNpmBinPath) + } + } } // Add custom environment variables from profile From 817005d0ceba76251954efa5a019df89ada5b879 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 21:04:44 +0800 Subject: [PATCH 6/9] chore: disable killProcess in windows nsis install script --- electron-builder.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-builder.yml b/electron-builder.yml index ca9cc6c4d..e57256ee6 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -60,6 +60,7 @@ nsis: createDesktopShortcut: always allowToChangeInstallationDirectory: true oneClick: false + killProcess: false include: build/nsis-installer.nsh afterSign: scripts/notarize.js afterPack: scripts/afterPack.js From c6229f6fe0b82dc426e6a77a5a8ccd53bfdc5ef2 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 22 Nov 2025 21:22:55 +0800 Subject: [PATCH 7/9] fix: disable process check from nsis --- build/nsis-installer.nsh | 2 +- electron-builder.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build/nsis-installer.nsh b/build/nsis-installer.nsh index ba2dd4c9e..190660dc1 100644 --- a/build/nsis-installer.nsh +++ b/build/nsis-installer.nsh @@ -130,7 +130,7 @@ inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable ($2)..." "$5" "$6" ExecWait "$6 /install /norestart" ; vc_redist exit code is unreliable, so we re-check registry - + Push $2 ; Pass arch to checkVCRedist again Call checkVCRedist Pop $2 diff --git a/electron-builder.yml b/electron-builder.yml index e57256ee6..ca9cc6c4d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -60,7 +60,6 @@ nsis: createDesktopShortcut: always allowToChangeInstallationDirectory: true oneClick: false - killProcess: false include: build/nsis-installer.nsh afterSign: scripts/notarize.js afterPack: scripts/afterPack.js From d8bc51c486d619253246a5181b5fb815b87847af Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 23 Nov 2025 14:41:49 +0800 Subject: [PATCH 8/9] feat: add better title from init dialog --- .../configPresenter/acpInitHelper.ts | 2 +- .../settings/components/AcpTerminalDialog.vue | 107 ++++++------------ src/renderer/src/i18n/en-US/settings.json | 1 - src/renderer/src/i18n/fa-IR/settings.json | 1 - src/renderer/src/i18n/fr-FR/settings.json | 3 +- src/renderer/src/i18n/ja-JP/settings.json | 1 - src/renderer/src/i18n/ko-KR/settings.json | 1 - src/renderer/src/i18n/pt-BR/settings.json | 1 - src/renderer/src/i18n/ru-RU/settings.json | 1 - src/renderer/src/i18n/zh-CN/settings.json | 1 - src/renderer/src/i18n/zh-HK/settings.json | 1 - src/renderer/src/i18n/zh-TW/settings.json | 1 - 12 files changed, 36 insertions(+), 85 deletions(-) diff --git a/src/main/presenter/configPresenter/acpInitHelper.ts b/src/main/presenter/configPresenter/acpInitHelper.ts index 18d9b5486..dbd547d4f 100644 --- a/src/main/presenter/configPresenter/acpInitHelper.ts +++ b/src/main/presenter/configPresenter/acpInitHelper.ts @@ -64,7 +64,7 @@ const BUILTIN_INIT_COMMANDS: Record = { description: 'Initialize Claude Code ACP' }, 'codex-acp': { - commands: ['npm i -g @zed-industries/codex-acp', 'npm install -g @openai/codex-sdk', 'codex'], + commands: ['npm i -g @zed-industries/codex-acp', 'npm install -g @openai/codex', 'codex'], description: 'Initialize Codex CLI ACP' } } diff --git a/src/renderer/settings/components/AcpTerminalDialog.vue b/src/renderer/settings/components/AcpTerminalDialog.vue index eddc027e8..f2f3e05ea 100644 --- a/src/renderer/settings/components/AcpTerminalDialog.vue +++ b/src/renderer/settings/components/AcpTerminalDialog.vue @@ -1,48 +1,32 @@