From 5ce40085d5ae5a59308e789dff61f8aa92b49b6e Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Tue, 25 Nov 2025 19:27:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(vscode-ide-companion):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20CLI=20=E6=A3=80=E6=B5=8B=E5=92=8C=E8=BF=9E=E6=8E=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/acp/acpConnection.ts | 90 ++++++++++++++++++- .../src/agents/qwenAgentManager.ts | 3 +- .../src/agents/qwenConnectionHandler.ts | 7 +- .../src/cli/CliInstaller.ts | 8 +- .../src/cli/cliDetector.ts | 66 ++++++++++++-- .../src/webview/WebViewProvider.ts | 7 +- .../webview/handlers/FileMessageHandler.ts | 34 +++++-- .../src/webview/hooks/useWebViewMessages.ts | 1 + 8 files changed, 196 insertions(+), 20 deletions(-) diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index 5d59aa5c..44b2819a 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -20,6 +20,7 @@ import type { } from './connectionTypes.js'; import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpSessionManager } from './acpSessionManager.js'; +import { statSync } from 'fs'; /** * ACP Connection Handler for VSCode Extension @@ -66,7 +67,75 @@ export class AcpConnection { } /** - * Connect to ACP backend + * Determine the correct Node.js executable path for a given CLI installation + * Handles various Node.js version managers (nvm, n, manual installations) + * + * @param cliPath - Path to the CLI executable + * @returns Path to the Node.js executable, or null if not found + */ + private determineNodePathForCli(cliPath: string): string | null { + // Common patterns for Node.js installations + const nodePathPatterns = [ + // NVM pattern: /Users/user/.nvm/versions/node/vXX.XX.X/bin/qwen -> /Users/user/.nvm/versions/node/vXX.XX.X/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + + // N pattern: /Users/user/n/bin/qwen -> /Users/user/n/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + + // Manual installation pattern: /usr/local/bin/qwen -> /usr/local/bin/node + cliPath.replace(/\/qwen$/, '/node'), + + // Alternative pattern: /opt/nodejs/bin/qwen -> /opt/nodejs/bin/node + cliPath.replace(/\/bin\/qwen$/, '/bin/node'), + ]; + + // Check each pattern + for (const nodePath of nodePathPatterns) { + try { + if (statSync(nodePath).isFile()) { + // Verify it's executable + const stats = statSync(nodePath); + if (stats.mode & 0o111) { + // Check if executable + console.log( + `[ACP] Found Node.js executable for CLI at: ${nodePath}`, + ); + return nodePath; + } + } + } catch (_error) { + // File doesn't exist or other error, continue to next pattern + continue; + } + } + + // Try to find node in the same directory as the CLI + const cliDir = cliPath.substring(0, cliPath.lastIndexOf('/')); + const potentialNodePaths = [`${cliDir}/node`, `${cliDir}/bin/node`]; + + for (const nodePath of potentialNodePaths) { + try { + if (statSync(nodePath).isFile()) { + const stats = statSync(nodePath); + if (stats.mode & 0o111) { + console.log( + `[ACP] Found Node.js executable in CLI directory at: ${nodePath}`, + ); + return nodePath; + } + } + } catch (_error) { + // File doesn't exist, continue + continue; + } + } + + console.log(`[ACP] Could not determine Node.js path for CLI: ${cliPath}`); + return null; + } + + /** + * 连接到ACP后端 * * @param backend - Backend type * @param cliPath - CLI path @@ -112,8 +181,23 @@ export class AcpConnection { spawnCommand = isWindows ? 'npx.cmd' : 'npx'; spawnArgs = [...parts.slice(1), '--experimental-acp', ...extraArgs]; } else { - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', ...extraArgs]; + // For qwen CLI, ensure we use the correct Node.js version + // Handle various Node.js version managers (nvm, n, manual installations) + if (cliPath.includes('/qwen') && !isWindows) { + // Try to determine the correct node executable for this qwen installation + const nodePath = this.determineNodePathForCli(cliPath); + if (nodePath) { + spawnCommand = nodePath; + spawnArgs = [cliPath, '--experimental-acp', ...extraArgs]; + } else { + // Fallback to direct execution + spawnCommand = cliPath; + spawnArgs = ['--experimental-acp', ...extraArgs]; + } + } else { + spawnCommand = cliPath; + spawnArgs = ['--experimental-acp', ...extraArgs]; + } } console.log('[ACP] Spawning command:', spawnCommand, spawnArgs.join(' ')); diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 25c02d53..15e5db22 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -89,7 +89,7 @@ export class QwenAgentManager { async connect( workingDir: string, authStateManager?: AuthStateManager, - _cliPath?: string, // TODO: reserved for future override via settings + _cliPath?: string, ): Promise { this.currentWorkingDir = workingDir; await this.connectionHandler.connect( @@ -97,6 +97,7 @@ export class QwenAgentManager { this.sessionReader, workingDir, authStateManager, + _cliPath, ); } diff --git a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts index d1df4435..030974e6 100644 --- a/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/agents/qwenConnectionHandler.ts @@ -36,6 +36,7 @@ export class QwenConnectionHandler { sessionReader: QwenSessionReader, workingDir: string, authStateManager?: AuthStateManager, + cliPath?: string, ): Promise { const connectId = Date.now(); console.log(`\n========================================`); @@ -65,7 +66,9 @@ export class QwenConnectionHandler { } const config = vscode.workspace.getConfiguration('qwenCode'); - const cliPath = config.get('qwen.cliPath', 'qwen'); + // Use the provided CLI path if available, otherwise use the configured path + const effectiveCliPath = + cliPath || config.get('qwen.cliPath', 'qwen'); const openaiApiKey = config.get('qwen.openaiApiKey', ''); const openaiBaseUrl = config.get('qwen.openaiBaseUrl', ''); const model = config.get('qwen.model', ''); @@ -87,7 +90,7 @@ export class QwenConnectionHandler { console.log('[QwenAgentManager] Using proxy:', proxy); } - await connection.connect('qwen', cliPath, workingDir, extraArgs); + await connection.connect('qwen', effectiveCliPath, workingDir, extraArgs); // Determine authentication method const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; diff --git a/packages/vscode-ide-companion/src/cli/CliInstaller.ts b/packages/vscode-ide-companion/src/cli/CliInstaller.ts index c7c86e77..d75ae3bb 100644 --- a/packages/vscode-ide-companion/src/cli/CliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/CliInstaller.ts @@ -121,8 +121,11 @@ export class CliInstaller { ); const { stdout, stderr } = await execAsync( - 'npm install -g @qwen-code/qwen-code@latest', - { timeout: 120000 }, // 2 minutes timeout + installCommand, + { + timeout: 120000, + shell: '/bin/bash', + }, // 2 minutes timeout ); console.log('[CliInstaller] Installation output:', stdout); @@ -156,6 +159,7 @@ export class CliInstaller { const errorMessage = error instanceof Error ? error.message : String(error); console.error('[CliInstaller] Installation failed:', errorMessage); + console.error('[CliInstaller] Error stack:', error); vscode.window .showErrorMessage( diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index c09ed250..b88755dd 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -40,28 +40,77 @@ export class CliDetector { this.cachedResult && now - this.lastCheckTime < this.CACHE_DURATION_MS ) { + console.log('[CliDetector] Returning cached result'); return this.cachedResult; } + console.log( + '[CliDetector] Starting CLI detection, current PATH:', + process.env.PATH, + ); + try { const isWindows = process.platform === 'win32'; const whichCommand = isWindows ? 'where' : 'which'; // Check if qwen command exists try { - const { stdout } = await execAsync(`${whichCommand} qwen`, { + // Use NVM environment for consistent detection + // Fallback chain: default alias -> node alias -> current version + const detectionCommand = + process.platform === 'win32' + ? `${whichCommand} qwen` + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; + + console.log( + '[CliDetector] Detecting CLI with command:', + detectionCommand, + ); + + const { stdout } = await execAsync(detectionCommand, { timeout: 5000, + shell: '/bin/bash', }); - const cliPath = stdout.trim().split('\n')[0]; + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual path + const lines = stdout + .trim() + .split('\n') + .filter((line) => line.trim()); + const cliPath = lines[lines.length - 1]; + + console.log('[CliDetector] Found CLI at:', cliPath); // Try to get version let version: string | undefined; try { - const { stdout: versionOutput } = await execAsync('qwen --version', { + // Use NVM environment for version check + // Fallback chain: default alias -> node alias -> current version + // Also ensure we use the correct Node.js version that matches the CLI installation + const versionCommand = + process.platform === 'win32' + ? 'qwen --version' + : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; + + console.log( + '[CliDetector] Getting version with command:', + versionCommand, + ); + + const { stdout: versionOutput } = await execAsync(versionCommand, { timeout: 5000, + shell: '/bin/bash', }); - version = versionOutput.trim(); - } catch { + // The output may contain multiple lines, with NVM activation messages + // We want the last line which should be the actual version + const versionLines = versionOutput + .trim() + .split('\n') + .filter((line) => line.trim()); + version = versionLines[versionLines.length - 1]; + console.log('[CliDetector] CLI version:', version); + } catch (versionError) { + console.log('[CliDetector] Failed to get CLI version:', versionError); // Version check failed, but CLI is installed } @@ -72,7 +121,8 @@ export class CliDetector { }; this.lastCheckTime = now; return this.cachedResult; - } catch (_error) { + } catch (detectionError) { + console.log('[CliDetector] CLI not found, error:', detectionError); // CLI not found this.cachedResult = { isInstalled: false, @@ -82,6 +132,7 @@ export class CliDetector { return this.cachedResult; } } catch (error) { + console.log('[CliDetector] General detection error:', error); const errorMessage = error instanceof Error ? error.message : String(error); this.cachedResult = { @@ -115,6 +166,9 @@ export class CliDetector { 'Install via npm:', ' npm install -g @qwen-code/qwen-code@latest', '', + 'If you are using nvm (automatically handled by the plugin):', + ' The plugin will automatically use your default nvm version', + '', 'Or install from source:', ' git clone https://github.com/QwenLM/qwen-code.git', ' cd qwen-code', diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index e6c3a47d..565a4b07 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -416,7 +416,12 @@ export class WebViewProvider { const authInfo = await this.authStateManager.getAuthInfo(); console.log('[WebViewProvider] Auth cache status:', authInfo); - await this.agentManager.connect(workingDir, this.authStateManager); + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect( + workingDir, + this.authStateManager, + cliDetection.cliPath, + ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index 28f83686..097d8eba 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -181,6 +181,9 @@ export class FileMessageHandler extends BaseMessageHandler { */ private async handleGetWorkspaceFiles(query?: string): Promise { try { + console.log('[FileMessageHandler] handleGetWorkspaceFiles start', { + query, + }); const files: Array<{ id: string; label: string; @@ -220,6 +223,11 @@ export class FileMessageHandler extends BaseMessageHandler { // Search or show recent files if (query) { + // Query mode: perform filesystem search (may take longer on large workspaces) + console.log( + '[FileMessageHandler] Searching workspace files for query', + query, + ); const uris = await vscode.workspace.findFiles( `**/*${query}*`, '**/node_modules/**', @@ -230,6 +238,7 @@ export class FileMessageHandler extends BaseMessageHandler { addFile(uri); } } else { + // Non-query mode: respond quickly with currently active and open files // Add current active file first const activeEditor = vscode.window.activeTextEditor; if (activeEditor) { @@ -247,7 +256,21 @@ export class FileMessageHandler extends BaseMessageHandler { } } - // If not enough files, add some workspace files + // Send an initial quick response so UI can render immediately + try { + this.sendToWebView({ type: 'workspaceFiles', data: { files } }); + console.log( + '[FileMessageHandler] Sent initial workspaceFiles (open tabs/active)', + files.length, + ); + } catch (e) { + console.warn( + '[FileMessageHandler] Failed sending initial response', + e, + ); + } + + // If not enough files, add some workspace files (bounded) if (files.length < 10) { const recentUris = await vscode.workspace.findFiles( '**/*', @@ -264,10 +287,11 @@ export class FileMessageHandler extends BaseMessageHandler { } } - this.sendToWebView({ - type: 'workspaceFiles', - data: { files }, - }); + this.sendToWebView({ type: 'workspaceFiles', data: { files } }); + console.log( + '[FileMessageHandler] Sent final workspaceFiles', + files.length, + ); } catch (error) { console.error( '[FileMessageHandler] Failed to get workspace files:', diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index 4b8c658e..984664b3 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -552,6 +552,7 @@ export const useWebViewMessages = ({ path: string; }>; if (files) { + console.log('[WebView] Received workspaceFiles:', files.length); handlers.fileContext.setWorkspaceFiles(files); } break;