From 61ce586117cd5c95479d5fb786133a2a2fe2922c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Sat, 13 Dec 2025 20:42:59 +0800 Subject: [PATCH] refactor(vscode-ide-companion/cli): consolidate CLI detection and version management - Replace separate CliDetector, CliVersionChecker, and CliVersionManager classes with unified CliManager - Remove redundant code and simplify CLI detection logic - Maintain all existing functionality while improving code organization - Update imports in dependent files to use CliManager This change reduces complexity by consolidating CLI-related functionality into a single manager class. --- .../src/cli/cliContextManager.ts | 2 +- .../src/cli/cliDetector.ts | 332 ------------ .../src/cli/cliInstaller.ts | 10 +- .../src/cli/cliManager.ts | 498 ++++++++++++++++++ .../src/cli/cliVersionChecker.ts | 133 ----- .../src/cli/cliVersionManager.ts | 191 ------- .../src/extension.test.ts | 12 - .../src/services/qwenAgentManager.ts | 2 +- .../src/services/qwenConnectionHandler.ts | 4 +- .../src/utils/authErrors.ts | 17 - .../src/utils/authNotificationHandler.ts | 33 +- .../src/webview/WebViewProvider.ts | 8 +- .../messages/Assistant/AssistantMessage.tsx | 6 +- 13 files changed, 527 insertions(+), 721 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/cli/cliDetector.ts create mode 100644 packages/vscode-ide-companion/src/cli/cliManager.ts delete mode 100644 packages/vscode-ide-companion/src/cli/cliVersionChecker.ts delete mode 100644 packages/vscode-ide-companion/src/cli/cliVersionManager.ts diff --git a/packages/vscode-ide-companion/src/cli/cliContextManager.ts b/packages/vscode-ide-companion/src/cli/cliContextManager.ts index c812a08e..b13798b8 100644 --- a/packages/vscode-ide-companion/src/cli/cliContextManager.ts +++ b/packages/vscode-ide-companion/src/cli/cliContextManager.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; +import type { CliFeatureFlags, CliVersionInfo } from './cliManager.js'; export class CliContextManager { private static instance: CliContextManager; diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts deleted file mode 100644 index 3fb8c454..00000000 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); - -export interface CliDetectionResult { - isInstalled: boolean; - cliPath?: string; - version?: string; - error?: string; -} - -/** - * Detects if Qwen Code CLI is installed and accessible - */ -export class CliDetector { - private static cachedResult: CliDetectionResult | null = null; - private static lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - /** - * Lightweight CLI Detection Method - * - * This method is designed for performance optimization, checking only if the CLI exists - * without retrieving version information. - * Suitable for quick detection scenarios, such as pre-checks before initializing connections. - * - * Compared to the full detectQwenCli method, this method: - * - Omits version information retrieval step - * - Uses shorter timeout (3 seconds) - * - Faster response time - * - * @param forceRefresh - Whether to force refresh cached results, default is false - * @returns Promise - Detection result containing installation status and path - */ - static async detectQwenCliLightweight( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Check if cached result is available and not expired (30-second validity) - if ( - !forceRefresh && - this.cachedResult && - now - this.lastCheckTime < this.CACHE_DURATION_MS - ) { - return this.cachedResult; - } - - try { - const isWindows = process.platform === 'win32'; - const whichCommand = isWindows ? 'where' : 'which'; - - // Check if qwen command exists - try { - // Use simplified detection without NVM for speed - const detectionCommand = isWindows - ? `${whichCommand} qwen` - : `${whichCommand} qwen`; - - // Execute command to detect CLI path, set shorter timeout (3 seconds) - const { stdout } = await execAsync(detectionCommand, { - timeout: 3000, // Reduced timeout for faster detection - shell: isWindows ? undefined : '/bin/bash', - }); - - // Output may contain multiple lines, get first line as actual path - const lines = stdout - .trim() - .split('\n') - .filter((line) => line.trim()); - const cliPath = lines[0]; // Take only the first path - - // Build successful detection result, note no version information - this.cachedResult = { - isInstalled: true, - cliPath, - // Version information not retrieved in lightweight detection - }; - this.lastCheckTime = now; - return this.cachedResult; - } catch (detectionError) { - console.log('[CliDetector] CLI not found, error:', detectionError); - - // CLI not found, build error message - let error = `Qwen Code CLI not found in PATH. Please install 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. Solutions: - \n1. Reinstall 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 PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedResult = { - isInstalled: false, - error, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } catch (error) { - console.log('[CliDetector] 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. Solutions: - \n1. Reinstall 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 PATH environment variable includes npm's global bin directory`; - } - - this.cachedResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } - - /** - * 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.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 { - // 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: 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('[CliDetector] 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( - '[CliDetector] 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('[CliDetector] CLI version:', version); - } catch (versionError) { - console.log('[CliDetector] Failed to get CLI version:', versionError); - // Version check failed, but CLI is installed - } - - this.cachedResult = { - isInstalled: true, - cliPath, - version, - }; - this.lastCheckTime = now; - return this.cachedResult; - } catch (detectionError) { - console.log('[CliDetector] 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.cachedResult = { - isInstalled: false, - error, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } catch (error) { - console.log('[CliDetector] 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.cachedResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } - - /** - * Clears the cached detection result - */ - static clearCache(): void { - this.cachedResult = null; - this.lastCheckTime = 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', - }; - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliInstaller.ts b/packages/vscode-ide-companion/src/cli/cliInstaller.ts index 8bbf4f51..7ad46f06 100644 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/cliInstaller.ts @@ -5,7 +5,7 @@ */ import * as vscode from 'vscode'; -import { CliDetector } from './cliDetector.js'; +import { CliManager } from './cliManager.js'; /** * CLI Detection and Installation Handler @@ -20,7 +20,7 @@ export class CliInstaller { sendToWebView: (message: unknown) => void, ): Promise { try { - const result = await CliDetector.detectQwenCli(); + const result = await CliManager.detectQwenCli(); sendToWebView({ type: 'cliDetectionResult', @@ -31,7 +31,7 @@ export class CliInstaller { error: result.error, installInstructions: result.isInstalled ? undefined - : CliDetector.getInstallationInstructions(), + : CliManager.getInstallationInstructions(), }, }); @@ -134,8 +134,8 @@ export class CliInstaller { } // Clear cache and recheck - CliDetector.clearCache(); - const detection = await CliDetector.detectQwenCli(); + CliManager.clearCache(); + const detection = await CliManager.detectQwenCli(); if (detection.isInstalled) { vscode.window diff --git a/packages/vscode-ide-companion/src/cli/cliManager.ts b/packages/vscode-ide-companion/src/cli/cliManager.ts new file mode 100644 index 00000000..a11fe668 --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliManager.ts @@ -0,0 +1,498 @@ +/** + * @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/cli/cliVersionChecker.ts b/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts deleted file mode 100644 index 6c93609d..00000000 --- a/packages/vscode-ide-companion/src/cli/cliVersionChecker.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { CliDetector, type CliDetectionResult } from './cliDetector.js'; -import { - CliVersionManager, - MIN_CLI_VERSION_FOR_SESSION_METHODS, -} from './cliVersionManager.js'; -import semver from 'semver'; - -/** - * CLI Version Checker - * - * Handles CLI version checking with throttling to prevent frequent notifications. - * This class manages version checking and provides version information without - * constantly bothering the user with popups. - */ -export class CliVersionChecker { - private static instance: CliVersionChecker; - private lastNotificationTime: number = 0; - private static readonly NOTIFICATION_COOLDOWN_MS = 300000; // 5 minutes cooldown - private context: vscode.ExtensionContext; - - private constructor(context: vscode.ExtensionContext) { - this.context = context; - } - - /** - * Get singleton instance - */ - static getInstance(context?: vscode.ExtensionContext): CliVersionChecker { - if (!CliVersionChecker.instance && context) { - CliVersionChecker.instance = new CliVersionChecker(context); - } - return CliVersionChecker.instance; - } - - /** - * 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 CliDetector.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 versionManager = CliVersionManager.getInstance(); - const versionInfo = await versionManager.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`, - ); - this.lastNotificationTime = Date.now(); - } - - return { - isInstalled: true, - version: currentVersion, - isSupported, - needsUpdate, - }; - } catch (error) { - console.error('[CliVersionChecker] 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 > - CliVersionChecker.NOTIFICATION_COOLDOWN_MS - ); - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts deleted file mode 100644 index 0cd6ca2c..00000000 --- a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import semver from 'semver'; -import { CliDetector, type CliDetectionResult } from './cliDetector.js'; - -export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.5.0'; - -export interface CliFeatureFlags { - supportsSessionList: boolean; - supportsSessionLoad: boolean; -} - -export interface CliVersionInfo { - version: string | undefined; - isSupported: boolean; - features: CliFeatureFlags; - detectionResult: CliDetectionResult; -} - -/** - * CLI Version Manager - * - * Manages CLI version detection and feature availability based on version - */ -export class CliVersionManager { - private static instance: CliVersionManager; - private cachedVersionInfo: CliVersionInfo | null = null; - private lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): CliVersionManager { - if (!CliVersionManager.instance) { - CliVersionManager.instance = new CliVersionManager(); - } - return CliVersionManager.instance; - } - - /** - * 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( - `[CliVersionManager] Invalid semver: version=${version}, min=${minVersion}`, - ); - return false; - } - console.log(`[CliVersionManager] 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.lastCheckTime < CliVersionManager.CACHE_DURATION_MS - ) { - console.log('[CliVersionManager] Returning cached version info'); - return this.cachedVersionInfo; - } - - console.log('[CliVersionManager] Detecting CLI version...'); - - try { - // Detect CLI installation - const detectionResult = await CliDetector.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.lastCheckTime = now; - - console.log( - '[CliVersionManager] CLI version detection result:', - versionInfo, - ); - - return versionInfo; - } catch (error) { - console.error('[CliVersionManager] 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 - */ - clearCache(): void { - this.cachedVersionInfo = null; - this.lastCheckTime = 0; - CliDetector.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; - } -} diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index dd6b3352..31d5aa52 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -43,14 +43,6 @@ vi.mock('vscode', () => ({ registerWebviewPanelSerializer: vi.fn(() => ({ dispose: vi.fn(), })), - createStatusBarItem: vi.fn(() => ({ - text: '', - tooltip: '', - command: '', - show: vi.fn(), - hide: vi.fn(), - dispose: vi.fn(), - })), }, workspace: { workspaceFolders: [], @@ -66,10 +58,6 @@ vi.mock('vscode', () => ({ Uri: { joinPath: vi.fn(), }, - StatusBarAlignment: { - Left: 1, - Right: 2, - }, ExtensionMode: { Development: 1, Production: 2, diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index d7804ab4..6624d71e 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -25,7 +25,7 @@ import { import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; -import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliManager.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js'; diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 32873807..7085340e 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -12,7 +12,7 @@ import type { AcpConnection } from './acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import { CliDetector } from '../cli/cliDetector.js'; +import { CliManager } from '../cli/cliManager.js'; import { authMethod } from '../types/acpTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; @@ -50,7 +50,7 @@ export class QwenConnectionHandler { let requiresAuth = false; // Check if CLI exists using standard detection (with cached results for better performance) - const detectionResult = await CliDetector.detectQwenCli( + const detectionResult = await CliManager.detectQwenCli( /* forceRefresh */ false, // Use cached results when available for better performance ); if (!detectionResult.isInstalled) { diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts index d598d8cc..8b0e6af9 100644 --- a/packages/vscode-ide-companion/src/utils/authErrors.ts +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -4,15 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * Authentication Error Utility - * - * Used to uniformly identify and handle various authentication-related error messages. - * Determines if re-authentication is needed by matching predefined error patterns. - * - * @param error - The error object or string to check - * @returns true if it's an authentication-related error, false otherwise - */ const AUTH_ERROR_PATTERNS = [ 'Authentication required', // Standard authentication request message '(code: -32000)', // RPC error code -32000 indicates authentication failure @@ -23,14 +14,6 @@ const AUTH_ERROR_PATTERNS = [ /** * Determines if the given error is authentication-related - * - * This function detects various forms of authentication errors, including: - * - Direct error objects - * - String-form error messages - * - Other types of errors converted to strings for pattern matching - * - * @param error - The error object to check, can be an Error instance, string, or other type - * @returns boolean - true if the error is authentication-related, false otherwise */ export const isAuthenticationRequiredError = (error: unknown): boolean => { // Null check to avoid unnecessary processing diff --git a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts index 3586e042..362867c2 100644 --- a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts +++ b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts @@ -7,12 +7,12 @@ import * as vscode from 'vscode'; import type { AuthenticateUpdateNotification } from '../types/acpTypes.js'; -// Store reference to the authentication notification to allow auto-closing -let authNotificationDisposable: { dispose: () => void } | null = null; +// Store reference to the current notification +let currentNotification: Thenable | null = null; /** * Handle authentication update notifications by showing a VS Code notification - * with the authentication URI and a copy button. + * with the authentication URI and action buttons. * * @param data - Authentication update notification data containing the auth URI */ @@ -21,30 +21,21 @@ export function handleAuthenticateUpdate( ): void { const authUri = data._meta.authUri; - // Dismiss any existing authentication notification - if (authNotificationDisposable) { - authNotificationDisposable.dispose(); - authNotificationDisposable = null; - } - - // Show an information message with the auth URI and copy button - const notificationPromise = vscode.window.showInformationMessage( - `Qwen Code needs authentication. Click the button below to open the authentication page or copy the link to your browser.`, + // Store reference to the current notification + currentNotification = vscode.window.showInformationMessage( + `Qwen Code needs authentication. Click an action below:`, 'Open in Browser', 'Copy Link', + 'Dismiss', ); - // Create a simple disposable object - authNotificationDisposable = { - dispose: () => { - // We can't actually cancel the promise, but we can clear our reference - }, - }; - - notificationPromise.then((selection) => { + currentNotification.then((selection) => { if (selection === 'Open in Browser') { // Open the authentication URI in the default browser vscode.env.openExternal(vscode.Uri.parse(authUri)); + vscode.window.showInformationMessage( + 'Opening authentication page in your browser...', + ); } else if (selection === 'Copy Link') { // Copy the authentication URI to clipboard vscode.env.clipboard.writeText(authUri); @@ -54,6 +45,6 @@ export function handleAuthenticateUpdate( } // Clear the notification reference after user interaction - authNotificationDisposable = null; + currentNotification = null; }); } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index e0b533a0..7e3c97c6 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -8,12 +8,11 @@ import * as vscode from 'vscode'; import { QwenAgentManager } from '../services/qwenAgentManager.js'; import { ConversationStore } from '../services/conversationStore.js'; import type { AcpPermissionRequest } from '../types/acpTypes.js'; -import { CliDetector } from '../cli/cliDetector.js'; +import { CliManager } from '../cli/cliManager.js'; import { PanelManager } from '../webview/PanelManager.js'; import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; -import { CliVersionChecker } from '../cli/cliVersionChecker.js'; import { getFileName } from './utils/webviewUtils.js'; import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js'; @@ -566,7 +565,7 @@ export class WebViewProvider { ); // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); + const cliDetection = await CliManager.detectQwenCli(); if (!cliDetection.isInstalled) { console.log( @@ -590,7 +589,7 @@ export class WebViewProvider { console.log('[WebViewProvider] CLI version:', cliDetection.version); // Perform version check with throttled notifications - const versionChecker = CliVersionChecker.getInstance(this.context); + const versionChecker = CliManager.getInstance(this.context); await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam try { @@ -674,7 +673,6 @@ export class WebViewProvider { return vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: 'Logging in to Qwen Code... ', cancellable: false, }, async (progress) => { diff --git a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx index ed8badcc..84712efa 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx @@ -75,7 +75,11 @@ export const AssistantMessage: React.FC = ({ whiteSpace: 'normal', }} > - +