diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index c288d39a..cce68a53 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -34,6 +34,9 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData }; * * Coordinates various modules and provides unified interface */ +interface AgentConnectOptions { + autoAuthenticate?: boolean; +} interface AgentSessionOptions { autoAuthenticate?: boolean; } @@ -189,12 +192,14 @@ export class QwenAgentManager { async connect( workingDir: string, cliEntryPath: string, + options?: AgentConnectOptions, ): Promise { this.currentWorkingDir = workingDir; return this.connectionHandler.connect( this.connection, workingDir, cliEntryPath, + options, ); } @@ -276,9 +281,10 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - // Prefer ACP method first; fall back to file system if it fails for any reason. try { - console.log('[QwenAgentManager] Attempting to get session list via ACP'); + console.log( + '[QwenAgentManager] Attempting to get session list via ACP method', + ); const response = await this.connection.listSessions(); console.log('[QwenAgentManager] ACP session list response:', response); @@ -288,19 +294,21 @@ export class QwenAgentManager { const res: unknown = response; let items: Array> = []; - if (Array.isArray(res)) { - items = res as Array>; - } else if (res && typeof res === 'object' && 'items' in res) { + // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC + // "result" directly (not the full AcpResponse). Treat it as unknown + // and carefully narrow before accessing `items` to satisfy strict TS. + if (res && typeof res === 'object' && 'items' in res) { const itemsValue = (res as { items?: unknown }).items; items = Array.isArray(itemsValue) ? (itemsValue as Array>) : []; } - console.log('[QwenAgentManager] Sessions retrieved via ACP:', { - count: items.length, - }); - + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + res, + items.length, + ); if (items.length > 0) { const sessions = items.map((item) => ({ id: item.sessionId || item.id, @@ -314,6 +322,11 @@ export class QwenAgentManager { filePath: item.filePath, cwd: item.cwd, })); + + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + sessions.length, + ); return sessions; } } catch (error) { @@ -376,6 +389,7 @@ export class QwenAgentManager { }> { const size = params?.size ?? 20; const cursor = params?.cursor; + try { const response = await this.connection.listSessions({ size, @@ -470,7 +484,6 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { - // Prefer reading CLI's JSONL if we can find filePath from session/list try { const list = await this.getSessionList(); const item = list.find( @@ -690,7 +703,9 @@ export class QwenAgentManager { const planText = planEntries .map( (entry: Record, index: number) => - `${index + 1}. ${entry.description || entry.title || 'Unnamed step'}`, + `${index + 1}. ${ + entry.description || entry.title || 'Unnamed step' + }`, ) .join('\n'); msgs.push({ @@ -969,13 +984,15 @@ export class QwenAgentManager { sessionId, ); - // Prefer ACP session/load first; fall back to file system on failure. try { - console.log('[QwenAgentManager] Attempting to load session via ACP'); + console.log( + '[QwenAgentManager] Attempting to load session via ACP method', + ); await this.loadSessionViaAcp(sessionId); console.log('[QwenAgentManager] Session loaded successfully via ACP'); - // After loading via ACP, we still need to get messages from file system. - // In future, we might get them directly from the ACP response. + + // After loading via ACP, we still need to get messages from file system + // In future, we might get them directly from the ACP response } catch (error) { console.warn( '[QwenAgentManager] ACP session load failed, falling back to file system method:', @@ -1094,7 +1111,7 @@ export class QwenAgentManager { // Let CLI handle authentication - it's the single source of truth await this.connection.authenticate(authMethod); // Add a slight delay to ensure auth state is settled - await new Promise((resolve) => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 300)); await this.connection.newSession(workingDir); } catch (reauthErr) { console.error( diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 28f2db0b..13bbd6aa 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -11,6 +11,8 @@ */ import type { AcpConnection } from './acpConnection.js'; +import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +import { authMethod } from '../types/acpTypes.js'; export interface QwenConnectionResult { sessionCreated: boolean; @@ -27,30 +29,153 @@ export class QwenConnectionHandler { * * @param connection - ACP connection instance * @param workingDir - Working directory - * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) + * @param cliPath - CLI path (optional, if provided will override the path in configuration) */ async connect( connection: AcpConnection, workingDir: string, cliEntryPath: string, + options?: { + autoAuthenticate?: boolean; + }, ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] šŸš€ CONNECT() CALLED - ID: ${connectId}`); - const sessionCreated = false; - const requiresAuth = false; + const autoAuthenticate = options?.autoAuthenticate ?? true; + let sessionCreated = false; + let requiresAuth = false; // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - await connection.connect(cliEntryPath, workingDir, extraArgs); + await connection.connect(cliEntryPath!, workingDir, extraArgs); - // Note: Session creation is now handled by the caller (QwenAgentManager) - // This prevents automatic session creation on every connection which was - // causing unwanted authentication prompts + // Try to restore existing session or create new session + // Note: Auto-restore on connect is disabled to avoid surprising loads + // when user opens a "New Chat" tab. Restoration is now an explicit action + // (session selector → session/load) or handled by higher-level flows. + const sessionRestored = false; + + // Create new session if unable to restore + if (!sessionRestored) { + console.log( + '[QwenAgentManager] no sessionRestored, Creating new session...', + ); + + try { + console.log( + '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', + ); + await this.newSessionWithRetry( + connection, + workingDir, + 3, + authMethod, + autoAuthenticate, + ); + console.log('[QwenAgentManager] New session created successfully'); + sessionCreated = true; + } catch (sessionError) { + const needsAuth = + autoAuthenticate === false && + isAuthenticationRequiredError(sessionError); + if (needsAuth) { + requiresAuth = true; + console.log( + '[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.', + ); + } else { + console.log( + `\nāš ļø [SESSION FAILED] newSessionWithRetry threw error\n`, + ); + console.log(`[QwenAgentManager] Error details:`, sessionError); + throw sessionError; + } + } + } else { + sessionCreated = true; + } console.log(`\n========================================`); console.log(`[QwenAgentManager] āœ… CONNECT() COMPLETED SUCCESSFULLY`); console.log(`========================================\n`); return { sessionCreated, requiresAuth }; } + + /** + * Create new session (with retry) + * + * @param connection - ACP connection instance + * @param workingDir - Working directory + * @param maxRetries - Maximum number of retries + */ + private async newSessionWithRetry( + connection: AcpConnection, + workingDir: string, + maxRetries: number, + authMethod: string, + autoAuthenticate: boolean, + ): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + console.log( + `[QwenAgentManager] Creating session (attempt ${attempt}/${maxRetries})...`, + ); + await connection.newSession(workingDir); + console.log('[QwenAgentManager] Session created successfully'); + return; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error( + `[QwenAgentManager] Session creation attempt ${attempt} failed:`, + errorMessage, + ); + + // If Qwen reports that authentication is required, try to + // authenticate on-the-fly once and retry without waiting. + const requiresAuth = isAuthenticationRequiredError(error); + if (requiresAuth) { + if (!autoAuthenticate) { + console.log( + '[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.', + ); + throw error; + } + console.log( + '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', + ); + try { + await connection.authenticate(authMethod); + // FIXME: @yiliang114 If there is no delay for a while, immediately executing + // newSession may cause the cli authorization jump to be triggered again + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); + // Retry immediately after successful auth + await connection.newSession(workingDir); + console.log( + '[QwenAgentManager] Session created successfully after auth', + ); + return; + } catch (authErr) { + console.error( + '[QwenAgentManager] Re-authentication failed:', + authErr, + ); + // Fall through to retry logic below + } + } + + if (attempt === maxRetries) { + throw new Error( + `Session creation failed after ${maxRetries} attempts: ${errorMessage}`, + ); + } + + const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); + console.log(`[QwenAgentManager] Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 87d771bc..4ab55283 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -576,6 +576,7 @@ export class WebViewProvider { const connectResult = await this.agentManager.connect( workingDir, bundledCliEntry, + options, ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true;