diff --git a/.vscode/launch.json b/.vscode/launch.json index 0ae4f1b1..bab4f22e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,7 +27,7 @@ "outFiles": [ "${workspaceFolder}/packages/vscode-ide-companion/dist/**/*.js" ], - "preLaunchTask": "npm: build: vscode-ide-companion" + "preLaunchTask": "launch: vscode-ide-companion (copy+build)" }, { "name": "Attach", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 58709bc9..e0ee4730 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -20,6 +20,22 @@ "problemMatcher": [], "label": "npm: build: vscode-ide-companion", "detail": "npm run build -w packages/vscode-ide-companion" + }, + { + "label": "copy: bundled-cli (dev)", + "type": "shell", + "command": "node", + "args": ["packages/vscode-ide-companion/scripts/copy-bundled-cli.js"], + "problemMatcher": [] + }, + { + "label": "launch: vscode-ide-companion (copy+build)", + "dependsOrder": "sequence", + "dependsOn": [ + "copy: bundled-cli (dev)", + "npm: build: vscode-ide-companion" + ], + "problemMatcher": [] } ] } diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index f96ec33c..9e4d294e 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1461,7 +1461,7 @@ describe('SettingsDialog', () => { context: { fileFiltering: { respectGitIgnore: false, - respectQwemIgnore: true, + respectQwenIgnore: true, enableRecursiveFileSearch: false, disableFuzzySearch: true, }, @@ -1535,7 +1535,7 @@ describe('SettingsDialog', () => { loadMemoryFromIncludeDirectories: false, fileFiltering: { respectGitIgnore: false, - respectQwemIgnore: false, + respectQwenIgnore: false, enableRecursiveFileSearch: false, disableFuzzySearch: false, }, diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index 7c2c04f9..fbc2244b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -14,14 +14,14 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -48,14 +48,14 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -82,14 +82,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -116,14 +116,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false* │ -│ │ │ ▼ │ │ │ │ │ @@ -150,14 +150,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -184,14 +184,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -218,14 +218,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -252,14 +252,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -286,14 +286,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -320,14 +320,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title true* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips true* │ -│ │ │ ▼ │ │ │ │ │ diff --git a/packages/vscode-ide-companion/.vscodeignore b/packages/vscode-ide-companion/.vscodeignore index e74d0536..18e07a04 100644 --- a/packages/vscode-ide-companion/.vscodeignore +++ b/packages/vscode-ide-companion/.vscodeignore @@ -1,5 +1,6 @@ ** !dist/ +!dist/** ../ ../../ !LICENSE diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 09c5ff66..8698275b 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -113,7 +113,7 @@ "main": "./dist/extension.cjs", "type": "module", "scripts": { - "prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod", + "prepackage": "node ./scripts/prepackage.js", "build": "npm run build:dev", "build:dev": "npm run check-types && npm run lint && node esbuild.js", "build:prod": "node esbuild.js --production", diff --git a/packages/vscode-ide-companion/scripts/copy-bundled-cli.js b/packages/vscode-ide-companion/scripts/copy-bundled-cli.js new file mode 100644 index 00000000..d720e47f --- /dev/null +++ b/packages/vscode-ide-companion/scripts/copy-bundled-cli.js @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Copy the already-built root dist/ folder into the extension dist/qwen-cli/. + * + * Assumes repoRoot/dist already exists (e.g. produced by `npm run bundle` and + * optionally `npm run prepare:package`). + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const extensionRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(extensionRoot, '..', '..'); +const rootDistDir = path.join(repoRoot, 'dist'); +const extensionDistDir = path.join(extensionRoot, 'dist'); +const bundledCliDir = path.join(extensionDistDir, 'qwen-cli'); + +async function main() { + const cliJs = path.join(rootDistDir, 'cli.js'); + const vendorDir = path.join(rootDistDir, 'vendor'); + + if (!existsSync(cliJs) || !existsSync(vendorDir)) { + throw new Error( + `[copy-bundled-cli] Missing root dist artifacts. Expected:\n- ${cliJs}\n- ${vendorDir}\n\nRun root "npm run bundle" first.`, + ); + } + + await fs.mkdir(extensionDistDir, { recursive: true }); + const existingNodeModules = path.join(bundledCliDir, 'node_modules'); + const tmpNodeModules = path.join( + extensionDistDir, + 'qwen-cli.node_modules.tmp', + ); + const keepNodeModules = existsSync(existingNodeModules); + + // Preserve destination node_modules if it exists (e.g. after packaging install). + if (keepNodeModules) { + await fs.rm(tmpNodeModules, { recursive: true, force: true }); + await fs.rename(existingNodeModules, tmpNodeModules); + } + + await fs.rm(bundledCliDir, { recursive: true, force: true }); + await fs.mkdir(bundledCliDir, { recursive: true }); + + await fs.cp(rootDistDir, bundledCliDir, { recursive: true }); + + if (keepNodeModules) { + await fs.rename(tmpNodeModules, existingNodeModules); + } + + console.log(`[copy-bundled-cli] Copied ${rootDistDir} -> ${bundledCliDir}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/vscode-ide-companion/scripts/prepackage.js b/packages/vscode-ide-companion/scripts/prepackage.js new file mode 100644 index 00000000..ce9ca356 --- /dev/null +++ b/packages/vscode-ide-companion/scripts/prepackage.js @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * VS Code extension packaging orchestration. + * + * We bundle the CLI into the extension so users don't need a global install. + * To match the published CLI layout, we need to: + * - build root bundle (dist/cli.js + vendor/ + sandbox profiles) + * - run root prepare:package (dist/package.json + locales + README/LICENSE) + * - install production deps into root dist/ (dist/node_modules) so runtime deps + * like optional node-pty are present inside the VSIX payload. + * + * Then we generate notices and build the extension. + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const extensionRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(extensionRoot, '..', '..'); +const bundledCliDir = path.join(extensionRoot, 'dist', 'qwen-cli'); + +function npmBin() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +function run(cmd, args, opts = {}) { + const res = spawnSync(cmd, args, { + stdio: 'inherit', + shell: false, + ...opts, + }); + if (res.error) { + throw res.error; + } + if (typeof res.status === 'number' && res.status !== 0) { + throw new Error( + `Command failed (${res.status}): ${cmd} ${args.map((a) => JSON.stringify(a)).join(' ')}`, + ); + } +} + +function main() { + const npm = npmBin(); + + console.log('[prepackage] Bundling root CLI...'); + run(npm, ['--prefix', repoRoot, 'run', 'bundle'], { cwd: repoRoot }); + + console.log('[prepackage] Preparing root dist/ package metadata...'); + run(npm, ['--prefix', repoRoot, 'run', 'prepare:package'], { cwd: repoRoot }); + + console.log('[prepackage] Generating notices...'); + run(npm, ['run', 'generate:notices'], { cwd: extensionRoot }); + + console.log('[prepackage] Typechecking...'); + run(npm, ['run', 'check-types'], { cwd: extensionRoot }); + + console.log('[prepackage] Linting...'); + run(npm, ['run', 'lint'], { cwd: extensionRoot }); + + console.log('[prepackage] Building extension (production)...'); + run(npm, ['run', 'build:prod'], { cwd: extensionRoot }); + + console.log('[prepackage] Copying bundled CLI dist/ into extension...'); + run( + process.execPath, + [path.join(extensionRoot, 'scripts', 'copy-bundled-cli.js')], + { + cwd: extensionRoot, + }, + ); + + console.log( + '[prepackage] Installing production deps into extension dist/qwen-cli...', + ); + run( + npm, + [ + '--prefix', + bundledCliDir, + 'install', + '--omit=dev', + '--no-audit', + '--no-fund', + ], + { cwd: bundledCliDir }, + ); +} + +main(); diff --git a/packages/vscode-ide-companion/src/cli/cliContextManager.ts b/packages/vscode-ide-companion/src/cli/cliContextManager.ts deleted file mode 100644 index c812a08e..00000000 --- a/packages/vscode-ide-companion/src/cli/cliContextManager.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; - -export class CliContextManager { - private static instance: CliContextManager; - private currentVersionInfo: CliVersionInfo | null = null; - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): CliContextManager { - if (!CliContextManager.instance) { - CliContextManager.instance = new CliContextManager(); - } - return CliContextManager.instance; - } - - /** - * Set current CLI version information - * - * @param versionInfo - CLI version information - */ - setCurrentVersionInfo(versionInfo: CliVersionInfo): void { - this.currentVersionInfo = versionInfo; - } - - /** - * Get current CLI feature flags - * - * @returns Current CLI feature flags or default flags if not set - */ - getCurrentFeatures(): CliFeatureFlags { - if (this.currentVersionInfo) { - return this.currentVersionInfo.features; - } - - // Return default feature flags (all disabled) - return { - supportsSessionList: false, - supportsSessionLoad: false, - }; - } - - supportsSessionList(): boolean { - return this.getCurrentFeatures().supportsSessionList; - } - - supportsSessionLoad(): boolean { - return this.getCurrentFeatures().supportsSessionLoad; - } -} 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 875c2858..00000000 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ /dev/null @@ -1,215 +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 - - /** - * 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: '/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: '/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 deleted file mode 100644 index 4eb0d0e7..00000000 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { CliDetector } from './cliDetector.js'; - -/** - * CLI Detection and Installation Handler - * Responsible for detecting, installing, and prompting for Qwen CLI - */ -export class CliInstaller { - /** - * Check CLI installation status and send results to WebView - * @param sendToWebView Callback function to send messages to WebView - */ - static async checkInstallation( - sendToWebView: (message: unknown) => void, - ): Promise { - try { - const result = await CliDetector.detectQwenCli(); - - sendToWebView({ - type: 'cliDetectionResult', - data: { - isInstalled: result.isInstalled, - cliPath: result.cliPath, - version: result.version, - error: result.error, - installInstructions: result.isInstalled - ? undefined - : CliDetector.getInstallationInstructions(), - }, - }); - - if (!result.isInstalled) { - console.log('[CliInstaller] Qwen CLI not detected:', result.error); - } else { - console.log( - '[CliInstaller] Qwen CLI detected:', - result.cliPath, - result.version, - ); - } - } catch (error) { - console.error('[CliInstaller] CLI detection error:', error); - } - } - - /** - * Prompt user to install CLI - * Display warning message with installation options - */ - static async promptInstallation(): Promise { - const selection = await vscode.window.showWarningMessage( - 'Qwen Code CLI is not installed. You can browse conversation history, but cannot send new messages.', - 'Install Now', - 'View Documentation', - 'Remind Me Later', - ); - - if (selection === 'Install Now') { - await this.install(); - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse('https://github.com/QwenLM/qwen-code#installation'), - ); - } - } - - /** - * Install Qwen CLI - * Install global CLI package via npm - */ - static async install(): Promise { - try { - // Show progress notification - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Installing Qwen Code CLI', - cancellable: false, - }, - async (progress) => { - progress.report({ - message: 'Running: npm install -g @qwen-code/qwen-code@latest', - }); - - const { exec } = await import('child_process'); - const { promisify } = await import('util'); - const execAsync = promisify(exec); - - try { - // Use NVM environment to ensure we get the same Node.js version - // as when they run 'node -v' in terminal - // Fallback chain: default alias -> node alias -> current version - const installCommand = - process.platform === 'win32' - ? 'npm install -g @qwen-code/qwen-code@latest' - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); npm install -g @qwen-code/qwen-code@latest'; - - console.log( - '[CliInstaller] Installing with command:', - installCommand, - ); - console.log( - '[CliInstaller] Current process PATH:', - process.env['PATH'], - ); - - // Also log Node.js version being used by VS Code - console.log( - '[CliInstaller] VS Code Node.js version:', - process.version, - ); - console.log( - '[CliInstaller] VS Code Node.js execPath:', - process.execPath, - ); - - const { stdout, stderr } = await execAsync( - installCommand, - { - timeout: 120000, - shell: '/bin/bash', - }, // 2 minutes timeout - ); - - console.log('[CliInstaller] Installation output:', stdout); - if (stderr) { - console.warn('[CliInstaller] Installation stderr:', stderr); - } - - // Clear cache and recheck - CliDetector.clearCache(); - const detection = await CliDetector.detectQwenCli(); - - if (detection.isInstalled) { - vscode.window - .showInformationMessage( - `✅ Qwen Code CLI installed successfully! Version: ${detection.version}`, - 'Reload Window', - ) - .then((selection) => { - if (selection === 'Reload Window') { - vscode.commands.executeCommand( - 'workbench.action.reloadWindow', - ); - } - }); - } else { - throw new Error( - 'Installation completed but CLI still not detected', - ); - } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error('[CliInstaller] Installation failed:', errorMessage); - console.error('[CliInstaller] Error stack:', error); - - // Provide specific guidance for permission errors - let userFriendlyMessage = `Failed to install Qwen Code CLI: ${errorMessage}`; - - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyMessage += `\n\nThis is likely due to permission issues. Possible solutions: - \n1. Reinstall without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. Fix npm permissions: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Configure npm to use a different directory: npm config set prefix ~/.npm-global`; - } - - vscode.window - .showErrorMessage( - userFriendlyMessage, - 'Try Manual Installation', - 'View Documentation', - ) - .then((selection) => { - if (selection === 'Try Manual Installation') { - const terminal = vscode.window.createTerminal( - 'Qwen Code Installation', - ); - terminal.show(); - - // Provide different installation commands based on error type - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - terminal.sendText('# Try installing without sudo:'); - terminal.sendText( - 'npm install -g @qwen-code/qwen-code@latest', - ); - terminal.sendText(''); - terminal.sendText('# Or fix npm permissions:'); - terminal.sendText( - 'sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}', - ); - } else { - terminal.sendText( - 'npm install -g @qwen-code/qwen-code@latest', - ); - } - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse( - 'https://github.com/QwenLM/qwen-code#installation', - ), - ); - } - }); - } - }, - ); - } catch (error) { - console.error('[CliInstaller] Install CLI error:', error); - } - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliPathDetector.ts b/packages/vscode-ide-companion/src/cli/cliPathDetector.ts deleted file mode 100644 index 7f329873..00000000 --- a/packages/vscode-ide-companion/src/cli/cliPathDetector.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import { statSync } from 'fs'; - -export interface CliPathDetectionResult { - path: string | null; - error?: string; -} - -/** - * Determine the correct Node.js executable path for a given CLI installation - * Handles various Node.js version managers (nvm, n, manual installations) - * - * @param cliPath - Path to the CLI executable - * @returns Path to the Node.js executable, or null if not found - */ -export function determineNodePathForCli( - cliPath: string, -): CliPathDetectionResult { - // Common patterns for Node.js installations - const nodePathPatterns = [ - // NVM pattern: /Users/user/.nvm/versions/node/vXX.XX.X/bin/qwen -> /Users/user/.nvm/versions/node/vXX.XX.X/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - - // N pattern: /Users/user/n/bin/qwen -> /Users/user/n/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - - // Manual installation pattern: /usr/local/bin/qwen -> /usr/local/bin/node - cliPath.replace(/\/qwen$/, '/node'), - - // Alternative pattern: /opt/nodejs/bin/qwen -> /opt/nodejs/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - ]; - - // Check each pattern - for (const nodePath of nodePathPatterns) { - try { - const stats = statSync(nodePath); - if (stats.isFile()) { - // Verify it's executable - if (stats.mode & 0o111) { - console.log(`[CLI] Found Node.js executable for CLI at: ${nodePath}`); - return { path: nodePath }; - } else { - console.log(`[CLI] Node.js found at ${nodePath} but not executable`); - return { - path: null, - error: `Node.js found at ${nodePath} but not executable. You may need to fix file permissions or reinstall the CLI.`, - }; - } - } - } catch (error) { - // Differentiate between error types - if (error instanceof Error) { - if ('code' in error && error.code === 'EACCES') { - console.log(`[CLI] Permission denied accessing ${nodePath}`); - return { - path: null, - error: `Permission denied accessing ${nodePath}. The CLI may have been installed with sudo privileges. Try reinstalling without sudo or adjusting file permissions.`, - }; - } else if ('code' in error && error.code === 'ENOENT') { - // File not found, continue to next pattern - continue; - } else { - console.log(`[CLI] Error accessing ${nodePath}: ${error.message}`); - return { - path: null, - error: `Error accessing Node.js at ${nodePath}: ${error.message}`, - }; - } - } - } - } - - // Try to find node in the same directory as the CLI - const cliDir = cliPath.substring(0, cliPath.lastIndexOf('/')); - const potentialNodePaths = [`${cliDir}/node`, `${cliDir}/bin/node`]; - - for (const nodePath of potentialNodePaths) { - try { - const stats = statSync(nodePath); - if (stats.isFile()) { - if (stats.mode & 0o111) { - console.log( - `[CLI] Found Node.js executable in CLI directory at: ${nodePath}`, - ); - return { path: nodePath }; - } else { - console.log(`[CLI] Node.js found at ${nodePath} but not executable`); - return { - path: null, - error: `Node.js found at ${nodePath} but not executable. You may need to fix file permissions or reinstall the CLI.`, - }; - } - } - } catch (error) { - // Differentiate between error types - if (error instanceof Error) { - if ('code' in error && error.code === 'EACCES') { - console.log(`[CLI] Permission denied accessing ${nodePath}`); - return { - path: null, - error: `Permission denied accessing ${nodePath}. The CLI may have been installed with sudo privileges. Try reinstalling without sudo or adjusting file permissions.`, - }; - } else if ('code' in error && error.code === 'ENOENT') { - // File not found, continue - continue; - } else { - console.log(`[CLI] Error accessing ${nodePath}: ${error.message}`); - return { - path: null, - error: `Error accessing Node.js at ${nodePath}: ${error.message}`, - }; - } - } - } - } - - console.log(`[CLI] Could not determine Node.js path for CLI: ${cliPath}`); - return { - path: null, - error: `Could not find Node.js executable for CLI at ${cliPath}. Please verify the CLI installation.`, - }; -} 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.ts b/packages/vscode-ide-companion/src/extension.ts index 2adfaef1..c27a7e9d 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -292,7 +292,14 @@ export async function activate(context: vscode.ExtensionContext) { } if (selectedFolder) { - const qwenCmd = 'qwen'; + const cliEntry = vscode.Uri.joinPath( + context.extensionUri, + 'dist', + 'qwen-cli', + 'cli.js', + ).fsPath; + const quote = (s: string) => `"${s.replaceAll('"', '\\"')}"`; + const qwenCmd = `${quote(process.execPath)} ${quote(cliEntry)}`; const terminal = vscode.window.createTerminal({ name: `Qwen Code (${selectedFolder.name})`, cwd: selectedFolder.uri.fsPath, diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 464f8bcb..659c020f 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -20,7 +20,7 @@ import type { } from '../types/connectionTypes.js'; import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpSessionManager } from './acpSessionManager.js'; -import { determineNodePathForCli } from '../cli/cliPathDetector.js'; +import * as fs from 'node:fs'; /** * ACP Connection Handler for VSCode Extension @@ -54,12 +54,12 @@ export class AcpConnection { /** * Connect to Qwen ACP * - * @param cliPath - CLI path + * @param cliEntryPath - Path to the bundled CLI entrypoint (cli.js) * @param workingDir - Working directory * @param extraArgs - Extra command line arguments */ async connect( - cliPath: string, + cliEntryPath: string, workingDir: string = process.cwd(), extraArgs: string[] = [], ): Promise { @@ -69,7 +69,6 @@ export class AcpConnection { this.workingDir = workingDir; - const isWindows = process.platform === 'win32'; const env = { ...process.env }; // If proxy is configured in extraArgs, also set it as environment variable @@ -88,48 +87,20 @@ export class AcpConnection { env['https_proxy'] = proxyUrl; } - let spawnCommand: string; - let spawnArgs: string[]; + // Always run the bundled CLI using the VS Code extension host's Node runtime. + // This avoids PATH/NVM/global install problems and ensures deterministic behavior. + const spawnCommand: string = process.execPath; + const spawnArgs: string[] = [ + cliEntryPath, + '--experimental-acp', + '--channel=VSCode', + ...extraArgs, + ]; - if (cliPath.startsWith('npx ')) { - const parts = cliPath.split(' '); - spawnCommand = isWindows ? 'npx.cmd' : 'npx'; - spawnArgs = [ - ...parts.slice(1), - '--experimental-acp', - '--channel=VSCode', - ...extraArgs, - ]; - } else { - // For qwen CLI, ensure we use the correct Node.js version - // Handle various Node.js version managers (nvm, n, manual installations) - if (cliPath.includes('/qwen') && !isWindows) { - // Try to determine the correct node executable for this qwen installation - const nodePathResult = determineNodePathForCli(cliPath); - if (nodePathResult.path) { - spawnCommand = nodePathResult.path; - spawnArgs = [ - cliPath, - '--experimental-acp', - '--channel=VSCode', - ...extraArgs, - ]; - } else { - // Fallback to direct execution - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', '--channel=VSCode', ...extraArgs]; - - // Log any error for debugging - if (nodePathResult.error) { - console.warn( - `[ACP] Node.js path detection warning: ${nodePathResult.error}`, - ); - } - } - } else { - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', '--channel=VSCode', ...extraArgs]; - } + if (!fs.existsSync(cliEntryPath)) { + throw new Error( + `Bundled Qwen CLI entry not found at ${cliEntryPath}. The extension may not have been packaged correctly.`, + ); } console.log('[ACP] Spawning command:', spawnCommand, spawnArgs.join(' ')); @@ -138,7 +109,8 @@ export class AcpConnection { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], env, - shell: isWindows, + // We spawn node directly; no shell needed (and shell quoting can break paths). + shell: false, }; this.child = spawn(spawnCommand, spawnArgs, options); diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 2475e309..c87d3783 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -19,9 +19,7 @@ import type { } from '../types/chatTypes.js'; import { QwenConnectionHandler } from '../services/qwenConnectionHandler.js'; 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'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -162,15 +160,15 @@ export class QwenAgentManager { * Connect to Qwen service * * @param workingDir - Working directory - * @param cliPath - CLI path (optional, if provided will override the path in configuration) + * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) */ - async connect(workingDir: string, _cliPath?: string): Promise { + async connect(workingDir: string, cliEntryPath: string): Promise { this.currentWorkingDir = workingDir; await this.connectionHandler.connect( this.connection, this.sessionReader, workingDir, - _cliPath, + cliEntryPath, ); } @@ -252,71 +250,51 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - // Check if CLI supports session/list method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.supportsSessionList(); + // Prefer ACP method first; fall back to file system if it fails for any reason. + try { + console.log('[QwenAgentManager] Attempting to get session list via ACP'); + const response = await this.connection.listSessions(); + console.log('[QwenAgentManager] ACP session list response:', response); - console.log( - '[QwenAgentManager] CLI supports session/list:', - supportsSessionList, - ); + // sendRequest resolves with the JSON-RPC "result" directly + // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } + // Older prototypes might return an array. Support both. + const res: unknown = response; + let items: Array> = []; - // Try ACP method first if supported - if (supportsSessionList) { - try { - console.log( - '[QwenAgentManager] Attempting to get session list via ACP method', - ); - const response = await this.connection.listSessions(); - console.log('[QwenAgentManager] ACP session list response:', response); - - // sendRequest resolves with the JSON-RPC "result" directly - // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } - // Older prototypes might return an array. Support both. - const res: unknown = response; - let items: Array> = []; - - // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC - // "result" directly (not the full AcpResponse). Treat it as unknown - // and carefully narrow before accessing `items` to satisfy strict TS. - if (res && typeof res === 'object' && 'items' in res) { - const itemsValue = (res as { items?: unknown }).items; - items = Array.isArray(itemsValue) - ? (itemsValue as Array>) - : []; - } - - console.log( - '[QwenAgentManager] Sessions retrieved via ACP:', - res, - items.length, - ); - if (items.length > 0) { - const sessions = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); - - console.log( - '[QwenAgentManager] Sessions retrieved via ACP:', - sessions.length, - ); - return sessions; - } - } catch (error) { - console.warn( - '[QwenAgentManager] ACP session list failed, falling back to file system method:', - error, - ); + if (Array.isArray(res)) { + items = res as Array>; + } else if (res && typeof res === 'object' && 'items' in res) { + const itemsValue = (res as { items?: unknown }).items; + items = Array.isArray(itemsValue) + ? (itemsValue as Array>) + : []; } + + console.log('[QwenAgentManager] Sessions retrieved via ACP:', { + count: items.length, + }); + + if (items.length > 0) { + const sessions = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + return sessions; + } + } catch (error) { + console.warn( + '[QwenAgentManager] ACP session list failed, falling back to file system method:', + error, + ); } // Always fall back to file system method @@ -372,63 +350,52 @@ export class QwenAgentManager { }> { const size = params?.size ?? 20; const cursor = params?.cursor; + try { + const response = await this.connection.listSessions({ + size, + ...(cursor !== undefined ? { cursor } : {}), + }); + // sendRequest resolves with the JSON-RPC "result" directly + const res: unknown = response; + let items: Array> = []; - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.supportsSessionList(); - - if (supportsSessionList) { - try { - const response = await this.connection.listSessions({ - size, - ...(cursor !== undefined ? { cursor } : {}), - }); - // sendRequest resolves with the JSON-RPC "result" directly - const res: unknown = response; - let items: Array> = []; - - if (Array.isArray(res)) { - items = res; - } else if (typeof res === 'object' && res !== null && 'items' in res) { - const responseObject = res as { - items?: Array>; - }; - items = Array.isArray(responseObject.items) - ? responseObject.items - : []; - } - - const mapped = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); - - const nextCursor: number | undefined = - typeof res === 'object' && res !== null && 'nextCursor' in res - ? typeof res.nextCursor === 'number' - ? res.nextCursor - : undefined - : undefined; - const hasMore: boolean = - typeof res === 'object' && res !== null && 'hasMore' in res - ? Boolean(res.hasMore) - : false; - - return { sessions: mapped, nextCursor, hasMore }; - } catch (error) { - console.warn( - '[QwenAgentManager] Paged ACP session list failed:', - error, - ); - // fall through to file system + if (Array.isArray(res)) { + items = res; + } else if (typeof res === 'object' && res !== null && 'items' in res) { + const responseObject = res as { + items?: Array>; + }; + items = Array.isArray(responseObject.items) ? responseObject.items : []; } + + const mapped = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + + const nextCursor: number | undefined = + typeof res === 'object' && res !== null && 'nextCursor' in res + ? typeof res.nextCursor === 'number' + ? res.nextCursor + : undefined + : undefined; + const hasMore: boolean = + typeof res === 'object' && res !== null && 'hasMore' in res + ? Boolean(res.hasMore) + : false; + + return { sessions: mapped, nextCursor, hasMore }; + } catch (error) { + console.warn('[QwenAgentManager] Paged ACP session list failed:', error); + // fall through to file system } // Fallback: file system for current project only (to match ACP semantics) @@ -478,31 +445,28 @@ export class QwenAgentManager { async getSessionMessages(sessionId: string): Promise { try { // Prefer reading CLI's JSONL if we can find filePath from session/list - const cliContextManager = CliContextManager.getInstance(); - if (cliContextManager.supportsSessionList()) { - try { - const list = await this.getSessionList(); - const item = list.find( - (s) => s.sessionId === sessionId || s.id === sessionId, - ); - console.log( - '[QwenAgentManager] Session list item for filePath lookup:', - item, - ); - if ( - typeof item === 'object' && - item !== null && - 'filePath' in item && - typeof item.filePath === 'string' - ) { - const messages = await this.readJsonlMessages(item.filePath); - // Even if messages array is empty, we should return it rather than falling back - // This ensures we don't accidentally show messages from a different session format - return messages; - } - } catch (e) { - console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); + try { + const list = await this.getSessionList(); + const item = list.find( + (s) => s.sessionId === sessionId || s.id === sessionId, + ); + console.log( + '[QwenAgentManager] Session list item for filePath lookup:', + item, + ); + if ( + typeof item === 'object' && + item !== null && + 'filePath' in item && + typeof item.filePath === 'string' + ) { + const messages = await this.readJsonlMessages(item.filePath); + // Even if messages array is empty, we should return it rather than falling back + // This ensures we don't accidentally show messages from a different session format + return messages; } + } catch (e) { + console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); } // Fallback: legacy JSON session files @@ -906,16 +870,6 @@ export class QwenAgentManager { sessionId: string, cwdOverride?: string, ): Promise { - // Check if CLI supports session/load method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - - if (!supportsSessionLoad) { - throw new Error( - `CLI version does not support session/load method. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, - ); - } - try { // Route upcoming session/update messages as discrete messages for replay this.rehydratingSessionId = sessionId; @@ -989,32 +943,18 @@ export class QwenAgentManager { sessionId, ); - // Check if CLI supports session/load method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - - console.log( - '[QwenAgentManager] CLI supports session/load:', - supportsSessionLoad, - ); - - // Try ACP method first if supported - if (supportsSessionLoad) { - try { - console.log( - '[QwenAgentManager] Attempting to load session via ACP method', - ); - await this.loadSessionViaAcp(sessionId); - console.log('[QwenAgentManager] Session loaded successfully via ACP'); - - // After loading via ACP, we still need to get messages from file system - // In future, we might get them directly from the ACP response - } catch (error) { - console.warn( - '[QwenAgentManager] ACP session load failed, falling back to file system method:', - error, - ); - } + // Prefer ACP session/load first; fall back to file system on failure. + try { + console.log('[QwenAgentManager] Attempting to load session via ACP'); + await this.loadSessionViaAcp(sessionId); + console.log('[QwenAgentManager] Session loaded successfully via ACP'); + // After loading via ACP, we still need to get messages from file system. + // In future, we might get them directly from the ACP response. + } catch (error) { + console.warn( + '[QwenAgentManager] ACP session load failed, falling back to file system method:', + error, + ); } // Always fall back to file system method diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 91d4c6bf..33aeef30 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -10,14 +10,8 @@ * Handles Qwen Agent connection establishment, authentication, and session creation */ -import * as vscode from 'vscode'; import type { AcpConnection } from './acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import { - CliVersionManager, - MIN_CLI_VERSION_FOR_SESSION_METHODS, -} from '../cli/cliVersionManager.js'; -import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; /** @@ -31,52 +25,21 @@ export class QwenConnectionHandler { * @param connection - ACP connection instance * @param sessionReader - Session reader instance * @param workingDir - Working directory - * @param cliPath - CLI path (optional, if provided will override the path in configuration) + * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) */ async connect( connection: AcpConnection, sessionReader: QwenSessionReader, workingDir: string, - cliPath?: string, + cliEntryPath: string, ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`); - // Check CLI version and features - const cliVersionManager = CliVersionManager.getInstance(); - const versionInfo = await cliVersionManager.detectCliVersion(); - console.log('[QwenAgentManager] CLI version info:', versionInfo); - - // Store CLI context - const cliContextManager = CliContextManager.getInstance(); - cliContextManager.setCurrentVersionInfo(versionInfo); - - // Show warning if CLI version is below minimum requirement - if (!versionInfo.isSupported) { - // Wait to determine release version number - const selection = await vscode.window.showWarningMessage( - `Qwen Code CLI version ${versionInfo.version} 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', - ); - - // Handle the user's selection - if (selection === 'Upgrade Now') { - // Open terminal and run npm install command - const terminal = vscode.window.createTerminal('Qwen Code CLI Upgrade'); - terminal.show(); - terminal.sendText('npm install -g @qwen-code/qwen-code@latest'); - } - } - - const config = vscode.workspace.getConfiguration('qwenCode'); - // Use the provided CLI path if available, otherwise use the configured path - const effectiveCliPath = - cliPath || config.get('qwen.cliPath', 'qwen'); - // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - await connection.connect(effectiveCliPath, workingDir, extraArgs); + 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 diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index f2b36ab0..02688e17 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -8,11 +8,9 @@ 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 { PanelManager } from '../webview/PanelManager.js'; import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; -import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js'; @@ -555,63 +553,44 @@ export class WebViewProvider { ); console.log('[WebViewProvider] Using CLI-managed authentication'); - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); + const bundledCliEntry = vscode.Uri.joinPath( + this.extensionUri, + 'dist', + 'qwen-cli', + 'cli.js', + ).fsPath; - if (!cliDetection.isInstalled) { - console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + try { + console.log('[WebViewProvider] Connecting to bundled agent...'); + console.log('[WebViewProvider] Bundled CLI entry:', bundledCliEntry); + + await this.agentManager.connect(workingDir, bundledCliEntry); + console.log('[WebViewProvider] Agent connected successfully'); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } 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.`, ); - console.log( - '[WebViewProvider] CLI detection error:', - cliDetection.error, - ); - - // Show VSCode notification with installation option - await CliInstaller.promptInstallation(); - - // Initialize empty conversation (can still browse history) + // Fallback to empty conversation await this.initializeEmptyConversation(); - } else { - console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', - ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); - try { - console.log('[WebViewProvider] Connecting to agent...'); - - // Pass the detected CLI path to ensure we use the correct installation - await this.agentManager.connect(workingDir, cliDetection.cliPath); - console.log('[WebViewProvider] Agent connected successfully'); - this.agentInitialized = true; - - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); - - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); - } 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), + }, + }); } };