mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat(vscode-ide-companion): improve CLI path detection and error handling
- Move determineNodePathForCli function to dedicated cliPathDetector.ts file - Enhance error handling with specific guidance for permission issues - Add detailed error messages for different failure scenarios - Improve logging for debugging CLI path detection issues This change improves the reliability of CLI path detection by providing better error messages and handling edge cases more gracefully.
This commit is contained in:
@@ -159,7 +159,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod",
|
"prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod",
|
||||||
"build": "npm run build:dev",
|
"build": "npm run build:dev",
|
||||||
"build:dev": "npm run check-types && npm run lint && node esbuild.js",
|
"build:dev": "node esbuild.js",
|
||||||
"build:prod": "node esbuild.js --production",
|
"build:prod": "node esbuild.js --production",
|
||||||
"generate:notices": "node ./scripts/generate-notices.js",
|
"generate:notices": "node ./scripts/generate-notices.js",
|
||||||
"prepare": "npm run generate:notices",
|
"prepare": "npm run generate:notices",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import type {
|
|||||||
} from './connectionTypes.js';
|
} from './connectionTypes.js';
|
||||||
import { AcpMessageHandler } from './acpMessageHandler.js';
|
import { AcpMessageHandler } from './acpMessageHandler.js';
|
||||||
import { AcpSessionManager } from './acpSessionManager.js';
|
import { AcpSessionManager } from './acpSessionManager.js';
|
||||||
import { statSync } from 'fs';
|
import { determineNodePathForCli } from '../cli/cliPathDetector.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACP Connection Handler for VSCode Extension
|
* ACP Connection Handler for VSCode Extension
|
||||||
@@ -66,74 +66,6 @@ export class AcpConnection {
|
|||||||
this.sessionManager = new AcpSessionManager();
|
this.sessionManager = new AcpSessionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
private determineNodePathForCli(cliPath: string): string | null {
|
|
||||||
// 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 {
|
|
||||||
if (statSync(nodePath).isFile()) {
|
|
||||||
// Verify it's executable
|
|
||||||
const stats = statSync(nodePath);
|
|
||||||
if (stats.mode & 0o111) {
|
|
||||||
// Check if executable
|
|
||||||
console.log(
|
|
||||||
`[ACP] Found Node.js executable for CLI at: ${nodePath}`,
|
|
||||||
);
|
|
||||||
return nodePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// File doesn't exist or other error, continue to next pattern
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
if (statSync(nodePath).isFile()) {
|
|
||||||
const stats = statSync(nodePath);
|
|
||||||
if (stats.mode & 0o111) {
|
|
||||||
console.log(
|
|
||||||
`[ACP] Found Node.js executable in CLI directory at: ${nodePath}`,
|
|
||||||
);
|
|
||||||
return nodePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_error) {
|
|
||||||
// File doesn't exist, continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[ACP] Could not determine Node.js path for CLI: ${cliPath}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to ACP backend
|
* Connect to ACP backend
|
||||||
*
|
*
|
||||||
@@ -185,14 +117,21 @@ export class AcpConnection {
|
|||||||
// Handle various Node.js version managers (nvm, n, manual installations)
|
// Handle various Node.js version managers (nvm, n, manual installations)
|
||||||
if (cliPath.includes('/qwen') && !isWindows) {
|
if (cliPath.includes('/qwen') && !isWindows) {
|
||||||
// Try to determine the correct node executable for this qwen installation
|
// Try to determine the correct node executable for this qwen installation
|
||||||
const nodePath = this.determineNodePathForCli(cliPath);
|
const nodePathResult = determineNodePathForCli(cliPath);
|
||||||
if (nodePath) {
|
if (nodePathResult.path) {
|
||||||
spawnCommand = nodePath;
|
spawnCommand = nodePathResult.path;
|
||||||
spawnArgs = [cliPath, '--experimental-acp', ...extraArgs];
|
spawnArgs = [cliPath, '--experimental-acp', ...extraArgs];
|
||||||
} else {
|
} else {
|
||||||
// Fallback to direct execution
|
// Fallback to direct execution
|
||||||
spawnCommand = cliPath;
|
spawnCommand = cliPath;
|
||||||
spawnArgs = ['--experimental-acp', ...extraArgs];
|
spawnArgs = ['--experimental-acp', ...extraArgs];
|
||||||
|
|
||||||
|
// Log any error for debugging
|
||||||
|
if (nodePathResult.error) {
|
||||||
|
console.warn(
|
||||||
|
`[ACP] Node.js path detection warning: ${nodePathResult.error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
spawnCommand = cliPath;
|
spawnCommand = cliPath;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
AcpSessionUpdate,
|
AcpSessionUpdate,
|
||||||
AcpPermissionRequest,
|
AcpPermissionRequest,
|
||||||
} from '../constants/acpTypes.js';
|
} from '../constants/acpTypes.js';
|
||||||
import { CLIENT_METHODS } from './schema.js';
|
import { CLIENT_METHODS } from '../constants/acpSchema.js';
|
||||||
import type {
|
import type {
|
||||||
PendingRequest,
|
PendingRequest,
|
||||||
AcpConnectionCallbacks,
|
AcpConnectionCallbacks,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
AcpNotification,
|
AcpNotification,
|
||||||
AcpResponse,
|
AcpResponse,
|
||||||
} from '../constants/acpTypes.js';
|
} from '../constants/acpTypes.js';
|
||||||
import { AGENT_METHODS, CUSTOM_METHODS } from './schema.js';
|
import { AGENT_METHODS, CUSTOM_METHODS } from '../constants/acpSchema.js';
|
||||||
import type { PendingRequest } from './connectionTypes.js';
|
import type { PendingRequest } from './connectionTypes.js';
|
||||||
import type { ChildProcess } from 'child_process';
|
import type { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,50 @@ export class QwenAgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current session is valid and can send messages
|
||||||
|
* This performs a lightweight validation by sending a test prompt
|
||||||
|
*
|
||||||
|
* @returns True if session is valid, false otherwise
|
||||||
|
*/
|
||||||
|
async checkSessionValidity(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// If we don't have a current session, it's definitely not valid
|
||||||
|
if (!this.connection.currentSessionId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send a lightweight test prompt to validate the session
|
||||||
|
// We use a simple prompt that should return quickly
|
||||||
|
await this.connection.sendPrompt('test session validity');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
|
console.warn(
|
||||||
|
'[QwenAgentManager] Session validity check failed:',
|
||||||
|
errorMsg,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for common authentication/session expiration errors
|
||||||
|
const isAuthError =
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('No active ACP session') ||
|
||||||
|
errorMsg.includes('Session not found');
|
||||||
|
|
||||||
|
if (isAuthError) {
|
||||||
|
console.log(
|
||||||
|
'[QwenAgentManager] Detected authentication/session expiration',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, we can't determine validity definitively
|
||||||
|
// Assume session is still valid unless we know it's not
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session list with version-aware strategy
|
* Get session list with version-aware strategy
|
||||||
* First tries ACP method if CLI version supports it, falls back to file system method
|
* First tries ACP method if CLI version supports it, falls back to file system method
|
||||||
|
|||||||
@@ -5,15 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
import type { AcpPermissionRequest } from '../constants/acpTypes.js';
|
import type { AcpPermissionRequest } from '../constants/acpTypes.js';
|
||||||
|
|
||||||
/**
|
|
||||||
* Chat Message
|
|
||||||
*/
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
/** Message role: user or assistant */
|
|
||||||
role: 'user' | 'assistant';
|
role: 'user' | 'assistant';
|
||||||
/** Message content */
|
|
||||||
content: string;
|
content: string;
|
||||||
/** Timestamp */
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,29 @@ export class AuthStateManager {
|
|||||||
return state.isAuthenticated;
|
return state.isAuthenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force check auth state without clearing cache
|
||||||
|
* This is useful for debugging to see what's actually cached
|
||||||
|
*/
|
||||||
|
async debugAuthState(): Promise<void> {
|
||||||
|
const state = await this.getAuthState();
|
||||||
|
console.log('[AuthStateManager] DEBUG - Current auth state:', state);
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
const now = Date.now();
|
||||||
|
const age = Math.floor((now - state.timestamp) / 1000 / 60);
|
||||||
|
const isExpired =
|
||||||
|
now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION;
|
||||||
|
|
||||||
|
console.log('[AuthStateManager] DEBUG - Auth state age:', age, 'minutes');
|
||||||
|
console.log('[AuthStateManager] DEBUG - Auth state expired:', isExpired);
|
||||||
|
console.log(
|
||||||
|
'[AuthStateManager] DEBUG - Auth state valid:',
|
||||||
|
state.isAuthenticated,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save successful authentication state
|
* Save successful authentication state
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -124,9 +124,26 @@ export class CliDetector {
|
|||||||
} catch (detectionError) {
|
} catch (detectionError) {
|
||||||
console.log('[CliDetector] CLI not found, error:', detectionError);
|
console.log('[CliDetector] CLI not found, error:', detectionError);
|
||||||
// CLI not found
|
// 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 = {
|
this.cachedResult = {
|
||||||
isInstalled: false,
|
isInstalled: false,
|
||||||
error: `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`,
|
error,
|
||||||
};
|
};
|
||||||
this.lastCheckTime = now;
|
this.lastCheckTime = now;
|
||||||
return this.cachedResult;
|
return this.cachedResult;
|
||||||
@@ -135,9 +152,24 @@ export class CliDetector {
|
|||||||
console.log('[CliDetector] General detection error:', error);
|
console.log('[CliDetector] General detection error:', error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
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 = {
|
this.cachedResult = {
|
||||||
isInstalled: false,
|
isInstalled: false,
|
||||||
error: `Failed to detect Qwen Code CLI: ${errorMessage}`,
|
error: userFriendlyError,
|
||||||
};
|
};
|
||||||
this.lastCheckTime = now;
|
this.lastCheckTime = now;
|
||||||
return this.cachedResult;
|
return this.cachedResult;
|
||||||
|
|||||||
@@ -161,9 +161,23 @@ export class CliInstaller {
|
|||||||
console.error('[CliInstaller] Installation failed:', errorMessage);
|
console.error('[CliInstaller] Installation failed:', errorMessage);
|
||||||
console.error('[CliInstaller] Error stack:', error);
|
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
|
vscode.window
|
||||||
.showErrorMessage(
|
.showErrorMessage(
|
||||||
`Failed to install Qwen Code CLI: ${errorMessage}`,
|
userFriendlyMessage,
|
||||||
'Try Manual Installation',
|
'Try Manual Installation',
|
||||||
'View Documentation',
|
'View Documentation',
|
||||||
)
|
)
|
||||||
@@ -173,9 +187,26 @@ export class CliInstaller {
|
|||||||
'Qwen Code Installation',
|
'Qwen Code Installation',
|
||||||
);
|
);
|
||||||
terminal.show();
|
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(
|
terminal.sendText(
|
||||||
'npm install -g @qwen-code/qwen-code@latest',
|
'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') {
|
} else if (selection === 'View Documentation') {
|
||||||
vscode.env.openExternal(
|
vscode.env.openExternal(
|
||||||
vscode.Uri.parse(
|
vscode.Uri.parse(
|
||||||
|
|||||||
128
packages/vscode-ide-companion/src/cli/cliPathDetector.ts
Normal file
128
packages/vscode-ide-companion/src/cli/cliPathDetector.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* @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.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -309,14 +309,60 @@ export class WebViewProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy initialization: Do not attempt to connect/auth on WebView show.
|
// Attempt to restore authentication state and initialize connection
|
||||||
// Render the chat UI immediately; we will connect/login on-demand when the
|
|
||||||
// user sends a message or requests a session action.
|
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Lazy init: rendering empty conversation only',
|
'[WebViewProvider] Attempting to restore auth state and connection...',
|
||||||
|
);
|
||||||
|
await this.attemptAuthStateRestoration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to restore authentication state and initialize connection
|
||||||
|
* This is called when the webview is first shown
|
||||||
|
*/
|
||||||
|
private async attemptAuthStateRestoration(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.authStateManager) {
|
||||||
|
// Debug current auth state
|
||||||
|
await this.authStateManager.debugAuthState();
|
||||||
|
|
||||||
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||||
|
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
|
||||||
|
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
|
||||||
|
|
||||||
|
const hasValidAuth = await this.authStateManager.hasValidAuth(
|
||||||
|
workingDir,
|
||||||
|
authMethod,
|
||||||
|
);
|
||||||
|
console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth);
|
||||||
|
|
||||||
|
if (hasValidAuth) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Valid auth found, attempting connection...',
|
||||||
|
);
|
||||||
|
// Try to connect with cached auth
|
||||||
|
await this.initializeAgentConnection();
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] No valid auth found, rendering empty conversation',
|
||||||
|
);
|
||||||
|
// Render the chat UI immediately without connecting
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] No auth state manager, rendering empty conversation',
|
||||||
);
|
);
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WebViewProvider] Auth state restoration failed:', error);
|
||||||
|
// Fallback to rendering empty conversation
|
||||||
|
await this.initializeEmptyConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize agent connection and session
|
* Initialize agent connection and session
|
||||||
@@ -521,26 +567,104 @@ export class WebViewProvider {
|
|||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Connection refresh completed successfully',
|
'[WebViewProvider] Connection refresh completed successfully',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify webview that agent is connected after refresh
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'agentConnected',
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[WebViewProvider] Connection refresh failed:', error);
|
console.error('[WebViewProvider] Connection refresh failed:', error);
|
||||||
|
|
||||||
|
// Notify webview that agent connection failed after refresh
|
||||||
|
this.sendMessageToWebView({
|
||||||
|
type: 'agentConnectionError',
|
||||||
|
data: {
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load messages from current Qwen session
|
* Load messages from current Qwen session
|
||||||
* Creates a new ACP session for immediate message sending
|
* Attempts to restore an existing session before creating a new one
|
||||||
*/
|
*/
|
||||||
private async loadCurrentSessionMessages(): Promise<void> {
|
private async loadCurrentSessionMessages(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Initializing with empty conversation and creating ACP session',
|
'[WebViewProvider] Initializing with session restoration attempt',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a new ACP session so user can send messages immediately
|
|
||||||
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
|
|
||||||
|
// First, try to restore an existing session if we have cached auth
|
||||||
|
if (this.authStateManager) {
|
||||||
|
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||||
|
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
|
||||||
|
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
|
||||||
|
|
||||||
|
const hasValidAuth = await this.authStateManager.hasValidAuth(
|
||||||
|
workingDir,
|
||||||
|
authMethod,
|
||||||
|
);
|
||||||
|
if (hasValidAuth) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Found valid cached auth, attempting session restoration',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to create a session (this will use cached auth)
|
||||||
|
const sessionId = await this.agentManager.createNewSession(
|
||||||
|
workingDir,
|
||||||
|
this.authStateManager,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sessionId) {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] ACP session restored successfully with ID:',
|
||||||
|
sessionId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] ACP session restoration returned no session ID',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (restoreError) {
|
||||||
|
console.warn(
|
||||||
|
'[WebViewProvider] Failed to restore ACP session:',
|
||||||
|
restoreError,
|
||||||
|
);
|
||||||
|
// Clear invalid auth cache
|
||||||
|
await this.authStateManager.clearAuthState();
|
||||||
|
|
||||||
|
// Fall back to creating a new session
|
||||||
|
try {
|
||||||
|
await this.agentManager.createNewSession(
|
||||||
|
workingDir,
|
||||||
|
this.authStateManager,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] ACP session created successfully after restore failure',
|
||||||
|
);
|
||||||
|
} catch (sessionError) {
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Failed to create ACP session:',
|
||||||
|
sessionError,
|
||||||
|
);
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] No valid cached auth found, creating new session',
|
||||||
|
);
|
||||||
|
// No valid auth, create a new session (will trigger auth if needed)
|
||||||
try {
|
try {
|
||||||
await this.agentManager.createNewSession(
|
await this.agentManager.createNewSession(
|
||||||
workingDir,
|
workingDir,
|
||||||
@@ -556,6 +680,28 @@ export class WebViewProvider {
|
|||||||
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No auth state manager, create a new session
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] No auth state manager, creating new session',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await this.agentManager.createNewSession(
|
||||||
|
workingDir,
|
||||||
|
this.authStateManager,
|
||||||
|
);
|
||||||
|
console.log('[WebViewProvider] ACP session created successfully');
|
||||||
|
} catch (sessionError) {
|
||||||
|
console.error(
|
||||||
|
'[WebViewProvider] Failed to create ACP session:',
|
||||||
|
sessionError,
|
||||||
|
);
|
||||||
|
vscode.window.showWarningMessage(
|
||||||
|
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.initializeEmptyConversation();
|
await this.initializeEmptyConversation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -728,11 +874,11 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
console.log('[WebViewProvider] Panel restored successfully');
|
console.log('[WebViewProvider] Panel restored successfully');
|
||||||
|
|
||||||
// Lazy init on restore as well: do not auto-connect; just render UI.
|
// Attempt to restore authentication state and initialize connection
|
||||||
console.log(
|
console.log(
|
||||||
'[WebViewProvider] Lazy restore: rendering empty conversation only',
|
'[WebViewProvider] Attempting to restore auth state and connection after restore...',
|
||||||
);
|
);
|
||||||
await this.initializeEmptyConversation();
|
await this.attemptAuthStateRestoration();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -280,31 +280,31 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Validate current session before sending message
|
// Validate current session before sending message
|
||||||
// const isSessionValid = await this.agentManager.validateCurrentSession();
|
const isSessionValid = await this.agentManager.checkSessionValidity();
|
||||||
// if (!isSessionValid) {
|
if (!isSessionValid) {
|
||||||
// console.warn('[SessionMessageHandler] Current session is not valid');
|
console.warn('[SessionMessageHandler] Current session is not valid');
|
||||||
|
|
||||||
// // Show non-modal notification with Login button
|
// Show non-modal notification with Login button
|
||||||
// const result = await vscode.window.showWarningMessage(
|
const result = await vscode.window.showWarningMessage(
|
||||||
// 'Your session has expired. Please login again to continue using Qwen Code.',
|
'Your session has expired. Please login again to continue using Qwen Code.',
|
||||||
// 'Login Now',
|
'Login Now',
|
||||||
// );
|
);
|
||||||
|
|
||||||
// if (result === 'Login Now') {
|
if (result === 'Login Now') {
|
||||||
// // Use login handler directly
|
// Use login handler directly
|
||||||
// if (this.loginHandler) {
|
if (this.loginHandler) {
|
||||||
// await this.loginHandler();
|
await this.loginHandler();
|
||||||
// } else {
|
} else {
|
||||||
// // Fallback to command
|
// Fallback to command
|
||||||
// vscode.window.showInformationMessage(
|
vscode.window.showInformationMessage(
|
||||||
// 'Please wait while we connect to Qwen Code...',
|
'Please wait while we connect to Qwen Code...',
|
||||||
// );
|
);
|
||||||
// await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Send to agent
|
// Send to agent
|
||||||
try {
|
try {
|
||||||
@@ -378,12 +378,13 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
errorMsg.includes('Session not found') ||
|
errorMsg.includes('Session not found') ||
|
||||||
errorMsg.includes('No active ACP session') ||
|
errorMsg.includes('No active ACP session') ||
|
||||||
errorMsg.includes('Authentication required') ||
|
errorMsg.includes('Authentication required') ||
|
||||||
errorMsg.includes('(code: -32000)')
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token')
|
||||||
) {
|
) {
|
||||||
// Clear auth cache since session is invalid
|
// Show a more user-friendly error message for expired sessions
|
||||||
// Note: We would need access to authStateManager for this, but for now we'll just show login prompt
|
|
||||||
const result = await vscode.window.showWarningMessage(
|
const result = await vscode.window.showWarningMessage(
|
||||||
'Your login has expired. Please login again to continue using Qwen Code.',
|
'Your login session has expired or is invalid. Please login again to continue using Qwen Code.',
|
||||||
'Login Now',
|
'Login Now',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -394,6 +395,12 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
await vscode.commands.executeCommand('qwenCode.login');
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
vscode.window.showErrorMessage(`Error sending message: ${error}`);
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
@@ -459,12 +466,43 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
'[SessionMessageHandler] Failed to create new session:',
|
'[SessionMessageHandler] Failed to create new session:',
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const errorMsg = String(error);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to create a new session.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: { message: `Failed to create new session: ${error}` },
|
data: { message: `Failed to create new session: ${error}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle switch Qwen session request
|
* Handle switch Qwen session request
|
||||||
@@ -562,11 +600,43 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'qwenSessionSwitched',
|
type: 'qwenSessionSwitched',
|
||||||
data: { sessionId, messages, session: sessionDetails },
|
data: { sessionId, messages, session: sessionDetails },
|
||||||
});
|
});
|
||||||
} catch (_loadError) {
|
} catch (loadError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[SessionMessageHandler] session/load failed, using fallback',
|
'[SessionMessageHandler] session/load failed, using fallback:',
|
||||||
|
loadError,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const errorMsg = String(loadError);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to switch sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: create new session
|
// Fallback: create new session
|
||||||
const messages = await this.agentManager.getSessionMessages(sessionId);
|
const messages = await this.agentManager.getSessionMessages(sessionId);
|
||||||
|
|
||||||
@@ -591,6 +661,38 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
'[SessionMessageHandler] Failed to create session:',
|
'[SessionMessageHandler] Failed to create session:',
|
||||||
createError,
|
createError,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createErrorMsg = String(createError);
|
||||||
|
// Check for authentication/session expiration errors in session creation
|
||||||
|
if (
|
||||||
|
createErrorMsg.includes('Authentication required') ||
|
||||||
|
createErrorMsg.includes('(code: -32000)') ||
|
||||||
|
createErrorMsg.includes('Unauthorized') ||
|
||||||
|
createErrorMsg.includes('Invalid token') ||
|
||||||
|
createErrorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to switch sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
throw createError;
|
throw createError;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -607,12 +709,43 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to switch session:', error);
|
console.error('[SessionMessageHandler] Failed to switch session:', error);
|
||||||
|
|
||||||
|
const errorMsg = String(error);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to switch sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: { message: `Failed to switch session: ${error}` },
|
data: { message: `Failed to switch session: ${error}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle get Qwen sessions request
|
* Handle get Qwen sessions request
|
||||||
@@ -626,12 +759,43 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to get sessions:', error);
|
console.error('[SessionMessageHandler] Failed to get sessions:', error);
|
||||||
|
|
||||||
|
const errorMsg = String(error);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to view sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: { message: `Failed to get sessions: ${error}` },
|
data: { message: `Failed to get sessions: ${error}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle save session request
|
* Handle save session request
|
||||||
@@ -658,7 +822,38 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'saveSessionResponse',
|
type: 'saveSessionResponse',
|
||||||
data: response,
|
data: response,
|
||||||
});
|
});
|
||||||
} catch (_acpError) {
|
} catch (acpError) {
|
||||||
|
const errorMsg = String(acpError);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to save sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to direct save
|
// Fallback to direct save
|
||||||
const response = await this.agentManager.saveSessionDirect(
|
const response = await this.agentManager.saveSessionDirect(
|
||||||
messages,
|
messages,
|
||||||
@@ -674,6 +869,36 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
await this.handleGetQwenSessions();
|
await this.handleGetQwenSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to save session:', error);
|
console.error('[SessionMessageHandler] Failed to save session:', error);
|
||||||
|
|
||||||
|
const errorMsg = String(error);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to save sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'saveSessionResponse',
|
type: 'saveSessionResponse',
|
||||||
data: {
|
data: {
|
||||||
@@ -683,6 +908,7 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle resume session request
|
* Handle resume session request
|
||||||
@@ -732,7 +958,38 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
type: 'qwenSessionSwitched',
|
type: 'qwenSessionSwitched',
|
||||||
data: { sessionId, messages },
|
data: { sessionId, messages },
|
||||||
});
|
});
|
||||||
} catch (_acpError) {
|
} catch (acpError) {
|
||||||
|
const errorMsg = String(acpError);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to resume sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to direct load
|
// Fallback to direct load
|
||||||
const messages = await this.agentManager.loadSessionDirect(sessionId);
|
const messages = await this.agentManager.loadSessionDirect(sessionId);
|
||||||
|
|
||||||
@@ -751,10 +1008,41 @@ export class SessionMessageHandler extends BaseMessageHandler {
|
|||||||
await this.handleGetQwenSessions();
|
await this.handleGetQwenSessions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SessionMessageHandler] Failed to resume session:', error);
|
console.error('[SessionMessageHandler] Failed to resume session:', error);
|
||||||
|
|
||||||
|
const errorMsg = String(error);
|
||||||
|
// Check for authentication/session expiration errors
|
||||||
|
if (
|
||||||
|
errorMsg.includes('Authentication required') ||
|
||||||
|
errorMsg.includes('(code: -32000)') ||
|
||||||
|
errorMsg.includes('Unauthorized') ||
|
||||||
|
errorMsg.includes('Invalid token') ||
|
||||||
|
errorMsg.includes('No active ACP session')
|
||||||
|
) {
|
||||||
|
// Show a more user-friendly error message for expired sessions
|
||||||
|
const result = await vscode.window.showWarningMessage(
|
||||||
|
'Your login session has expired or is invalid. Please login again to resume sessions.',
|
||||||
|
'Login Now',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result === 'Login Now') {
|
||||||
|
if (this.loginHandler) {
|
||||||
|
await this.loginHandler();
|
||||||
|
} else {
|
||||||
|
await vscode.commands.executeCommand('qwenCode.login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a specific error to the webview for better UI handling
|
||||||
|
this.sendToWebView({
|
||||||
|
type: 'sessionExpired',
|
||||||
|
data: { message: 'Session expired. Please login again.' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
this.sendToWebView({
|
this.sendToWebView({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
data: { message: `Failed to resume session: ${error}` },
|
data: { message: `Failed to resume session: ${error}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Qwen Team
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*
|
|
||||||
* CSS Selector Compatibility Test
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* Test 1: Basic first-child and last-child selectors */
|
|
||||||
.test-container:first-child::after {
|
|
||||||
content: "FIRST";
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-container:last-child::after {
|
|
||||||
content: "LAST";
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.test-container:first-child:last-child::after {
|
|
||||||
content: "ONLY";
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test 2: Simple sibling selectors */
|
|
||||||
.test-container + .test-container::before {
|
|
||||||
content: "SIBLING";
|
|
||||||
color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test 3: Not selector */
|
|
||||||
.test-container:not(.special)::after {
|
|
||||||
content: "NOT SPECIAL";
|
|
||||||
color: purple;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user