mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge branch 'feat/bundle-cli-in-vscode' of https://github.com/QwenLM/qwen-code into feat/vscode-ide-companion-borading
This commit is contained in:
@@ -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 './cliManager.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,225 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { CliManager } from './cliManager.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 CliManager.detectQwenCli();
|
||||
|
||||
sendToWebView({
|
||||
type: 'cliDetectionResult',
|
||||
data: {
|
||||
isInstalled: result.isInstalled,
|
||||
cliPath: result.cliPath,
|
||||
version: result.version,
|
||||
error: result.error,
|
||||
installInstructions: result.isInstalled
|
||||
? undefined
|
||||
: CliManager.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: process.platform === 'win32' ? undefined : '/bin/bash',
|
||||
}, // 2 minutes timeout
|
||||
);
|
||||
|
||||
console.log('[CliInstaller] Installation output:', stdout);
|
||||
if (stderr) {
|
||||
console.warn('[CliInstaller] Installation stderr:', stderr);
|
||||
}
|
||||
|
||||
// Clear cache and recheck
|
||||
CliManager.clearCache();
|
||||
const detection = await CliManager.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.`,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -21,7 +21,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
|
||||
@@ -57,12 +57,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> {
|
||||
@@ -72,7 +72,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
|
||||
@@ -91,48 +90,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(' '));
|
||||
@@ -141,7 +112,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);
|
||||
|
||||
@@ -23,9 +23,7 @@ import {
|
||||
type QwenConnectionResult,
|
||||
} 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/cliManager.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js';
|
||||
|
||||
@@ -189,11 +187,11 @@ 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,
|
||||
cliEntryPath: string,
|
||||
options?: AgentConnectOptions,
|
||||
): Promise<QwenConnectionResult> {
|
||||
this.currentWorkingDir = workingDir;
|
||||
@@ -201,7 +199,7 @@ export class QwenAgentManager {
|
||||
this.connection,
|
||||
this.sessionReader,
|
||||
workingDir,
|
||||
_cliPath,
|
||||
cliEntryPath,
|
||||
options,
|
||||
);
|
||||
}
|
||||
@@ -284,71 +282,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
|
||||
@@ -404,63 +382,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)
|
||||
@@ -510,31 +477,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
|
||||
@@ -938,16 +902,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;
|
||||
@@ -1021,32 +975,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
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
import type { AcpConnection } from './acpConnection.js';
|
||||
import type { QwenSessionReader } from '../services/qwenSessionReader.js';
|
||||
import { CliManager } from '../cli/cliManager.js';
|
||||
import { authMethod } from '../types/acpTypes.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
|
||||
@@ -32,13 +31,13 @@ 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,
|
||||
options?: {
|
||||
autoAuthenticate?: boolean;
|
||||
},
|
||||
@@ -49,19 +48,10 @@ export class QwenConnectionHandler {
|
||||
let sessionCreated = false;
|
||||
let requiresAuth = false;
|
||||
|
||||
// Check if CLI exists using standard detection (with cached results for better performance)
|
||||
const detectionResult = await CliManager.detectQwenCli(
|
||||
/* forceRefresh */ false, // Use cached results when available for better performance
|
||||
);
|
||||
if (!detectionResult.isInstalled) {
|
||||
throw new Error(detectionResult.error || 'Qwen CLI not found');
|
||||
}
|
||||
console.log('[QwenAgentManager] CLI detected at:', detectionResult.cliPath);
|
||||
|
||||
// Build extra CLI arguments (only essential parameters)
|
||||
const extraArgs: string[] = [];
|
||||
|
||||
await connection.connect(cliPath!, 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 { CliManager } from '../cli/cliManager.js';
|
||||
import { PanelManager } from '../webview/PanelManager.js';
|
||||
import { MessageHandler } from '../webview/MessageHandler.js';
|
||||
import { WebViewContent } from '../webview/WebViewContent.js';
|
||||
import { CliInstaller } from '../cli/cliInstaller.js';
|
||||
import { getFileName } from './utils/webviewUtils.js';
|
||||
import { type ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||
import { isAuthenticationRequiredError } from '../utils/authErrors.js';
|
||||
@@ -564,33 +562,36 @@ export class WebViewProvider {
|
||||
`[WebViewProvider] Using CLI-managed authentication (autoAuth=${autoAuthenticate})`,
|
||||
);
|
||||
|
||||
// Check if CLI is installed before attempting to connect
|
||||
const cliDetection = await CliManager.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);
|
||||
|
||||
// Perform version check with throttled notifications
|
||||
const versionChecker = CliManager.getInstance(this.context);
|
||||
await versionChecker.checkCliVersion(true); // Silent check to avoid popup spam
|
||||
|
||||
try {
|
||||
console.log('[WebViewProvider] Connecting to agent...');
|
||||
@@ -598,7 +599,7 @@ export class WebViewProvider {
|
||||
// Pass the detected CLI path to ensure we use the correct installation
|
||||
const connectResult = await this.agentManager.connect(
|
||||
workingDir,
|
||||
cliDetection.cliPath,
|
||||
bundledCliEntry,
|
||||
options,
|
||||
);
|
||||
console.log('[WebViewProvider] Agent connected successfully');
|
||||
|
||||
Reference in New Issue
Block a user