mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 01:23:53 +00:00
Merge pull request #1246 from QwenLM/feat/bundle-cli-in-vscode
Bundle CLI into VSCode release package
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -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",
|
||||
|
||||
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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* │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ │
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
**
|
||||
!dist/
|
||||
!dist/**
|
||||
../
|
||||
../../
|
||||
!LICENSE
|
||||
|
||||
@@ -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",
|
||||
|
||||
67
packages/vscode-ide-companion/scripts/copy-bundled-cli.js
Normal file
67
packages/vscode-ide-companion/scripts/copy-bundled-cli.js
Normal file
@@ -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);
|
||||
});
|
||||
98
packages/vscode-ide-companion/scripts/prepackage.js
Normal file
98
packages/vscode-ide-companion/scripts/prepackage.js
Normal file
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<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: '/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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.`,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> {
|
||||
@@ -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);
|
||||
|
||||
@@ -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<void> {
|
||||
async connect(workingDir: string, cliEntryPath: string): Promise<void> {
|
||||
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<Record<string, unknown>> = [];
|
||||
|
||||
// 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<Record<string, unknown>> = [];
|
||||
|
||||
// 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<Record<string, unknown>>)
|
||||
: [];
|
||||
}
|
||||
|
||||
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<Record<string, unknown>>;
|
||||
} else if (res && typeof res === 'object' && 'items' in res) {
|
||||
const itemsValue = (res as { items?: unknown }).items;
|
||||
items = Array.isArray(itemsValue)
|
||||
? (itemsValue as Array<Record<string, unknown>>)
|
||||
: [];
|
||||
}
|
||||
|
||||
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<Record<string, unknown>> = [];
|
||||
|
||||
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<Record<string, unknown>> = [];
|
||||
|
||||
if (Array.isArray(res)) {
|
||||
items = res;
|
||||
} else if (typeof res === 'object' && res !== null && 'items' in res) {
|
||||
const responseObject = res as {
|
||||
items?: Array<Record<string, unknown>>;
|
||||
};
|
||||
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<Record<string, unknown>>;
|
||||
};
|
||||
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<ChatMessage[]> {
|
||||
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<unknown> {
|
||||
// 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
|
||||
|
||||
@@ -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<void> {
|
||||
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<string>('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
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user