diff --git a/packages/vscode-ide-companion/src/cli/cliManager.ts b/packages/vscode-ide-companion/src/cli/cliManager.ts deleted file mode 100644 index a11fe668..00000000 --- a/packages/vscode-ide-companion/src/cli/cliManager.ts +++ /dev/null @@ -1,498 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import semver from 'semver'; -import { CliInstaller } from './cliInstaller.js'; - -const execAsync = promisify(exec); - -export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0'; - -export interface CliDetectionResult { - isInstalled: boolean; - cliPath?: string; - version?: string; - error?: string; -} - -export interface CliFeatureFlags { - supportsSessionList: boolean; - supportsSessionLoad: boolean; -} - -export interface CliVersionInfo { - version: string | undefined; - isSupported: boolean; - features: CliFeatureFlags; - detectionResult: CliDetectionResult; -} - -export class CliManager { - private static instance: CliManager; - private lastNotificationTime: number = 0; - private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown - private context: vscode.ExtensionContext | undefined; - - // Cache mechanisms - private static cachedDetectionResult: CliDetectionResult | null = null; - private static detectionLastCheckTime: number = 0; - private cachedVersionInfo: CliVersionInfo | null = null; - private versionLastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - private constructor(context?: vscode.ExtensionContext) { - this.context = context; - } - - /** - * Get singleton instance - */ - static getInstance(context?: vscode.ExtensionContext): CliManager { - if (!CliManager.instance && context) { - CliManager.instance = new CliManager(context); - } - return CliManager.instance; - } - - /** - * Checks if the Qwen Code CLI is installed - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and details - */ - static async detectQwenCli( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedDetectionResult && - now - this.detectionLastCheckTime < this.CACHE_DURATION_MS - ) { - console.log('[CliManager] Returning cached detection result'); - return this.cachedDetectionResult; - } - - console.log( - '[CliManager] 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 { - // 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( - '[CliManager] Detecting CLI with command:', - detectionCommand, - ); - - const { stdout } = await execAsync(detectionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // 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('[CliManager] Found CLI at:', cliPath); - - // Try to get version - let version: string | undefined; - try { - // 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( - '[CliManager] Getting version with command:', - versionCommand, - ); - - const { stdout: versionOutput } = await execAsync(versionCommand, { - timeout: 5000, - shell: isWindows ? undefined : '/bin/bash', - }); - // 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('[CliManager] CLI version:', version); - } catch (versionError) { - console.log('[CliManager] Failed to get CLI version:', versionError); - // Version check failed, but CLI is installed - } - - this.cachedDetectionResult = { - isInstalled: true, - cliPath, - version, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } catch (detectionError) { - console.log('[CliManager] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; - - // Provide specific guidance for permission errors - if (detectionError instanceof Error) { - const errorMessage = detectionError.message; - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedDetectionResult = { - isInstalled: false, - error, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } - } catch (error) { - console.log('[CliManager] General detection error:', error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; - - // Provide specific guidance for permission errors - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - - this.cachedDetectionResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.detectionLastCheckTime = now; - return this.cachedDetectionResult; - } - } - - /** - * Clears the cached detection result - */ - static clearCache(): void { - this.cachedDetectionResult = null; - this.detectionLastCheckTime = 0; - } - - /** - * Gets installation instructions based on the platform - */ - static getInstallationInstructions(): { - title: string; - steps: string[]; - documentationUrl: string; - } { - return { - title: 'Qwen Code CLI is not installed', - steps: [ - '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', - ' npm install', - ' npm install -g .', - '', - 'After installation, reload VS Code or restart the extension.', - ], - documentationUrl: 'https://github.com/QwenLM/qwen-code#installation', - }; - } - - /** - * Check if CLI version meets minimum requirements - * - * @param version - Version string to check - * @param minVersion - Minimum required version - * @returns Whether version meets requirements - */ - private isVersionSupported( - version: string | undefined, - minVersion: string, - ): boolean { - if (!version) { - return false; - } - - // Use semver for robust comparison (handles v-prefix, pre-release, etc.) - const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null; - const min = - semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null; - - if (!v || !min) { - console.warn( - `[CliManager] Invalid semver: version=${version}, min=${minVersion}`, - ); - return false; - } - console.log(`[CliManager] Version ${v} meets requirements: ${min}`); - return semver.gte(v, min); - } - - /** - * Get feature flags based on CLI version - * - * @param version - CLI version string - * @returns Feature flags - */ - private getFeatureFlags(version: string | undefined): CliFeatureFlags { - const isSupportedVersion = this.isVersionSupported( - version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ); - - return { - supportsSessionList: isSupportedVersion, - supportsSessionLoad: isSupportedVersion, - }; - } - - /** - * Detect CLI version and features - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns CLI version information - */ - async detectCliVersion(forceRefresh = false): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedVersionInfo && - now - this.versionLastCheckTime < CliManager.CACHE_DURATION_MS - ) { - console.log('[CliManager] Returning cached version info'); - return this.cachedVersionInfo; - } - - console.log('[CliManager] Detecting CLI version...'); - - try { - // Detect CLI installation - const detectionResult = await CliManager.detectQwenCli(forceRefresh); - - const versionInfo: CliVersionInfo = { - version: detectionResult.version, - isSupported: this.isVersionSupported( - detectionResult.version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ), - features: this.getFeatureFlags(detectionResult.version), - detectionResult, - }; - - // Cache the result - this.cachedVersionInfo = versionInfo; - this.versionLastCheckTime = now; - - console.log('[CliManager] CLI version detection result:', versionInfo); - - return versionInfo; - } catch (error) { - console.error('[CliManager] Failed to detect CLI version:', error); - - // Return fallback result - const fallbackResult: CliVersionInfo = { - version: undefined, - isSupported: false, - features: { - supportsSessionList: false, - supportsSessionLoad: false, - }, - detectionResult: { - isInstalled: false, - error: error instanceof Error ? error.message : String(error), - }, - }; - - return fallbackResult; - } - } - - /** - * Clear cached version information - */ - clearVersionCache(): void { - this.cachedVersionInfo = null; - this.versionLastCheckTime = 0; - CliManager.clearCache(); - } - - /** - * Check if CLI supports session/list method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/list is supported - */ - async supportsSessionList(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionList; - } - - /** - * Check if CLI supports session/load method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/load is supported - */ - async supportsSessionLoad(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionLoad; - } - - /** - * Check CLI version with cooldown to prevent spamming notifications - * - * @param showNotifications - Whether to show notifications for issues - * @returns Promise with version check result - */ - async checkCliVersion(showNotifications: boolean = true): Promise<{ - isInstalled: boolean; - version?: string; - isSupported: boolean; - needsUpdate: boolean; - error?: string; - }> { - try { - // Detect CLI installation - const detectionResult: CliDetectionResult = - await CliManager.detectQwenCli(); - - if (!detectionResult.isInstalled) { - if (showNotifications && this.canShowNotification()) { - vscode.window.showWarningMessage( - `Qwen Code CLI not found. Please install it using: npm install -g @qwen-code/qwen-code@latest`, - ); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: false, - error: detectionResult.error, - isSupported: false, - needsUpdate: false, - }; - } - - // Get version information - const versionInfo = await this.detectCliVersion(); - - const currentVersion = detectionResult.version; - const isSupported = versionInfo.isSupported; - - // Check if update is needed (version is too old) - const needsUpdate = currentVersion - ? !semver.satisfies( - currentVersion, - `>=${MIN_CLI_VERSION_FOR_SESSION_METHODS}`, - ) - : false; - - // Show notification only if needed and within cooldown period - if (showNotifications && !isSupported && this.canShowNotification()) { - vscode.window - .showWarningMessage( - `Qwen Code CLI version ${currentVersion} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later`, - 'Upgrade Now', - 'View Documentation', - ) - .then(async (selection) => { - if (selection === 'Upgrade Now') { - await CliInstaller.install(); - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse( - 'https://github.com/QwenLM/qwen-code#installation', - ), - ); - } - }); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: true, - version: currentVersion, - isSupported, - needsUpdate, - }; - } catch (error) { - console.error('[CliManager] Version check failed:', error); - - if (showNotifications && this.canShowNotification()) { - vscode.window.showErrorMessage( - `Failed to check Qwen Code CLI version: ${error instanceof Error ? error.message : String(error)}`, - ); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: false, - error: error instanceof Error ? error.message : String(error), - isSupported: false, - needsUpdate: false, - }; - } - } - - /** - * Check if notification can be shown based on cooldown period - */ - private canShowNotification(): boolean { - return ( - Date.now() - this.lastNotificationTime > - CliManager.NOTIFICATION_COOLDOWN_MS - ); - } -} diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 9fd92274..2d94a3b3 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -197,7 +197,6 @@ export class QwenAgentManager { this.currentWorkingDir = workingDir; return this.connectionHandler.connect( this.connection, - this.sessionReader, workingDir, cliEntryPath, options, diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 690c28e8..28f2db0b 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -11,9 +11,6 @@ */ import type { AcpConnection } from './acpConnection.js'; -import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import { authMethod } from '../types/acpTypes.js'; -import { isAuthenticationRequiredError } from '../utils/authErrors.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -29,156 +26,31 @@ export class QwenConnectionHandler { * Connect to Qwen service and establish session * * @param connection - ACP connection instance - * @param sessionReader - Session reader instance * @param workingDir - Working directory * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) */ async connect( connection: AcpConnection, - sessionReader: QwenSessionReader, workingDir: string, cliEntryPath: string, - options?: { - autoAuthenticate?: boolean; - }, ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] šŸš€ CONNECT() CALLED - ID: ${connectId}`); - const autoAuthenticate = options?.autoAuthenticate ?? true; - let sessionCreated = false; - let requiresAuth = false; + const sessionCreated = false; + const requiresAuth = false; // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; await connection.connect(cliEntryPath, workingDir, extraArgs); - // Try to restore existing session or create new session - // Note: Auto-restore on connect is disabled to avoid surprising loads - // when user opens a "New Chat" tab. Restoration is now an explicit action - // (session selector → session/load) or handled by higher-level flows. - const sessionRestored = false; - - // Create new session if unable to restore - if (!sessionRestored) { - console.log( - '[QwenAgentManager] no sessionRestored, Creating new session...', - ); - - try { - console.log( - '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', - ); - await this.newSessionWithRetry( - connection, - workingDir, - 3, - authMethod, - autoAuthenticate, - ); - console.log('[QwenAgentManager] New session created successfully'); - sessionCreated = true; - } catch (sessionError) { - const needsAuth = - autoAuthenticate === false && - isAuthenticationRequiredError(sessionError); - if (needsAuth) { - requiresAuth = true; - console.log( - '[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.', - ); - } else { - console.log( - `\nāš ļø [SESSION FAILED] newSessionWithRetry threw error\n`, - ); - console.log(`[QwenAgentManager] Error details:`, sessionError); - throw sessionError; - } - } - } else { - sessionCreated = true; - } + // Note: Session creation is now handled by the caller (QwenAgentManager) + // This prevents automatic session creation on every connection which was + // causing unwanted authentication prompts console.log(`\n========================================`); console.log(`[QwenAgentManager] āœ… CONNECT() COMPLETED SUCCESSFULLY`); console.log(`========================================\n`); return { sessionCreated, requiresAuth }; } - - /** - * Create new session (with retry) - * - * @param connection - ACP connection instance - * @param workingDir - Working directory - * @param maxRetries - Maximum number of retries - */ - private async newSessionWithRetry( - connection: AcpConnection, - workingDir: string, - maxRetries: number, - authMethod: string, - autoAuthenticate: boolean, - ): Promise { - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - console.log( - `[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`, - ); - await connection.newSession(workingDir); - console.log('[QwenAgentManager] Session created successfully'); - return; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error( - `[QwenAgentManager] Session creation attempt ${attempt} failed:`, - errorMessage, - ); - - // If Qwen reports that authentication is required, try to - // authenticate on-the-fly once and retry without waiting. - const requiresAuth = isAuthenticationRequiredError(error); - if (requiresAuth) { - if (!autoAuthenticate) { - console.log( - '[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.', - ); - throw error; - } - console.log( - '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', - ); - try { - await connection.authenticate(authMethod); - // FIXME: @yiliang114 If there is no delay for a while, immediately executing - // newSession may cause the cli authorization jump to be triggered again - // Add a slight delay to ensure auth state is settled - await new Promise((resolve) => setTimeout(resolve, 300)); - // Retry immediately after successful auth - await connection.newSession(workingDir); - console.log( - '[QwenAgentManager] Session created successfully after auth', - ); - return; - } catch (authErr) { - console.error( - '[QwenAgentManager] Re-authentication failed:', - authErr, - ); - // Fall through to retry logic below - } - } - - if (attempt === maxRetries) { - throw new Error( - `Session creation failed after ${maxRetries} attempts: ${errorMessage}`, - ); - } - - const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); - console.log(`[QwenAgentManager] Retrying in ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - } } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 721c20d6..4ab55283 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -570,94 +570,68 @@ export class WebViewProvider { ).fsPath; try { - console.log('[WebViewProvider] Connecting to bundled agent...'); - console.log('[WebViewProvider] Bundled CLI entry:', bundledCliEntry); + console.log('[WebViewProvider] Connecting to agent...'); - await this.agentManager.connect(workingDir, bundledCliEntry); + // Pass the detected CLI path to ensure we use the correct installation + const connectResult = await this.agentManager.connect( + workingDir, + bundledCliEntry, + options, + ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); + // If authentication is required and autoAuthenticate is false, + // send authState message and return without creating session + if (connectResult.requiresAuth && !autoAuthenticate) { + console.log( + '[WebViewProvider] Authentication required but auto-auth disabled, sending authState and returning', + ); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + // Initialize empty conversation to allow browsing history + await this.initializeEmptyConversation(); + return; + } - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); + if (connectResult.requiresAuth) { + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + } + + // Load messages from the current Qwen session + const sessionReady = await this.loadCurrentSessionMessages(options); + + if (sessionReady) { + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } else { + console.log( + '[WebViewProvider] Session creation deferred until user logs in.', + ); + } } catch (_error) { console.error('[WebViewProvider] Agent connection error:', _error); vscode.window.showWarningMessage( - `Failed to start bundled Qwen Code CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, + `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, ); // Fallback to empty conversation await this.initializeEmptyConversation(); - try { - console.log('[WebViewProvider] Connecting to agent...'); - - // Pass the detected CLI path to ensure we use the correct installation - const connectResult = await this.agentManager.connect( - workingDir, - bundledCliEntry, - options, - ); - console.log('[WebViewProvider] Agent connected successfully'); - this.agentInitialized = true; - - // If authentication is required and autoAuthenticate is false, - // send authState message and return without creating session - if (connectResult.requiresAuth && !autoAuthenticate) { - console.log( - '[WebViewProvider] Authentication required but auto-auth disabled, sending authState and returning', - ); - this.sendMessageToWebView({ - type: 'authState', - data: { authenticated: false }, - }); - // Initialize empty conversation to allow browsing history - await this.initializeEmptyConversation(); - return; - } - - if (connectResult.requiresAuth) { - this.sendMessageToWebView({ - type: 'authState', - data: { authenticated: false }, - }); - } - - // Load messages from the current Qwen session - const sessionReady = await this.loadCurrentSessionMessages(options); - - if (sessionReady) { - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); - } else { - console.log( - '[WebViewProvider] Session creation deferred until user logs in.', - ); - } - } catch (_error) { - console.error('[WebViewProvider] Agent connection error:', _error); - vscode.window.showWarningMessage( - `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, - ); - // Fallback to empty conversation - await this.initializeEmptyConversation(); - - // Notify webview that agent connection failed - this.sendMessageToWebView({ - type: 'agentConnectionError', - data: { - message: - _error instanceof Error ? _error.message : String(_error), - }, - }); - } + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: _error instanceof Error ? _error.message : String(_error), + }, + }); } };