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.
This commit is contained in:
yiliang114
2025-12-13 20:42:59 +08:00
parent 90fc4c33f0
commit 61ce586117
13 changed files with 527 additions and 721 deletions

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; import type { CliFeatureFlags, CliVersionInfo } from './cliManager.js';
export class CliContextManager { export class CliContextManager {
private static instance: CliContextManager; private static instance: CliContextManager;

View File

@@ -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<CliDetectionResult> - Detection result containing installation status and path
*/
static async detectQwenCliLightweight(
forceRefresh = false,
): Promise<CliDetectionResult> {
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<CliDetectionResult> {
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',
};
}
}

View File

@@ -5,7 +5,7 @@
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { CliDetector } from './cliDetector.js'; import { CliManager } from './cliManager.js';
/** /**
* CLI Detection and Installation Handler * CLI Detection and Installation Handler
@@ -20,7 +20,7 @@ export class CliInstaller {
sendToWebView: (message: unknown) => void, sendToWebView: (message: unknown) => void,
): Promise<void> { ): Promise<void> {
try { try {
const result = await CliDetector.detectQwenCli(); const result = await CliManager.detectQwenCli();
sendToWebView({ sendToWebView({
type: 'cliDetectionResult', type: 'cliDetectionResult',
@@ -31,7 +31,7 @@ export class CliInstaller {
error: result.error, error: result.error,
installInstructions: result.isInstalled installInstructions: result.isInstalled
? undefined ? undefined
: CliDetector.getInstallationInstructions(), : CliManager.getInstallationInstructions(),
}, },
}); });
@@ -134,8 +134,8 @@ export class CliInstaller {
} }
// Clear cache and recheck // Clear cache and recheck
CliDetector.clearCache(); CliManager.clearCache();
const detection = await CliDetector.detectQwenCli(); const detection = await CliManager.detectQwenCli();
if (detection.isInstalled) { if (detection.isInstalled) {
vscode.window vscode.window

View File

@@ -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<CliDetectionResult> {
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<CliVersionInfo> {
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<boolean> {
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<boolean> {
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
);
}
}

View File

@@ -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
);
}
}

View File

@@ -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<CliVersionInfo> {
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<boolean> {
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<boolean> {
const versionInfo = await this.detectCliVersion(forceRefresh);
return versionInfo.features.supportsSessionLoad;
}
}

View File

@@ -43,14 +43,6 @@ vi.mock('vscode', () => ({
registerWebviewPanelSerializer: vi.fn(() => ({ registerWebviewPanelSerializer: vi.fn(() => ({
dispose: vi.fn(), dispose: vi.fn(),
})), })),
createStatusBarItem: vi.fn(() => ({
text: '',
tooltip: '',
command: '',
show: vi.fn(),
hide: vi.fn(),
dispose: vi.fn(),
})),
}, },
workspace: { workspace: {
workspaceFolders: [], workspaceFolders: [],
@@ -66,10 +58,6 @@ vi.mock('vscode', () => ({
Uri: { Uri: {
joinPath: vi.fn(), joinPath: vi.fn(),
}, },
StatusBarAlignment: {
Left: 1,
Right: 2,
},
ExtensionMode: { ExtensionMode: {
Development: 1, Development: 1,
Production: 2, Production: 2,

View File

@@ -25,7 +25,7 @@ import {
import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js';
import { CliContextManager } from '../cli/cliContextManager.js'; import { CliContextManager } from '../cli/cliContextManager.js';
import { authMethod } from '../types/acpTypes.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 { isAuthenticationRequiredError } from '../utils/authErrors.js';
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js'; import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';

View File

@@ -12,7 +12,7 @@
import type { AcpConnection } from './acpConnection.js'; import type { AcpConnection } from './acpConnection.js';
import type { QwenSessionReader } from '../services/qwenSessionReader.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 { authMethod } from '../types/acpTypes.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js';
@@ -50,7 +50,7 @@ export class QwenConnectionHandler {
let requiresAuth = false; let requiresAuth = false;
// Check if CLI exists using standard detection (with cached results for better performance) // 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 /* forceRefresh */ false, // Use cached results when available for better performance
); );
if (!detectionResult.isInstalled) { if (!detectionResult.isInstalled) {

View File

@@ -4,15 +4,6 @@
* SPDX-License-Identifier: Apache-2.0 * 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 = [ const AUTH_ERROR_PATTERNS = [
'Authentication required', // Standard authentication request message 'Authentication required', // Standard authentication request message
'(code: -32000)', // RPC error code -32000 indicates authentication failure '(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 * 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 => { export const isAuthenticationRequiredError = (error: unknown): boolean => {
// Null check to avoid unnecessary processing // Null check to avoid unnecessary processing

View File

@@ -7,12 +7,12 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type { AuthenticateUpdateNotification } from '../types/acpTypes.js'; import type { AuthenticateUpdateNotification } from '../types/acpTypes.js';
// Store reference to the authentication notification to allow auto-closing // Store reference to the current notification
let authNotificationDisposable: { dispose: () => void } | null = null; let currentNotification: Thenable<string | undefined> | null = null;
/** /**
* Handle authentication update notifications by showing a VS Code notification * 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 * @param data - Authentication update notification data containing the auth URI
*/ */
@@ -21,30 +21,21 @@ export function handleAuthenticateUpdate(
): void { ): void {
const authUri = data._meta.authUri; const authUri = data._meta.authUri;
// Dismiss any existing authentication notification // Store reference to the current notification
if (authNotificationDisposable) { currentNotification = vscode.window.showInformationMessage(
authNotificationDisposable.dispose(); `Qwen Code needs authentication. Click an action below:`,
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.`,
'Open in Browser', 'Open in Browser',
'Copy Link', 'Copy Link',
'Dismiss',
); );
// Create a simple disposable object currentNotification.then((selection) => {
authNotificationDisposable = {
dispose: () => {
// We can't actually cancel the promise, but we can clear our reference
},
};
notificationPromise.then((selection) => {
if (selection === 'Open in Browser') { if (selection === 'Open in Browser') {
// Open the authentication URI in the default browser // Open the authentication URI in the default browser
vscode.env.openExternal(vscode.Uri.parse(authUri)); vscode.env.openExternal(vscode.Uri.parse(authUri));
vscode.window.showInformationMessage(
'Opening authentication page in your browser...',
);
} else if (selection === 'Copy Link') { } else if (selection === 'Copy Link') {
// Copy the authentication URI to clipboard // Copy the authentication URI to clipboard
vscode.env.clipboard.writeText(authUri); vscode.env.clipboard.writeText(authUri);
@@ -54,6 +45,6 @@ export function handleAuthenticateUpdate(
} }
// Clear the notification reference after user interaction // Clear the notification reference after user interaction
authNotificationDisposable = null; currentNotification = null;
}); });
} }

View File

@@ -8,12 +8,11 @@ import * as vscode from 'vscode';
import { QwenAgentManager } from '../services/qwenAgentManager.js'; import { QwenAgentManager } from '../services/qwenAgentManager.js';
import { ConversationStore } from '../services/conversationStore.js'; import { ConversationStore } from '../services/conversationStore.js';
import type { AcpPermissionRequest } from '../types/acpTypes.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 { PanelManager } from '../webview/PanelManager.js';
import { MessageHandler } from '../webview/MessageHandler.js'; import { MessageHandler } from '../webview/MessageHandler.js';
import { WebViewContent } from '../webview/WebViewContent.js'; import { WebViewContent } from '../webview/WebViewContent.js';
import { CliInstaller } from '../cli/cliInstaller.js'; import { CliInstaller } from '../cli/cliInstaller.js';
import { CliVersionChecker } from '../cli/cliVersionChecker.js';
import { getFileName } from './utils/webviewUtils.js'; import { getFileName } from './utils/webviewUtils.js';
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { isAuthenticationRequiredError } from '../utils/authErrors.js';
@@ -566,7 +565,7 @@ export class WebViewProvider {
); );
// Check if CLI is installed before attempting to connect // Check if CLI is installed before attempting to connect
const cliDetection = await CliDetector.detectQwenCli(); const cliDetection = await CliManager.detectQwenCli();
if (!cliDetection.isInstalled) { if (!cliDetection.isInstalled) {
console.log( console.log(
@@ -590,7 +589,7 @@ export class WebViewProvider {
console.log('[WebViewProvider] CLI version:', cliDetection.version); console.log('[WebViewProvider] CLI version:', cliDetection.version);
// Perform version check with throttled notifications // 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 await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam
try { try {
@@ -674,7 +673,6 @@ export class WebViewProvider {
return vscode.window.withProgress( return vscode.window.withProgress(
{ {
location: vscode.ProgressLocation.Notification, location: vscode.ProgressLocation.Notification,
title: 'Logging in to Qwen Code... ',
cancellable: false, cancellable: false,
}, },
async (progress) => { async (progress) => {

View File

@@ -75,7 +75,11 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
whiteSpace: 'normal', whiteSpace: 'normal',
}} }}
> >
<MessageContent content={content} onFileClick={onFileClick} /> <MessageContent
content={content}
onFileClick={onFileClick}
enableFileLinks={false}
/>
</div> </div>
</span> </span>
</div> </div>