diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index 2e1bd974..5e60c9af 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -159,7 +159,7 @@ "scripts": { "prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod", "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", "generate:notices": "node ./scripts/generate-notices.js", "prepare": "npm run generate:notices", diff --git a/packages/vscode-ide-companion/src/acp/acpConnection.ts b/packages/vscode-ide-companion/src/acp/acpConnection.ts index a77fc435..b0040cc4 100644 --- a/packages/vscode-ide-companion/src/acp/acpConnection.ts +++ b/packages/vscode-ide-companion/src/acp/acpConnection.ts @@ -20,7 +20,7 @@ import type { } from './connectionTypes.js'; import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpSessionManager } from './acpSessionManager.js'; -import { statSync } from 'fs'; +import { determineNodePathForCli } from '../cli/cliPathDetector.js'; /** * ACP Connection Handler for VSCode Extension @@ -66,74 +66,6 @@ export class AcpConnection { 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 * @@ -185,14 +117,21 @@ export class AcpConnection { // 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 nodePath = this.determineNodePathForCli(cliPath); - if (nodePath) { - spawnCommand = nodePath; + const nodePathResult = determineNodePathForCli(cliPath); + if (nodePathResult.path) { + spawnCommand = nodePathResult.path; spawnArgs = [cliPath, '--experimental-acp', ...extraArgs]; } else { // Fallback to direct execution spawnCommand = cliPath; spawnArgs = ['--experimental-acp', ...extraArgs]; + + // Log any error for debugging + if (nodePathResult.error) { + console.warn( + `[ACP] Node.js path detection warning: ${nodePathResult.error}`, + ); + } } } else { spawnCommand = cliPath; diff --git a/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts b/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts index 8665b6bc..fc6938fb 100644 --- a/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/acp/acpMessageHandler.ts @@ -18,7 +18,7 @@ import type { AcpSessionUpdate, AcpPermissionRequest, } from '../constants/acpTypes.js'; -import { CLIENT_METHODS } from './schema.js'; +import { CLIENT_METHODS } from '../constants/acpSchema.js'; import type { PendingRequest, AcpConnectionCallbacks, diff --git a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts index 4269d177..2f9b2a02 100644 --- a/packages/vscode-ide-companion/src/acp/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/acp/acpSessionManager.ts @@ -16,7 +16,7 @@ import type { AcpNotification, AcpResponse, } 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 { ChildProcess } from 'child_process'; diff --git a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts index 2538908a..48f02915 100644 --- a/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/agents/qwenAgentManager.ts @@ -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 { + 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 * First tries ACP method if CLI version supports it, falls back to file system method diff --git a/packages/vscode-ide-companion/src/agents/qwenTypes.ts b/packages/vscode-ide-companion/src/agents/qwenTypes.ts index 897853b3..6cb5c625 100644 --- a/packages/vscode-ide-companion/src/agents/qwenTypes.ts +++ b/packages/vscode-ide-companion/src/agents/qwenTypes.ts @@ -5,15 +5,9 @@ */ import type { AcpPermissionRequest } from '../constants/acpTypes.js'; -/** - * Chat Message - */ export interface ChatMessage { - /** Message role: user or assistant */ role: 'user' | 'assistant'; - /** Message content */ content: string; - /** Timestamp */ timestamp: number; } diff --git a/packages/vscode-ide-companion/src/auth/authStateManager.ts b/packages/vscode-ide-companion/src/auth/authStateManager.ts index c2cfa47e..a781b9bb 100644 --- a/packages/vscode-ide-companion/src/auth/authStateManager.ts +++ b/packages/vscode-ide-companion/src/auth/authStateManager.ts @@ -77,6 +77,29 @@ export class AuthStateManager { return state.isAuthenticated; } + /** + * Force check auth state without clearing cache + * This is useful for debugging to see what's actually cached + */ + async debugAuthState(): Promise { + 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 */ diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts index b88755dd..875c2858 100644 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ b/packages/vscode-ide-companion/src/cli/cliDetector.ts @@ -124,9 +124,26 @@ export class CliDetector { } catch (detectionError) { console.log('[CliDetector] CLI not found, error:', detectionError); // CLI not found + let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; + + // Provide specific guidance for permission errors + if (detectionError instanceof Error) { + const errorMessage = detectionError.message; + if ( + errorMessage.includes('EACCES') || + errorMessage.includes('Permission denied') + ) { + error += `\n\nThis may be due to permission issues. Possible solutions: + \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code + \n3. Use nvm for Node.js version management to avoid permission issues + \n4. Check your PATH environment variable includes npm's global bin directory`; + } + } + this.cachedResult = { isInstalled: false, - error: `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`, + error, }; this.lastCheckTime = now; return this.cachedResult; @@ -135,9 +152,24 @@ export class CliDetector { console.log('[CliDetector] General detection error:', error); const errorMessage = error instanceof Error ? error.message : String(error); + + let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; + + // Provide specific guidance for permission errors + if ( + errorMessage.includes('EACCES') || + errorMessage.includes('Permission denied') + ) { + userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: + \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest + \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code + \n3. Use nvm for Node.js version management to avoid permission issues + \n4. Check your PATH environment variable includes npm's global bin directory`; + } + this.cachedResult = { isInstalled: false, - error: `Failed to detect Qwen Code CLI: ${errorMessage}`, + error: userFriendlyError, }; this.lastCheckTime = now; return this.cachedResult; diff --git a/packages/vscode-ide-companion/src/cli/cliInstaller.ts b/packages/vscode-ide-companion/src/cli/cliInstaller.ts index a3df2310..4eb0d0e7 100644 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ b/packages/vscode-ide-companion/src/cli/cliInstaller.ts @@ -161,9 +161,23 @@ export class CliInstaller { 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( - `Failed to install Qwen Code CLI: ${errorMessage}`, + userFriendlyMessage, 'Try Manual Installation', 'View Documentation', ) @@ -173,9 +187,26 @@ export class CliInstaller { 'Qwen Code Installation', ); terminal.show(); - terminal.sendText( - 'npm install -g @qwen-code/qwen-code@latest', - ); + + // 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( diff --git a/packages/vscode-ide-companion/src/cli/cliPathDetector.ts b/packages/vscode-ide-companion/src/cli/cliPathDetector.ts new file mode 100644 index 00000000..7f329873 --- /dev/null +++ b/packages/vscode-ide-companion/src/cli/cliPathDetector.ts @@ -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.`, + }; +} diff --git a/packages/vscode-ide-companion/src/acp/schema.ts b/packages/vscode-ide-companion/src/constants/acpSchema.ts similarity index 100% rename from packages/vscode-ide-companion/src/acp/schema.ts rename to packages/vscode-ide-companion/src/constants/acpSchema.ts diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index fc68997d..20b328ed 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -309,13 +309,59 @@ export class WebViewProvider { }); } - // Lazy initialization: Do not attempt to connect/auth on WebView show. - // Render the chat UI immediately; we will connect/login on-demand when the - // user sends a message or requests a session action. + // Attempt to restore authentication state and initialize connection console.log( - '[WebViewProvider] Lazy init: rendering empty conversation only', + '[WebViewProvider] Attempting to restore auth state and connection...', ); - await this.initializeEmptyConversation(); + await this.attemptAuthStateRestoration(); + } + + /** + * Attempt to restore authentication state and initialize connection + * This is called when the webview is first shown + */ + private async attemptAuthStateRestoration(): Promise { + 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('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(); + } + } catch (error) { + console.error('[WebViewProvider] Auth state restoration failed:', error); + // Fallback to rendering empty conversation + await this.initializeEmptyConversation(); + } } /** @@ -521,40 +567,140 @@ export class WebViewProvider { console.log( '[WebViewProvider] Connection refresh completed successfully', ); + + // Notify webview that agent is connected after refresh + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); } catch (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; } } /** * 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 { try { 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 workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - try { - await this.agentManager.createNewSession( + // 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('qwen.openaiApiKey', ''); + const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth'; + + const hasValidAuth = await this.authStateManager.hasValidAuth( workingDir, - this.authStateManager, + authMethod, ); - 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.`, + 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 { + 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.`, + ); + } + } + } 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(); @@ -728,11 +874,11 @@ export class WebViewProvider { 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( - '[WebViewProvider] Lazy restore: rendering empty conversation only', + '[WebViewProvider] Attempting to restore auth state and connection after restore...', ); - await this.initializeEmptyConversation(); + await this.attemptAuthStateRestoration(); } /** diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 54b56614..30364e88 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -280,31 +280,31 @@ export class SessionMessageHandler extends BaseMessageHandler { return; } - // // Validate current session before sending message - // const isSessionValid = await this.agentManager.validateCurrentSession(); - // if (!isSessionValid) { - // console.warn('[SessionMessageHandler] Current session is not valid'); + // Validate current session before sending message + const isSessionValid = await this.agentManager.checkSessionValidity(); + if (!isSessionValid) { + console.warn('[SessionMessageHandler] Current session is not valid'); - // // Show non-modal notification with Login button - // const result = await vscode.window.showWarningMessage( - // 'Your session has expired. Please login again to continue using Qwen Code.', - // 'Login Now', - // ); + // Show non-modal notification with Login button + const result = await vscode.window.showWarningMessage( + 'Your session has expired. Please login again to continue using Qwen Code.', + 'Login Now', + ); - // if (result === 'Login Now') { - // // Use login handler directly - // if (this.loginHandler) { - // await this.loginHandler(); - // } else { - // // Fallback to command - // vscode.window.showInformationMessage( - // 'Please wait while we connect to Qwen Code...', - // ); - // await vscode.commands.executeCommand('qwenCode.login'); - // } - // } - // return; - // } + if (result === 'Login Now') { + // Use login handler directly + if (this.loginHandler) { + await this.loginHandler(); + } else { + // Fallback to command + vscode.window.showInformationMessage( + 'Please wait while we connect to Qwen Code...', + ); + await vscode.commands.executeCommand('qwenCode.login'); + } + } + return; + } // Send to agent try { @@ -378,12 +378,13 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('Session not found') || errorMsg.includes('No active ACP session') || 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 - // Note: We would need access to authStateManager for this, but for now we'll just show login prompt + // Show a more user-friendly error message for expired sessions 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', ); @@ -394,6 +395,12 @@ export class SessionMessageHandler extends BaseMessageHandler { 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 { vscode.window.showErrorMessage(`Error sending message: ${error}`); this.sendToWebView({ @@ -459,10 +466,41 @@ export class SessionMessageHandler extends BaseMessageHandler { '[SessionMessageHandler] Failed to create new session:', error, ); - this.sendToWebView({ - type: 'error', - data: { message: `Failed to create new 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 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({ + type: 'error', + data: { message: `Failed to create new session: ${error}` }, + }); + } } } @@ -562,11 +600,43 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'qwenSessionSwitched', data: { sessionId, messages, session: sessionDetails }, }); - } catch (_loadError) { + } catch (loadError) { 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 const messages = await this.agentManager.getSessionMessages(sessionId); @@ -591,6 +661,38 @@ export class SessionMessageHandler extends BaseMessageHandler { '[SessionMessageHandler] Failed to create session:', 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; } } else { @@ -607,10 +709,41 @@ export class SessionMessageHandler extends BaseMessageHandler { } } catch (error) { console.error('[SessionMessageHandler] Failed to switch session:', error); - this.sendToWebView({ - type: 'error', - data: { message: `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({ + type: 'error', + data: { message: `Failed to switch session: ${error}` }, + }); + } } } @@ -626,10 +759,41 @@ export class SessionMessageHandler extends BaseMessageHandler { }); } catch (error) { console.error('[SessionMessageHandler] Failed to get sessions:', error); - this.sendToWebView({ - type: 'error', - data: { message: `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({ + type: 'error', + data: { message: `Failed to get sessions: ${error}` }, + }); + } } } @@ -658,7 +822,38 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'saveSessionResponse', 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 const response = await this.agentManager.saveSessionDirect( messages, @@ -674,13 +869,44 @@ export class SessionMessageHandler extends BaseMessageHandler { await this.handleGetQwenSessions(); } catch (error) { console.error('[SessionMessageHandler] Failed to save session:', error); - this.sendToWebView({ - type: 'saveSessionResponse', - data: { - success: false, - message: `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({ + type: 'saveSessionResponse', + data: { + success: false, + message: `Failed to save session: ${error}`, + }, + }); + } } } @@ -732,7 +958,38 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'qwenSessionSwitched', 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 const messages = await this.agentManager.loadSessionDirect(sessionId); @@ -751,10 +1008,41 @@ export class SessionMessageHandler extends BaseMessageHandler { await this.handleGetQwenSessions(); } catch (error) { console.error('[SessionMessageHandler] Failed to resume session:', error); - this.sendToWebView({ - type: 'error', - data: { message: `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({ + type: 'error', + data: { message: `Failed to resume session: ${error}` }, + }); + } } } } diff --git a/packages/vscode-ide-companion/src/webview/styles/SelectorTest.css b/packages/vscode-ide-companion/src/webview/styles/SelectorTest.css deleted file mode 100644 index 844af6b0..00000000 --- a/packages/vscode-ide-companion/src/webview/styles/SelectorTest.css +++ /dev/null @@ -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; -} \ No newline at end of file