From b34894c8eaef6ac43cb4c92f83bd86e62be27576 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 11 Dec 2025 22:56:58 +0800 Subject: [PATCH 1/7] feat(vscode-ide-companion/auth): deduplicate concurrent authentication calls Prevent multiple simultaneous authentication flows by: - Adding static authInFlight promise tracking in AcpConnection - Implementing runExclusiveAuth method in AuthStateManager - Adding sessionCreateInFlight tracking in QwenAgentManager - Ensuring only one auth flow runs at a time across different components This prevents race conditions and duplicate login prompts when multiple components request authentication simultaneously. --- .../vscode-ide-companion/src/extension.ts | 20 +- .../src/services/acpConnection.ts | 25 +- .../src/services/authStateManager.ts | 112 +++-- .../src/services/qwenAgentManager.ts | 213 +++++---- .../vscode-ide-companion/src/utils/logger.ts | 42 ++ .../vscode-ide-companion/src/webview/App.tsx | 24 +- .../src/webview/WebViewProvider.ts | 407 ++++++++++-------- .../webview/handlers/SessionMessageHandler.ts | 35 ++ .../src/webview/hooks/useWebViewMessages.ts | 57 ++- .../src/webview/styles/styles.css | 1 - .../src/webview/utils/logger.ts | 25 ++ .../src/webview/utils/simpleDiff.ts | 92 ---- 12 files changed, 589 insertions(+), 464 deletions(-) create mode 100644 packages/vscode-ide-companion/src/webview/utils/logger.ts delete mode 100644 packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 2adfaef1..08979099 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -8,7 +8,11 @@ import * as vscode from 'vscode'; import { IDEServer } from './ide-server.js'; import semver from 'semver'; import { DiffContentProvider, DiffManager } from './diff-manager.js'; -import { createLogger } from './utils/logger.js'; +import { + createLogger, + getConsoleLogger, + initSharedConsoleLogger, +} from './utils/logger.js'; import { detectIdeFromEnv, IDE_DEFINITIONS, @@ -105,6 +109,8 @@ async function checkForUpdates( export async function activate(context: vscode.ExtensionContext) { logger = vscode.window.createOutputChannel('Qwen Code Companion'); + initSharedConsoleLogger(context); + const consoleLog = getConsoleLogger(); log = createLogger(context, logger); log('Extension activated'); @@ -142,18 +148,18 @@ export async function activate(context: vscode.ExtensionContext) { webviewPanel: vscode.WebviewPanel, state: unknown, ) { - console.log( + consoleLog( '[Extension] Deserializing WebView panel with state:', state, ); // Create a new provider for the restored panel const provider = createWebViewProvider(); - console.log('[Extension] Provider created for deserialization'); + consoleLog('[Extension] Provider created for deserialization'); // Restore state if available BEFORE restoring the panel if (state && typeof state === 'object') { - console.log('[Extension] Restoring state:', state); + consoleLog('[Extension] Restoring state:', state); provider.restoreState( state as { conversationId: string | null; @@ -161,11 +167,11 @@ export async function activate(context: vscode.ExtensionContext) { }, ); } else { - console.log('[Extension] No state to restore or invalid state'); + consoleLog('[Extension] No state to restore or invalid state'); } await provider.restorePanel(webviewPanel); - console.log('[Extension] Panel restore completed'); + consoleLog('[Extension] Panel restore completed'); log('WebView panel restored from serialization'); }, @@ -206,7 +212,6 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-allow on diff.accept failed:', err); } - console.log('[Extension] Diff accepted'); }), vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; @@ -223,7 +228,6 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-reject on diff.cancel failed:', err); } - console.log('[Extension] Diff cancelled'); })), vscode.commands.registerCommand('qwen.diff.closeAll', async () => { try { diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 5486e14d..4dd35b71 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -31,6 +31,8 @@ export class AcpConnection { private child: ChildProcess | null = null; private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; + // Deduplicate concurrent authenticate calls (across retry paths) + private static authInFlight: Promise | null = null; // Remember the working dir provided at connect() so later ACP calls // that require cwd (e.g. session/list) can include it. private workingDir: string = process.cwd(); @@ -271,12 +273,23 @@ export class AcpConnection { * @returns Authentication response */ async authenticate(methodId?: string): Promise { - return this.sessionManager.authenticate( - methodId, - this.child, - this.pendingRequests, - this.nextRequestId, - ); + if (AcpConnection.authInFlight) { + return AcpConnection.authInFlight; + } + + const p = this.sessionManager + .authenticate( + methodId, + this.child, + this.pendingRequests, + this.nextRequestId, + ) + .finally(() => { + AcpConnection.authInFlight = null; + }); + + AcpConnection.authInFlight = p; + return p; } /** diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts index 566a4afb..aa75fe36 100644 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ b/packages/vscode-ide-companion/src/services/authStateManager.ts @@ -5,6 +5,7 @@ */ import type * as vscode from 'vscode'; +import { createConsoleLogger, getConsoleLogger } from '../utils/logger.js'; interface AuthState { isAuthenticated: boolean; @@ -21,6 +22,9 @@ export class AuthStateManager { private static context: vscode.ExtensionContext | null = null; private static readonly AUTH_STATE_KEY = 'qwen.authState'; private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours + private static consoleLog: (...args: unknown[]) => void = getConsoleLogger(); + // Deduplicate concurrent auth flows (e.g., multiple tabs prompting login) + private static authFlowInFlight: Promise | null = null; private constructor() {} /** @@ -34,11 +38,37 @@ export class AuthStateManager { // If a context is provided, update the static context if (context) { AuthStateManager.context = context; + AuthStateManager.consoleLog = createConsoleLogger( + context, + 'AuthStateManager', + ); } return AuthStateManager.instance; } + /** + * Run an auth-related flow exclusively. If another flow is already running, + * return the same promise to prevent duplicate login prompts. + */ + static runExclusiveAuth(task: () => Promise): Promise { + if (AuthStateManager.authFlowInFlight) { + return AuthStateManager.authFlowInFlight as Promise; + } + + const p = Promise.resolve() + .then(task) + .finally(() => { + // Clear only if this promise is still the active one + if (AuthStateManager.authFlowInFlight === p) { + AuthStateManager.authFlowInFlight = null; + } + }); + + AuthStateManager.authFlowInFlight = p; + return p as Promise; + } + /** * Check if there's a valid cached authentication */ @@ -46,17 +76,19 @@ export class AuthStateManager { const state = await this.getAuthState(); if (!state) { - console.log('[AuthStateManager] No cached auth state found'); + AuthStateManager.consoleLog( + '[AuthStateManager] No cached auth state found', + ); return false; } - console.log('[AuthStateManager] Found cached auth state:', { + AuthStateManager.consoleLog('[AuthStateManager] Found cached auth state:', { workingDir: state.workingDir, authMethod: state.authMethod, timestamp: new Date(state.timestamp).toISOString(), isAuthenticated: state.isAuthenticated, }); - console.log('[AuthStateManager] Checking against:', { + AuthStateManager.consoleLog('[AuthStateManager] Checking against:', { workingDir, authMethod, }); @@ -67,8 +99,8 @@ export class AuthStateManager { now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; if (isExpired) { - console.log('[AuthStateManager] Cached auth expired'); - console.log( + AuthStateManager.consoleLog('[AuthStateManager] Cached auth expired'); + AuthStateManager.consoleLog( '[AuthStateManager] Cache age:', Math.floor((now - state.timestamp) / 1000 / 60), 'minutes', @@ -82,15 +114,29 @@ export class AuthStateManager { state.workingDir === workingDir && state.authMethod === authMethod; if (!isSameContext) { - console.log('[AuthStateManager] Working dir or auth method changed'); - console.log('[AuthStateManager] Cached workingDir:', state.workingDir); - console.log('[AuthStateManager] Current workingDir:', workingDir); - console.log('[AuthStateManager] Cached authMethod:', state.authMethod); - console.log('[AuthStateManager] Current authMethod:', authMethod); + AuthStateManager.consoleLog( + '[AuthStateManager] Working dir or auth method changed', + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Cached workingDir:', + state.workingDir, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Current workingDir:', + workingDir, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Cached authMethod:', + state.authMethod, + ); + AuthStateManager.consoleLog( + '[AuthStateManager] Current authMethod:', + authMethod, + ); return false; } - console.log('[AuthStateManager] Valid cached auth found'); + AuthStateManager.consoleLog('[AuthStateManager] Valid cached auth found'); return state.isAuthenticated; } @@ -100,7 +146,10 @@ export class AuthStateManager { */ async debugAuthState(): Promise { const state = await this.getAuthState(); - console.log('[AuthStateManager] DEBUG - Current auth state:', state); + AuthStateManager.consoleLog( + '[AuthStateManager] DEBUG - Current auth state:', + state, + ); if (state) { const now = Date.now(); @@ -108,9 +157,16 @@ export class AuthStateManager { 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.consoleLog( + '[AuthStateManager] DEBUG - Auth state age:', + age, + 'minutes', + ); + AuthStateManager.consoleLog( + '[AuthStateManager] DEBUG - Auth state expired:', + isExpired, + ); + AuthStateManager.consoleLog( '[AuthStateManager] DEBUG - Auth state valid:', state.isAuthenticated, ); @@ -135,7 +191,7 @@ export class AuthStateManager { timestamp: Date.now(), }; - console.log('[AuthStateManager] Saving auth state:', { + AuthStateManager.consoleLog('[AuthStateManager] Saving auth state:', { workingDir, authMethod, timestamp: new Date(state.timestamp).toISOString(), @@ -145,11 +201,14 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, state, ); - console.log('[AuthStateManager] Auth state saved'); + AuthStateManager.consoleLog('[AuthStateManager] Auth state saved'); // Verify the state was saved correctly const savedState = await this.getAuthState(); - console.log('[AuthStateManager] Verified saved state:', savedState); + AuthStateManager.consoleLog( + '[AuthStateManager] Verified saved state:', + savedState, + ); } /** @@ -163,9 +222,9 @@ export class AuthStateManager { ); } - console.log('[AuthStateManager] Clearing auth state'); + AuthStateManager.consoleLog('[AuthStateManager] Clearing auth state'); const currentState = await this.getAuthState(); - console.log( + AuthStateManager.consoleLog( '[AuthStateManager] Current state before clearing:', currentState, ); @@ -174,11 +233,14 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, undefined, ); - console.log('[AuthStateManager] Auth state cleared'); + AuthStateManager.consoleLog('[AuthStateManager] Auth state cleared'); // Verify the state was cleared const newState = await this.getAuthState(); - console.log('[AuthStateManager] State after clearing:', newState); + AuthStateManager.consoleLog( + '[AuthStateManager] State after clearing:', + newState, + ); } /** @@ -187,17 +249,15 @@ export class AuthStateManager { private async getAuthState(): Promise { // Ensure we have a valid context if (!AuthStateManager.context) { - console.log( + AuthStateManager.consoleLog( '[AuthStateManager] No context available for getting auth state', ); return undefined; } - const a = AuthStateManager.context.globalState.get( + return AuthStateManager.context.globalState.get( AuthStateManager.AUTH_STATE_KEY, ); - console.log('[AuthStateManager] Auth state:', a); - return a; } /** diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index a57d15b7..b954aed8 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -23,6 +23,7 @@ import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { getConsoleLogger } from '../utils/logger.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -45,11 +46,15 @@ export class QwenAgentManager { // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) // can reuse it and avoid forcing a fresh authentication unnecessarily. private defaultAuthStateManager?: AuthStateManager; + // Deduplicate concurrent session/new attempts + private sessionCreateInFlight: Promise | null = null; // Callback storage private callbacks: QwenAgentCallbacks = {}; + private consoleLog: (...args: unknown[]) => void; - constructor() { + constructor(consoleLogger = getConsoleLogger()) { + this.consoleLog = consoleLogger; this.connection = new AcpConnection(); this.sessionReader = new QwenSessionReader(); this.sessionManager = new QwenSessionManager(); @@ -76,7 +81,7 @@ export class QwenAgentManager { ).update; const text = update?.content?.text || ''; if (update?.sessionUpdate === 'user_message_chunk' && text) { - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: routing user message chunk', ); this.callbacks.onMessage?.({ @@ -87,7 +92,7 @@ export class QwenAgentManager { return; } if (update?.sessionUpdate === 'agent_message_chunk' && text) { - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: routing agent message chunk', ); this.callbacks.onMessage?.({ @@ -98,7 +103,7 @@ export class QwenAgentManager { return; } // For other types during rehydration, fall through to normal handler - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration: non-text update, forwarding to handler', ); } @@ -257,7 +262,7 @@ export class QwenAgentManager { * @returns Session list */ async getSessionList(): Promise>> { - console.log( + this.consoleLog( '[QwenAgentManager] Getting session list with version-aware strategy', ); @@ -265,7 +270,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionList = cliContextManager.supportsSessionList(); - console.log( + this.consoleLog( '[QwenAgentManager] CLI supports session/list:', supportsSessionList, ); @@ -273,11 +278,14 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionList) { try { - console.log( + this.consoleLog( '[QwenAgentManager] Attempting to get session list via ACP method', ); const response = await this.connection.listSessions(); - console.log('[QwenAgentManager] ACP session list response:', response); + this.consoleLog( + '[QwenAgentManager] ACP session list response:', + response, + ); // sendRequest resolves with the JSON-RPC "result" directly // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } @@ -295,7 +303,7 @@ export class QwenAgentManager { : []; } - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved via ACP:', res, items.length, @@ -314,7 +322,7 @@ export class QwenAgentManager { cwd: item.cwd, })); - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved via ACP:', sessions.length, ); @@ -330,9 +338,11 @@ export class QwenAgentManager { // Always fall back to file system method try { - console.log('[QwenAgentManager] Getting session list from file system'); + this.consoleLog( + '[QwenAgentManager] Getting session list from file system', + ); const sessions = await this.sessionReader.getAllSessions(undefined, true); - console.log( + this.consoleLog( '[QwenAgentManager] Session list from file system (all projects):', sessions.length, ); @@ -350,7 +360,7 @@ export class QwenAgentManager { }), ); - console.log( + this.consoleLog( '[QwenAgentManager] Sessions retrieved from file system:', result.length, ); @@ -490,7 +500,7 @@ export class QwenAgentManager { const item = list.find( (s) => s.sessionId === sessionId || s.id === sessionId, ); - console.log( + this.consoleLog( '[QwenAgentManager] Session list item for filePath lookup:', item, ); @@ -561,7 +571,7 @@ export class QwenAgentManager { } } // Simple linear reconstruction: filter user/assistant and sort by timestamp - console.log( + this.consoleLog( '[QwenAgentManager] JSONL records read:', records.length, filePath, @@ -718,7 +728,7 @@ export class QwenAgentManager { // Handle other types if needed } - console.log( + this.consoleLog( '[QwenAgentManager] JSONL messages reconstructed:', msgs.length, ); @@ -856,7 +866,7 @@ export class QwenAgentManager { tag: string, ): Promise<{ success: boolean; message?: string }> { try { - console.log( + this.consoleLog( '[QwenAgentManager] Saving session via /chat save command:', sessionId, 'with tag:', @@ -867,7 +877,9 @@ export class QwenAgentManager { // The CLI will handle this as a special command await this.connection.sendPrompt(`/chat save "${tag}"`); - console.log('[QwenAgentManager] /chat save command sent successfully'); + this.consoleLog( + '[QwenAgentManager] /chat save command sent successfully', + ); return { success: true, message: `Session saved with tag: ${tag}`, @@ -914,14 +926,14 @@ export class QwenAgentManager { conversationId: string, ): Promise<{ success: boolean; tag?: string; message?: string }> { try { - console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - console.log('[QwenAgentManager] Conversation ID:', conversationId); - console.log('[QwenAgentManager] Message count:', messages.length); - console.log( + this.consoleLog('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); + this.consoleLog('[QwenAgentManager] Conversation ID:', conversationId); + this.consoleLog('[QwenAgentManager] Message count:', messages.length); + this.consoleLog( '[QwenAgentManager] Current working dir:', this.currentWorkingDir, ); - console.log( + this.consoleLog( '[QwenAgentManager] Current session ID (from CLI):', this.currentSessionId, ); @@ -998,11 +1010,11 @@ export class QwenAgentManager { try { // Route upcoming session/update messages as discrete messages for replay this.rehydratingSessionId = sessionId; - console.log( + this.consoleLog( '[QwenAgentManager] Rehydration start for session:', sessionId, ); - console.log( + this.consoleLog( '[QwenAgentManager] Attempting session/load via ACP for session:', sessionId, ); @@ -1010,7 +1022,7 @@ export class QwenAgentManager { sessionId, cwdOverride, ); - console.log( + this.consoleLog( '[QwenAgentManager] Session load succeeded. Response:', JSON.stringify(response).substring(0, 200), ); @@ -1050,7 +1062,10 @@ export class QwenAgentManager { throw error; } finally { // End rehydration routing regardless of outcome - console.log('[QwenAgentManager] Rehydration end for session:', sessionId); + this.consoleLog( + '[QwenAgentManager] Rehydration end for session:', + sessionId, + ); this.rehydratingSessionId = null; } } @@ -1063,7 +1078,7 @@ export class QwenAgentManager { * @returns Loaded session messages or null */ async loadSession(sessionId: string): Promise { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session with version-aware strategy:', sessionId, ); @@ -1072,7 +1087,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - console.log( + this.consoleLog( '[QwenAgentManager] CLI supports session/load:', supportsSessionLoad, ); @@ -1080,11 +1095,13 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionLoad) { try { - console.log( + this.consoleLog( '[QwenAgentManager] Attempting to load session via ACP method', ); await this.loadSessionViaAcp(sessionId); - console.log('[QwenAgentManager] Session loaded successfully via ACP'); + this.consoleLog( + '[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 @@ -1098,11 +1115,11 @@ export class QwenAgentManager { // Always fall back to file system method try { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session messages from file system', ); const messages = await this.loadSessionMessagesFromFile(sessionId); - console.log( + this.consoleLog( '[QwenAgentManager] Session messages loaded successfully from file system', ); return messages; @@ -1125,7 +1142,7 @@ export class QwenAgentManager { sessionId: string, ): Promise { try { - console.log( + this.consoleLog( '[QwenAgentManager] Loading session from file system:', sessionId, ); @@ -1137,7 +1154,7 @@ export class QwenAgentManager { ); if (!session) { - console.log( + this.consoleLog( '[QwenAgentManager] Session not found in file system:', sessionId, ); @@ -1183,93 +1200,67 @@ export class QwenAgentManager { workingDir: string, authStateManager?: AuthStateManager, ): Promise { - console.log('[QwenAgentManager] Creating new session...'); + // Reuse existing session if present + if (this.connection.currentSessionId) { + return this.connection.currentSessionId; + } + // Deduplicate concurrent session/new attempts + if (this.sessionCreateInFlight) { + return this.sessionCreateInFlight; + } - // Check if we have valid cached authentication - let hasValidAuth = false; + this.consoleLog('[QwenAgentManager] Creating new session...'); // Prefer the provided authStateManager, otherwise fall back to the one // remembered during connect(). This prevents accidental re-auth in // fallback paths (e.g. session switching) when the handler didn't pass it. const effectiveAuth = authStateManager || this.defaultAuthStateManager; - if (effectiveAuth) { - hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod); - console.log( - '[QwenAgentManager] Has valid cached auth for new session:', - hasValidAuth, - ); - } - // Only authenticate if we don't have valid cached auth - if (!hasValidAuth) { - console.log( - '[QwenAgentManager] Authenticating before creating session...', - ); + this.sessionCreateInFlight = (async () => { try { - await this.connection.authenticate(authMethod); - console.log('[QwenAgentManager] Authentication successful'); - - // Save auth state - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Saving auth state after successful authentication', - ); - await effectiveAuth.saveAuthState(workingDir, authMethod); - } - } catch (authError) { - console.error('[QwenAgentManager] Authentication failed:', authError); - // Clear potentially invalid cache - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Clearing auth cache due to authentication failure', - ); - await effectiveAuth.clearAuthState(); - } - throw authError; - } - } else { - console.log( - '[QwenAgentManager] Skipping authentication - using valid cached auth', - ); - } - - // Try to create a new ACP session. If Qwen asks for auth despite our - // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. - try { - await this.connection.newSession(workingDir); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - const requiresAuth = - msg.includes('Authentication required') || - msg.includes('(code: -32000)'); - - if (requiresAuth) { - console.warn( - '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', - ); + // Try to create a new ACP session. If Qwen asks for auth despite our + // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. try { - await this.connection.authenticate(authMethod); - // Persist auth cache so subsequent calls can skip the web flow. - if (effectiveAuth) { - await effectiveAuth.saveAuthState(workingDir, authMethod); - } await this.connection.newSession(workingDir); - } catch (reauthErr) { - // Clear potentially stale cache on failure and rethrow - if (effectiveAuth) { - await effectiveAuth.clearAuthState(); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const requiresAuth = + msg.includes('Authentication required') || + msg.includes('(code: -32000)'); + + if (requiresAuth) { + console.warn( + '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', + ); + try { + await this.connection.authenticate(authMethod); + // Persist auth cache so subsequent calls can skip the web flow. + if (effectiveAuth) { + await effectiveAuth.saveAuthState(workingDir, authMethod); + } + await this.connection.newSession(workingDir); + } catch (reauthErr) { + // Clear potentially stale cache on failure and rethrow + if (effectiveAuth) { + await effectiveAuth.clearAuthState(); + } + throw reauthErr; + } + } else { + throw err; } - throw reauthErr; } - } else { - throw err; + const newSessionId = this.connection.currentSessionId; + this.consoleLog( + '[QwenAgentManager] New session created with ID:', + newSessionId, + ); + return newSessionId; + } finally { + this.sessionCreateInFlight = null; } - } - const newSessionId = this.connection.currentSessionId; - console.log( - '[QwenAgentManager] New session created with ID:', - newSessionId, - ); - return newSessionId; + })(); + + return this.sessionCreateInFlight; } /** @@ -1285,7 +1276,7 @@ export class QwenAgentManager { * Cancel current prompt */ async cancelCurrentPrompt(): Promise { - console.log('[QwenAgentManager] Cancelling current prompt'); + this.consoleLog('[QwenAgentManager] Cancelling current prompt'); await this.connection.cancelSession(); } diff --git a/packages/vscode-ide-companion/src/utils/logger.ts b/packages/vscode-ide-companion/src/utils/logger.ts index b3f8ad1e..f92cfe8c 100644 --- a/packages/vscode-ide-companion/src/utils/logger.ts +++ b/packages/vscode-ide-companion/src/utils/logger.ts @@ -6,6 +6,11 @@ import * as vscode from 'vscode'; +type ConsoleLogger = (...args: unknown[]) => void; + +// Shared console logger instance, initialized during extension activation. +let sharedConsoleLogger: ConsoleLogger = () => {}; + export function createLogger( context: vscode.ExtensionContext, logger: vscode.OutputChannel, @@ -16,3 +21,40 @@ export function createLogger( } }; } + +/** + * Creates a dev-only logger that writes to the VS Code console (Developer Tools). + */ +export function createConsoleLogger( + context: vscode.ExtensionContext, + scope?: string, +): ConsoleLogger { + return (...args: unknown[]) => { + if (context.extensionMode !== vscode.ExtensionMode.Development) { + return; + } + if (scope) { + console.log(`[${scope}]`, ...args); + return; + } + console.log(...args); + }; +} + +/** + * Initialize the shared console logger so other modules can import it without + * threading the extension context everywhere. + */ +export function initSharedConsoleLogger( + context: vscode.ExtensionContext, + scope?: string, +) { + sharedConsoleLogger = createConsoleLogger(context, scope); +} + +/** + * Get the shared console logger (no-op until initialized). + */ +export function getConsoleLogger(): ConsoleLogger { + return sharedConsoleLogger; +} diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 4b51d6b6..65d978fd 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -45,9 +45,11 @@ import { FileIcon, UserIcon } from './components/icons/index.js'; import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js'; import type { ApprovalModeValue } from '../types/acpTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; +import { createWebviewConsoleLogger } from './utils/logger.js'; export const App: React.FC = () => { const vscode = useVSCode(); + const consoleLog = useMemo(() => createWebviewConsoleLogger('App'), []); // Core hooks const sessionManagement = useSessionManagement(vscode); @@ -167,7 +169,7 @@ export const App: React.FC = () => { }, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); // Message submission - const handleSubmit = useMessageSubmit({ + const { handleSubmit: submitMessage } = useMessageSubmit({ inputText, setInputText, messageHandling, @@ -487,6 +489,22 @@ export const App: React.FC = () => { setThinkingEnabled((prev) => !prev); }; + // When user sends a message after scrolling up, re-pin and jump to the bottom + const handleSubmitWithScroll = useCallback( + (e: React.FormEvent) => { + setPinnedToBottom(true); + + const container = messagesContainerRef.current; + if (container) { + const top = container.scrollHeight - container.clientHeight; + container.scrollTo({ top }); + } + + submitMessage(e); + }, + [submitMessage], + ); + // Create unified message array containing all types of messages and tool calls const allMessages = useMemo< Array<{ @@ -524,7 +542,7 @@ export const App: React.FC = () => { ); }, [messageHandling.messages, inProgressToolCalls, completedToolCalls]); - console.log('[App] Rendering messages:', allMessages); + consoleLog('[App] Rendering messages:', allMessages); // Render all messages and tool calls const renderMessages = useCallback<() => React.ReactNode>( @@ -686,7 +704,7 @@ export const App: React.FC = () => { onCompositionStart={() => setIsComposing(true)} onCompositionEnd={() => setIsComposing(false)} onKeyDown={() => {}} - onSubmit={handleSubmit.handleSubmit} + onSubmit={handleSubmitWithScroll} onCancel={handleCancel} onToggleEditMode={handleToggleEditMode} onToggleThinking={handleToggleThinking} diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 82629787..11054259 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -16,6 +16,7 @@ import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; +import { createConsoleLogger } from '../utils/logger.js'; export class WebViewProvider { private panelManager: PanelManager; @@ -32,12 +33,15 @@ export class WebViewProvider { private pendingPermissionResolve: ((optionId: string) => void) | null = null; // Track current ACP mode id to influence permission/diff behavior private currentModeId: ApprovalModeValue | null = null; + private consoleLog: (...args: unknown[]) => void; constructor( context: vscode.ExtensionContext, private extensionUri: vscode.Uri, ) { - this.agentManager = new QwenAgentManager(); + const agentConsoleLogger = createConsoleLogger(context, 'QwenAgentManager'); + this.consoleLog = createConsoleLogger(context, 'WebViewProvider'); + this.agentManager = new QwenAgentManager(agentConsoleLogger); this.conversationStore = new ConversationStore(context); this.authStateManager = AuthStateManager.getInstance(context); this.panelManager = new PanelManager(extensionUri, () => { @@ -380,7 +384,7 @@ export class WebViewProvider { // Set up state serialization newPanel.onDidChangeViewState(() => { - console.log( + this.consoleLog( '[WebViewProvider] Panel view state changed, triggering serialization check', ); }); @@ -510,7 +514,7 @@ export class WebViewProvider { } // Attempt to restore authentication state and initialize connection - console.log( + this.consoleLog( '[WebViewProvider] Attempting to restore auth state and connection...', ); await this.attemptAuthStateRestoration(); @@ -532,23 +536,26 @@ export class WebViewProvider { workingDir, authMethod, ); - console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth); + this.consoleLog( + '[WebViewProvider] Has valid cached auth:', + hasValidAuth, + ); if (hasValidAuth) { - console.log( + this.consoleLog( '[WebViewProvider] Valid auth found, attempting connection...', ); // Try to connect with cached auth await this.initializeAgentConnection(); } else { - console.log( + this.consoleLog( '[WebViewProvider] No valid auth found, rendering empty conversation', ); // Render the chat UI immediately without connecting await this.initializeEmptyConversation(); } } else { - console.log( + this.consoleLog( '[WebViewProvider] No auth state manager, rendering empty conversation', ); await this.initializeEmptyConversation(); @@ -565,84 +572,101 @@ export class WebViewProvider { * Can be called from show() or via /login command */ async initializeAgentConnection(): Promise { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - - console.log( - '[WebViewProvider] Starting initialization, workingDir:', - workingDir, - ); - console.log( - '[WebViewProvider] AuthStateManager available:', - !!this.authStateManager, + return AuthStateManager.runExclusiveAuth(() => + this.doInitializeAgentConnection(), ); + } - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); + /** + * Internal: perform actual connection/initialization (no auth locking). + */ + private async doInitializeAgentConnection(): Promise { + const run = async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - if (!cliDetection.isInstalled) { - console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + this.consoleLog( + '[WebViewProvider] Starting initialization, workingDir:', + workingDir, ); - console.log('[WebViewProvider] CLI detection error:', cliDetection.error); - - // Show VSCode notification with installation option - await CliInstaller.promptInstallation(); - - // Initialize empty conversation (can still browse history) - await this.initializeEmptyConversation(); - } else { - console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', + this.consoleLog( + '[WebViewProvider] AuthStateManager available:', + !!this.authStateManager, ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); - try { - console.log('[WebViewProvider] Connecting to agent...'); - console.log( - '[WebViewProvider] Using authStateManager:', - !!this.authStateManager, + // Check if CLI is installed before attempting to connect + const cliDetection = await CliDetector.detectQwenCli(); + + if (!cliDetection.isInstalled) { + this.consoleLog( + '[WebViewProvider] Qwen CLI not detected, skipping agent connection', ); - const authInfo = await this.authStateManager.getAuthInfo(); - console.log('[WebViewProvider] Auth cache status:', authInfo); - - // Pass the detected CLI path to ensure we use the correct installation - await this.agentManager.connect( - workingDir, - this.authStateManager, - cliDetection.cliPath, + this.consoleLog( + '[WebViewProvider] CLI detection error:', + cliDetection.error, ); - console.log('[WebViewProvider] Agent connected successfully'); - this.agentInitialized = true; - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); + // Show VSCode notification with installation option + await CliInstaller.promptInstallation(); - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); - } catch (_error) { - console.error('[WebViewProvider] Agent connection error:', _error); - // Clear auth cache on error (might be auth issue) - await this.authStateManager.clearAuthState(); - vscode.window.showWarningMessage( - `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, - ); - // Fallback to empty conversation + // Initialize empty conversation (can still browse history) await this.initializeEmptyConversation(); + } else { + this.consoleLog( + '[WebViewProvider] Qwen CLI detected, attempting connection...', + ); + this.consoleLog('[WebViewProvider] CLI path:', cliDetection.cliPath); + this.consoleLog('[WebViewProvider] CLI version:', cliDetection.version); - // Notify webview that agent connection failed - this.sendMessageToWebView({ - type: 'agentConnectionError', - data: { - message: _error instanceof Error ? _error.message : String(_error), - }, - }); + try { + this.consoleLog('[WebViewProvider] Connecting to agent...'); + this.consoleLog( + '[WebViewProvider] Using authStateManager:', + !!this.authStateManager, + ); + const authInfo = await this.authStateManager.getAuthInfo(); + this.consoleLog('[WebViewProvider] Auth cache status:', authInfo); + + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect( + workingDir, + this.authStateManager, + cliDetection.cliPath, + ); + this.consoleLog('[WebViewProvider] Agent connected successfully'); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } catch (_error) { + console.error('[WebViewProvider] Agent connection error:', _error); + // Clear auth cache on error (might be auth issue) + await this.authStateManager.clearAuthState(); + vscode.window.showWarningMessage( + `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, + ); + // Fallback to empty conversation + await this.initializeEmptyConversation(); + + // Notify webview that agent connection failed + this.sendMessageToWebView({ + type: 'agentConnectionError', + data: { + message: + _error instanceof Error ? _error.message : String(_error), + }, + }); + } } - } + }; + + return run(); } /** @@ -650,86 +674,97 @@ export class WebViewProvider { * Called when user explicitly uses /login command */ async forceReLogin(): Promise { - console.log('[WebViewProvider] Force re-login requested'); - console.log( + this.consoleLog('[WebViewProvider] Force re-login requested'); + this.consoleLog( '[WebViewProvider] Current authStateManager:', !!this.authStateManager, ); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Logging in to Qwen Code... ', - cancellable: false, - }, - async (progress) => { - try { - progress.report({ message: 'Preparing sign-in...' }); + // If a login/connection flow is already running, reuse it to avoid double prompts + const p = Promise.resolve( + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + }, + async (progress) => { + try { + progress.report({ message: 'Preparing sign-in...' }); - // Clear existing auth cache - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - console.log('[WebViewProvider] Auth cache cleared'); - } else { - console.log('[WebViewProvider] No authStateManager to clear'); - } - - // Disconnect existing connection if any - if (this.agentInitialized) { - try { - this.agentManager.disconnect(); - console.log('[WebViewProvider] Existing connection disconnected'); - } catch (_error) { - console.log('[WebViewProvider] Error disconnecting:', _error); + // Clear existing auth cache + if (this.authStateManager) { + await this.authStateManager.clearAuthState(); + this.consoleLog('[WebViewProvider] Auth cache cleared'); + } else { + this.consoleLog('[WebViewProvider] No authStateManager to clear'); } - this.agentInitialized = false; + + // Disconnect existing connection if any + if (this.agentInitialized) { + try { + this.agentManager.disconnect(); + this.consoleLog( + '[WebViewProvider] Existing connection disconnected', + ); + } catch (_error) { + this.consoleLog( + '[WebViewProvider] Error disconnecting:', + _error, + ); + } + this.agentInitialized = false; + } + + // Wait a moment for cleanup to complete + await new Promise((resolve) => setTimeout(resolve, 300)); + + progress.report({ + message: 'Connecting to CLI and starting sign-in...', + }); + + // Reinitialize connection (will trigger fresh authentication) + await this.doInitializeAgentConnection(); + this.consoleLog( + '[WebViewProvider] Force re-login completed successfully', + ); + + // Ensure auth state is saved after successful re-login + if (this.authStateManager) { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + await this.authStateManager.saveAuthState(workingDir, authMethod); + this.consoleLog( + '[WebViewProvider] Auth state saved after re-login', + ); + } + + // Send success notification to WebView + this.sendMessageToWebView({ + type: 'loginSuccess', + data: { message: 'Successfully logged in!' }, + }); + } catch (_error) { + console.error('[WebViewProvider] Force re-login failed:', _error); + console.error( + '[WebViewProvider] Error stack:', + _error instanceof Error ? _error.stack : 'N/A', + ); + + // Send error notification to WebView + this.sendMessageToWebView({ + type: 'loginError', + data: { + message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, + }, + }); + + throw _error; } - - // Wait a moment for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 300)); - - progress.report({ - message: 'Connecting to CLI and starting sign-in...', - }); - - // Reinitialize connection (will trigger fresh authentication) - await this.initializeAgentConnection(); - console.log( - '[WebViewProvider] Force re-login completed successfully', - ); - - // Ensure auth state is saved after successful re-login - if (this.authStateManager) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log('[WebViewProvider] Auth state saved after re-login'); - } - - // Send success notification to WebView - this.sendMessageToWebView({ - type: 'loginSuccess', - data: { message: 'Successfully logged in!' }, - }); - } catch (_error) { - console.error('[WebViewProvider] Force re-login failed:', _error); - console.error( - '[WebViewProvider] Error stack:', - _error instanceof Error ? _error.stack : 'N/A', - ); - - // Send error notification to WebView - this.sendMessageToWebView({ - type: 'loginError', - data: { - message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, - }, - }); - - throw _error; - } - }, + }, + ), ); + + return AuthStateManager.runExclusiveAuth(() => p); } /** @@ -737,15 +772,15 @@ export class WebViewProvider { * Called when restoring WebView after VSCode restart */ async refreshConnection(): Promise { - console.log('[WebViewProvider] Refresh connection requested'); + this.consoleLog('[WebViewProvider] Refresh connection requested'); // Disconnect existing connection if any if (this.agentInitialized) { try { this.agentManager.disconnect(); - console.log('[WebViewProvider] Existing connection disconnected'); + this.consoleLog('[WebViewProvider] Existing connection disconnected'); } catch (_error) { - console.log('[WebViewProvider] Error disconnecting:', _error); + this.consoleLog('[WebViewProvider] Error disconnecting:', _error); } this.agentInitialized = false; } @@ -756,7 +791,7 @@ export class WebViewProvider { // Reinitialize connection (will use cached auth if available) try { await this.initializeAgentConnection(); - console.log( + this.consoleLog( '[WebViewProvider] Connection refresh completed successfully', ); @@ -786,35 +821,41 @@ export class WebViewProvider { */ private async loadCurrentSessionMessages(): Promise { try { - console.log( + this.consoleLog( '[WebViewProvider] Initializing with new session (skipping restoration)', ); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - // Skip session restoration entirely and create a new session directly - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); + // avoid creating another session if connect() already created one. + if (!this.agentManager.currentSessionId) { + try { + await this.agentManager.createNewSession( + workingDir, + this.authStateManager, + ); + this.consoleLog('[WebViewProvider] ACP session created successfully'); - // Ensure auth state is saved after successful session creation - if (this.authStateManager) { - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log( - '[WebViewProvider] Auth state saved after session creation', + // Ensure auth state is saved after successful session creation + if (this.authStateManager) { + await this.authStateManager.saveAuthState(workingDir, authMethod); + this.consoleLog( + '[WebViewProvider] Auth state saved after session creation', + ); + } + } 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.`, ); } - } 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 { + this.consoleLog( + '[WebViewProvider] Existing ACP session detected, skipping new session creation', ); } @@ -837,14 +878,14 @@ export class WebViewProvider { */ private async initializeEmptyConversation(): Promise { try { - console.log('[WebViewProvider] Initializing empty conversation'); + this.consoleLog('[WebViewProvider] Initializing empty conversation'); const newConv = await this.conversationStore.createConversation(); this.messageHandler.setCurrentConversationId(newConv.id); this.sendMessageToWebView({ type: 'conversationLoaded', data: newConv, }); - console.log( + this.consoleLog( '[WebViewProvider] Empty conversation initialized:', this.messageHandler.getCurrentConversationId(), ); @@ -968,7 +1009,7 @@ export class WebViewProvider { * Call this when auth cache is cleared to force re-authentication */ resetAgentState(): void { - console.log('[WebViewProvider] Resetting agent state'); + this.consoleLog('[WebViewProvider] Resetting agent state'); this.agentInitialized = false; // Disconnect existing connection this.agentManager.disconnect(); @@ -978,7 +1019,7 @@ export class WebViewProvider { * Clear authentication cache for this WebViewProvider instance */ async clearAuthCache(): Promise { - console.log('[WebViewProvider] Clearing auth cache for this instance'); + this.consoleLog('[WebViewProvider] Clearing auth cache for this instance'); if (this.authStateManager) { await this.authStateManager.clearAuthState(); this.resetAgentState(); @@ -990,8 +1031,8 @@ export class WebViewProvider { * This sets up the panel with all event listeners */ async restorePanel(panel: vscode.WebviewPanel): Promise { - console.log('[WebViewProvider] Restoring WebView panel'); - console.log( + this.consoleLog('[WebViewProvider] Restoring WebView panel'); + this.consoleLog( '[WebViewProvider] Current authStateManager in restore:', !!this.authStateManager, ); @@ -1122,10 +1163,10 @@ export class WebViewProvider { // Capture the tab reference on restore this.panelManager.captureTab(); - console.log('[WebViewProvider] Panel restored successfully'); + this.consoleLog('[WebViewProvider] Panel restored successfully'); // Attempt to restore authentication state and initialize connection - console.log( + this.consoleLog( '[WebViewProvider] Attempting to restore auth state and connection after restore...', ); await this.attemptAuthStateRestoration(); @@ -1139,12 +1180,12 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; } { - console.log('[WebViewProvider] Getting state for serialization'); - console.log( + this.consoleLog('[WebViewProvider] Getting state for serialization'); + this.consoleLog( '[WebViewProvider] Current conversationId:', this.messageHandler.getCurrentConversationId(), ); - console.log( + this.consoleLog( '[WebViewProvider] Current agentInitialized:', this.agentInitialized, ); @@ -1152,7 +1193,7 @@ export class WebViewProvider { conversationId: this.messageHandler.getCurrentConversationId(), agentInitialized: this.agentInitialized, }; - console.log('[WebViewProvider] Returning state:', state); + this.consoleLog('[WebViewProvider] Returning state:', state); return state; } @@ -1170,10 +1211,10 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; }): void { - console.log('[WebViewProvider] Restoring state:', state); + this.consoleLog('[WebViewProvider] Restoring state:', state); this.messageHandler.setCurrentConversationId(state.conversationId); this.agentInitialized = state.agentInitialized; - console.log( + this.consoleLog( '[WebViewProvider] State restored. agentInitialized:', this.agentInitialized, ); @@ -1206,8 +1247,6 @@ export class WebViewProvider { type: 'conversationCleared', data: {}, }); - - console.log('[WebViewProvider] New session created successfully'); } catch (_error) { console.error('[WebViewProvider] Failed to create new session:', _error); vscode.window.showErrorMessage(`Failed to create new session: ${_error}`); diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 741d9684..a46febcd 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -291,6 +291,41 @@ export class SessionMessageHandler extends BaseMessageHandler { return; } + // Ensure an ACP session exists before sending prompt + if (!this.agentManager.currentSessionId) { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + await this.agentManager.createNewSession(workingDir); + } catch (createErr) { + console.error( + '[SessionMessageHandler] Failed to create session before sending message:', + createErr, + ); + const errorMsg = + createErr instanceof Error ? createErr.message : String(createErr); + if ( + errorMsg.includes('Authentication required') || + errorMsg.includes('(code: -32000)') + ) { + const result = await vscode.window.showWarningMessage( + 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', + 'Login Now', + ); + if (result === 'Login Now') { + if (this.loginHandler) { + await this.loginHandler(); + } else { + await vscode.commands.executeCommand('qwen-code.login'); + } + } + return; + } + vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); + return; + } + } + // Send to agent try { this.resetStreamContent(); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index cd312361..a00fa0e5 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -14,6 +14,7 @@ import type { import type { ToolCallUpdate } from '../../types/chatTypes.js'; import type { ApprovalModeValue } from '../../types/acpTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; +import { createWebviewConsoleLogger } from '../utils/logger.js'; interface UseWebViewMessagesProps { // Session management @@ -129,6 +130,7 @@ export const useWebViewMessages = ({ }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); + const consoleLog = useRef(createWebviewConsoleLogger('WebViewMessages')); // Track active long-running tool calls (execute/bash/command) so we can // keep the bottom "waiting" message visible until all of them complete. const activeExecToolCallsRef = useRef>(new Set()); @@ -227,40 +229,26 @@ export const useWebViewMessages = ({ break; } - // case 'cliNotInstalled': { - // // Show CLI not installed message - // const errorMsg = - // (message?.data?.error as string) || - // 'Qwen Code CLI is not installed. Please install it to enable full functionality.'; + case 'agentConnected': { + // Agent connected successfully; clear any pending spinner + handlers.messageHandling.clearWaitingForResponse(); + break; + } - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`, - // timestamp: Date.now(), - // }); - // break; - // } + case 'agentConnectionError': { + // Agent connection failed; surface the error and unblock the UI + handlers.messageHandling.clearWaitingForResponse(); + const errorMsg = + (message?.data?.message as string) || + 'Failed to connect to Qwen agent.'; - // case 'agentConnected': { - // // Agent connected successfully - // handlers.messageHandling.clearWaitingForResponse(); - // break; - // } - - // case 'agentConnectionError': { - // // Agent connection failed - // handlers.messageHandling.clearWaitingForResponse(); - // const errorMsg = - // (message?.data?.message as string) || - // 'Failed to connect to Qwen agent.'; - - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, - // timestamp: Date.now(), - // }); - // break; - // } + handlers.messageHandling.addMessage({ + role: 'assistant', + content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, + timestamp: Date.now(), + }); + break; + } case 'loginError': { // Clear loading state and show error notice @@ -765,7 +753,10 @@ export const useWebViewMessages = ({ path: string; }>; if (files) { - console.log('[WebView] Received workspaceFiles:', files.length); + consoleLog.current( + '[WebView] Received workspaceFiles:', + files.length, + ); handlers.fileContext.setWorkspaceFiles(files); } break; diff --git a/packages/vscode-ide-companion/src/webview/styles/styles.css b/packages/vscode-ide-companion/src/webview/styles/styles.css index 4c3db053..956912cb 100644 --- a/packages/vscode-ide-companion/src/webview/styles/styles.css +++ b/packages/vscode-ide-companion/src/webview/styles/styles.css @@ -5,7 +5,6 @@ */ /* Import component styles */ -@import '../components/messages/Assistant/AssistantMessage.css'; @import './timeline.css'; @import '../components/messages/MarkdownRenderer/MarkdownRenderer.css'; diff --git a/packages/vscode-ide-companion/src/webview/utils/logger.ts b/packages/vscode-ide-companion/src/webview/utils/logger.ts new file mode 100644 index 00000000..40d1793e --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/utils/logger.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Creates a dev-only console logger for the WebView bundle. + * In production builds it becomes a no-op to avoid noisy logs. + */ +export function createWebviewConsoleLogger(scope?: string) { + return (...args: unknown[]) => { + const env = (globalThis as { process?: { env?: Record } }) + .process?.env; + const isProduction = env?.NODE_ENV === 'production'; + if (isProduction) { + return; + } + if (scope) { + console.log(`[${scope}]`, ...args); + return; + } + console.log(...args); + }; +} diff --git a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts b/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts deleted file mode 100644 index 0231f383..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * Minimal line-diff utility for webview previews. - * - * This is a lightweight LCS-based algorithm to compute add/remove operations - * between two texts. It intentionally avoids heavy dependencies and is - * sufficient for rendering a compact preview inside the chat. - */ - -export type DiffOp = - | { type: 'add'; line: string; newIndex: number } - | { type: 'remove'; line: string; oldIndex: number }; - -/** - * Compute a minimal line-diff (added/removed only). - * - Equal lines are omitted from output by design (we only preview changes). - * - Order of operations follows the new text progression so the preview feels natural. - */ -export function computeLineDiff( - oldText: string | null | undefined, - newText: string | undefined, -): DiffOp[] { - const a = (oldText || '').split('\n'); - const b = (newText || '').split('\n'); - - const n = a.length; - const m = b.length; - - // Build LCS DP table - const dp: number[][] = Array.from({ length: n + 1 }, () => - new Array(m + 1).fill(0), - ); - for (let i = n - 1; i >= 0; i--) { - for (let j = m - 1; j >= 0; j--) { - if (a[i] === b[j]) { - dp[i][j] = dp[i + 1][j + 1] + 1; - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); - } - } - } - - // Walk to produce operations - const ops: DiffOp[] = []; - let i = 0; - let j = 0; - while (i < n && j < m) { - if (a[i] === b[j]) { - i++; - j++; - } else if (dp[i + 1][j] >= dp[i][j + 1]) { - // remove a[i] - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } else { - // add b[j] - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - } - - // Remaining tails - while (i < n) { - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } - while (j < m) { - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - - return ops; -} - -/** - * Truncate a long list of operations for preview purposes. - * Keeps first `head` and last `tail` operations, inserting a gap marker. - */ -export function truncateOps( - ops: T[], - head = 120, - tail = 80, -): { items: T[]; truncated: boolean; omitted: number } { - if (ops.length <= head + tail) { - return { items: ops, truncated: false, omitted: 0 }; - } - const items = [...ops.slice(0, head), ...ops.slice(-tail)]; - return { items, truncated: true, omitted: ops.length - head - tail }; -} From c20df192a855575c694989df45877c59ccd0382c Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 11 Dec 2025 23:57:21 +0800 Subject: [PATCH 2/7] chore(vscode-ide-companion): revert some log util, will continue next time --- .../vscode-ide-companion/src/extension.ts | 20 ++-- .../src/services/acpConnection.ts | 1 + .../src/services/authStateManager.ts | 88 +++++--------- .../src/services/qwenAgentManager.ts | 91 ++++++--------- .../vscode-ide-companion/src/utils/logger.ts | 42 ------- .../vscode-ide-companion/src/webview/App.tsx | 4 +- .../src/webview/WebViewProvider.ts | 108 ++++++++---------- .../messages/toolcalls/shared/utils.ts | 19 --- .../src/webview/hooks/useWebViewMessages.ts | 7 +- .../src/webview/utils/logger.ts | 25 ---- 10 files changed, 123 insertions(+), 282 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/webview/utils/logger.ts diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 08979099..2adfaef1 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -8,11 +8,7 @@ import * as vscode from 'vscode'; import { IDEServer } from './ide-server.js'; import semver from 'semver'; import { DiffContentProvider, DiffManager } from './diff-manager.js'; -import { - createLogger, - getConsoleLogger, - initSharedConsoleLogger, -} from './utils/logger.js'; +import { createLogger } from './utils/logger.js'; import { detectIdeFromEnv, IDE_DEFINITIONS, @@ -109,8 +105,6 @@ async function checkForUpdates( export async function activate(context: vscode.ExtensionContext) { logger = vscode.window.createOutputChannel('Qwen Code Companion'); - initSharedConsoleLogger(context); - const consoleLog = getConsoleLogger(); log = createLogger(context, logger); log('Extension activated'); @@ -148,18 +142,18 @@ export async function activate(context: vscode.ExtensionContext) { webviewPanel: vscode.WebviewPanel, state: unknown, ) { - consoleLog( + console.log( '[Extension] Deserializing WebView panel with state:', state, ); // Create a new provider for the restored panel const provider = createWebViewProvider(); - consoleLog('[Extension] Provider created for deserialization'); + console.log('[Extension] Provider created for deserialization'); // Restore state if available BEFORE restoring the panel if (state && typeof state === 'object') { - consoleLog('[Extension] Restoring state:', state); + console.log('[Extension] Restoring state:', state); provider.restoreState( state as { conversationId: string | null; @@ -167,11 +161,11 @@ export async function activate(context: vscode.ExtensionContext) { }, ); } else { - consoleLog('[Extension] No state to restore or invalid state'); + console.log('[Extension] No state to restore or invalid state'); } await provider.restorePanel(webviewPanel); - consoleLog('[Extension] Panel restore completed'); + console.log('[Extension] Panel restore completed'); log('WebView panel restored from serialization'); }, @@ -212,6 +206,7 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-allow on diff.accept failed:', err); } + console.log('[Extension] Diff accepted'); }), vscode.commands.registerCommand('qwen.diff.cancel', (uri?: vscode.Uri) => { const docUri = uri ?? vscode.window.activeTextEditor?.document.uri; @@ -228,6 +223,7 @@ export async function activate(context: vscode.ExtensionContext) { } catch (err) { console.warn('[Extension] Auto-reject on diff.cancel failed:', err); } + console.log('[Extension] Diff cancelled'); })), vscode.commands.registerCommand('qwen.diff.closeAll', async () => { try { diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 4dd35b71..a9992de4 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -31,6 +31,7 @@ export class AcpConnection { private child: ChildProcess | null = null; private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; + // Deduplicate concurrent authenticate calls (across retry paths) private static authInFlight: Promise | null = null; // Remember the working dir provided at connect() so later ACP calls diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts index aa75fe36..824a642c 100644 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ b/packages/vscode-ide-companion/src/services/authStateManager.ts @@ -5,7 +5,6 @@ */ import type * as vscode from 'vscode'; -import { createConsoleLogger, getConsoleLogger } from '../utils/logger.js'; interface AuthState { isAuthenticated: boolean; @@ -22,7 +21,6 @@ export class AuthStateManager { private static context: vscode.ExtensionContext | null = null; private static readonly AUTH_STATE_KEY = 'qwen.authState'; private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - private static consoleLog: (...args: unknown[]) => void = getConsoleLogger(); // Deduplicate concurrent auth flows (e.g., multiple tabs prompting login) private static authFlowInFlight: Promise | null = null; private constructor() {} @@ -38,10 +36,6 @@ export class AuthStateManager { // If a context is provided, update the static context if (context) { AuthStateManager.context = context; - AuthStateManager.consoleLog = createConsoleLogger( - context, - 'AuthStateManager', - ); } return AuthStateManager.instance; @@ -76,19 +70,17 @@ export class AuthStateManager { const state = await this.getAuthState(); if (!state) { - AuthStateManager.consoleLog( - '[AuthStateManager] No cached auth state found', - ); + console.log('[AuthStateManager] No cached auth state found'); return false; } - AuthStateManager.consoleLog('[AuthStateManager] Found cached auth state:', { + console.log('[AuthStateManager] Found cached auth state:', { workingDir: state.workingDir, authMethod: state.authMethod, timestamp: new Date(state.timestamp).toISOString(), isAuthenticated: state.isAuthenticated, }); - AuthStateManager.consoleLog('[AuthStateManager] Checking against:', { + console.log('[AuthStateManager] Checking against:', { workingDir, authMethod, }); @@ -99,8 +91,8 @@ export class AuthStateManager { now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; if (isExpired) { - AuthStateManager.consoleLog('[AuthStateManager] Cached auth expired'); - AuthStateManager.consoleLog( + console.log('[AuthStateManager] Cached auth expired'); + console.log( '[AuthStateManager] Cache age:', Math.floor((now - state.timestamp) / 1000 / 60), 'minutes', @@ -114,29 +106,15 @@ export class AuthStateManager { state.workingDir === workingDir && state.authMethod === authMethod; if (!isSameContext) { - AuthStateManager.consoleLog( - '[AuthStateManager] Working dir or auth method changed', - ); - AuthStateManager.consoleLog( - '[AuthStateManager] Cached workingDir:', - state.workingDir, - ); - AuthStateManager.consoleLog( - '[AuthStateManager] Current workingDir:', - workingDir, - ); - AuthStateManager.consoleLog( - '[AuthStateManager] Cached authMethod:', - state.authMethod, - ); - AuthStateManager.consoleLog( - '[AuthStateManager] Current authMethod:', - authMethod, - ); + console.log('[AuthStateManager] Working dir or auth method changed'); + console.log('[AuthStateManager] Cached workingDir:', state.workingDir); + console.log('[AuthStateManager] Current workingDir:', workingDir); + console.log('[AuthStateManager] Cached authMethod:', state.authMethod); + console.log('[AuthStateManager] Current authMethod:', authMethod); return false; } - AuthStateManager.consoleLog('[AuthStateManager] Valid cached auth found'); + console.log('[AuthStateManager] Valid cached auth found'); return state.isAuthenticated; } @@ -146,10 +124,7 @@ export class AuthStateManager { */ async debugAuthState(): Promise { const state = await this.getAuthState(); - AuthStateManager.consoleLog( - '[AuthStateManager] DEBUG - Current auth state:', - state, - ); + console.log('[AuthStateManager] DEBUG - Current auth state:', state); if (state) { const now = Date.now(); @@ -157,16 +132,9 @@ export class AuthStateManager { const isExpired = now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; - AuthStateManager.consoleLog( - '[AuthStateManager] DEBUG - Auth state age:', - age, - 'minutes', - ); - AuthStateManager.consoleLog( - '[AuthStateManager] DEBUG - Auth state expired:', - isExpired, - ); - AuthStateManager.consoleLog( + 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, ); @@ -191,7 +159,7 @@ export class AuthStateManager { timestamp: Date.now(), }; - AuthStateManager.consoleLog('[AuthStateManager] Saving auth state:', { + console.log('[AuthStateManager] Saving auth state:', { workingDir, authMethod, timestamp: new Date(state.timestamp).toISOString(), @@ -201,14 +169,11 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, state, ); - AuthStateManager.consoleLog('[AuthStateManager] Auth state saved'); + console.log('[AuthStateManager] Auth state saved'); // Verify the state was saved correctly const savedState = await this.getAuthState(); - AuthStateManager.consoleLog( - '[AuthStateManager] Verified saved state:', - savedState, - ); + console.log('[AuthStateManager] Verified saved state:', savedState); } /** @@ -222,9 +187,9 @@ export class AuthStateManager { ); } - AuthStateManager.consoleLog('[AuthStateManager] Clearing auth state'); + console.log('[AuthStateManager] Clearing auth state'); const currentState = await this.getAuthState(); - AuthStateManager.consoleLog( + console.log( '[AuthStateManager] Current state before clearing:', currentState, ); @@ -233,14 +198,11 @@ export class AuthStateManager { AuthStateManager.AUTH_STATE_KEY, undefined, ); - AuthStateManager.consoleLog('[AuthStateManager] Auth state cleared'); + console.log('[AuthStateManager] Auth state cleared'); // Verify the state was cleared const newState = await this.getAuthState(); - AuthStateManager.consoleLog( - '[AuthStateManager] State after clearing:', - newState, - ); + console.log('[AuthStateManager] State after clearing:', newState); } /** @@ -249,15 +211,17 @@ export class AuthStateManager { private async getAuthState(): Promise { // Ensure we have a valid context if (!AuthStateManager.context) { - AuthStateManager.consoleLog( + console.log( '[AuthStateManager] No context available for getting auth state', ); return undefined; } - return AuthStateManager.context.globalState.get( + const a = AuthStateManager.context.globalState.get( AuthStateManager.AUTH_STATE_KEY, ); + console.log('[AuthStateManager] Auth state:', a); + return a; } /** diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index b954aed8..89214139 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -23,7 +23,6 @@ import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; -import { getConsoleLogger } from '../utils/logger.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -51,10 +50,8 @@ export class QwenAgentManager { // Callback storage private callbacks: QwenAgentCallbacks = {}; - private consoleLog: (...args: unknown[]) => void; - constructor(consoleLogger = getConsoleLogger()) { - this.consoleLog = consoleLogger; + constructor() { this.connection = new AcpConnection(); this.sessionReader = new QwenSessionReader(); this.sessionManager = new QwenSessionManager(); @@ -81,7 +78,7 @@ export class QwenAgentManager { ).update; const text = update?.content?.text || ''; if (update?.sessionUpdate === 'user_message_chunk' && text) { - this.consoleLog( + console.log( '[QwenAgentManager] Rehydration: routing user message chunk', ); this.callbacks.onMessage?.({ @@ -92,7 +89,7 @@ export class QwenAgentManager { return; } if (update?.sessionUpdate === 'agent_message_chunk' && text) { - this.consoleLog( + console.log( '[QwenAgentManager] Rehydration: routing agent message chunk', ); this.callbacks.onMessage?.({ @@ -103,7 +100,7 @@ export class QwenAgentManager { return; } // For other types during rehydration, fall through to normal handler - this.consoleLog( + console.log( '[QwenAgentManager] Rehydration: non-text update, forwarding to handler', ); } @@ -262,7 +259,7 @@ export class QwenAgentManager { * @returns Session list */ async getSessionList(): Promise>> { - this.consoleLog( + console.log( '[QwenAgentManager] Getting session list with version-aware strategy', ); @@ -270,7 +267,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionList = cliContextManager.supportsSessionList(); - this.consoleLog( + console.log( '[QwenAgentManager] CLI supports session/list:', supportsSessionList, ); @@ -278,14 +275,11 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionList) { try { - this.consoleLog( + console.log( '[QwenAgentManager] Attempting to get session list via ACP method', ); const response = await this.connection.listSessions(); - this.consoleLog( - '[QwenAgentManager] ACP session list response:', - response, - ); + console.log('[QwenAgentManager] ACP session list response:', response); // sendRequest resolves with the JSON-RPC "result" directly // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } @@ -303,7 +297,7 @@ export class QwenAgentManager { : []; } - this.consoleLog( + console.log( '[QwenAgentManager] Sessions retrieved via ACP:', res, items.length, @@ -322,7 +316,7 @@ export class QwenAgentManager { cwd: item.cwd, })); - this.consoleLog( + console.log( '[QwenAgentManager] Sessions retrieved via ACP:', sessions.length, ); @@ -338,11 +332,9 @@ export class QwenAgentManager { // Always fall back to file system method try { - this.consoleLog( - '[QwenAgentManager] Getting session list from file system', - ); + console.log('[QwenAgentManager] Getting session list from file system'); const sessions = await this.sessionReader.getAllSessions(undefined, true); - this.consoleLog( + console.log( '[QwenAgentManager] Session list from file system (all projects):', sessions.length, ); @@ -360,7 +352,7 @@ export class QwenAgentManager { }), ); - this.consoleLog( + console.log( '[QwenAgentManager] Sessions retrieved from file system:', result.length, ); @@ -500,7 +492,7 @@ export class QwenAgentManager { const item = list.find( (s) => s.sessionId === sessionId || s.id === sessionId, ); - this.consoleLog( + console.log( '[QwenAgentManager] Session list item for filePath lookup:', item, ); @@ -571,7 +563,7 @@ export class QwenAgentManager { } } // Simple linear reconstruction: filter user/assistant and sort by timestamp - this.consoleLog( + console.log( '[QwenAgentManager] JSONL records read:', records.length, filePath, @@ -728,7 +720,7 @@ export class QwenAgentManager { // Handle other types if needed } - this.consoleLog( + console.log( '[QwenAgentManager] JSONL messages reconstructed:', msgs.length, ); @@ -866,7 +858,7 @@ export class QwenAgentManager { tag: string, ): Promise<{ success: boolean; message?: string }> { try { - this.consoleLog( + console.log( '[QwenAgentManager] Saving session via /chat save command:', sessionId, 'with tag:', @@ -877,9 +869,7 @@ export class QwenAgentManager { // The CLI will handle this as a special command await this.connection.sendPrompt(`/chat save "${tag}"`); - this.consoleLog( - '[QwenAgentManager] /chat save command sent successfully', - ); + console.log('[QwenAgentManager] /chat save command sent successfully'); return { success: true, message: `Session saved with tag: ${tag}`, @@ -926,14 +916,14 @@ export class QwenAgentManager { conversationId: string, ): Promise<{ success: boolean; tag?: string; message?: string }> { try { - this.consoleLog('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - this.consoleLog('[QwenAgentManager] Conversation ID:', conversationId); - this.consoleLog('[QwenAgentManager] Message count:', messages.length); - this.consoleLog( + console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); + console.log('[QwenAgentManager] Conversation ID:', conversationId); + console.log('[QwenAgentManager] Message count:', messages.length); + console.log( '[QwenAgentManager] Current working dir:', this.currentWorkingDir, ); - this.consoleLog( + console.log( '[QwenAgentManager] Current session ID (from CLI):', this.currentSessionId, ); @@ -1010,11 +1000,11 @@ export class QwenAgentManager { try { // Route upcoming session/update messages as discrete messages for replay this.rehydratingSessionId = sessionId; - this.consoleLog( + console.log( '[QwenAgentManager] Rehydration start for session:', sessionId, ); - this.consoleLog( + console.log( '[QwenAgentManager] Attempting session/load via ACP for session:', sessionId, ); @@ -1022,7 +1012,7 @@ export class QwenAgentManager { sessionId, cwdOverride, ); - this.consoleLog( + console.log( '[QwenAgentManager] Session load succeeded. Response:', JSON.stringify(response).substring(0, 200), ); @@ -1062,10 +1052,7 @@ export class QwenAgentManager { throw error; } finally { // End rehydration routing regardless of outcome - this.consoleLog( - '[QwenAgentManager] Rehydration end for session:', - sessionId, - ); + console.log('[QwenAgentManager] Rehydration end for session:', sessionId); this.rehydratingSessionId = null; } } @@ -1078,7 +1065,7 @@ export class QwenAgentManager { * @returns Loaded session messages or null */ async loadSession(sessionId: string): Promise { - this.consoleLog( + console.log( '[QwenAgentManager] Loading session with version-aware strategy:', sessionId, ); @@ -1087,7 +1074,7 @@ export class QwenAgentManager { const cliContextManager = CliContextManager.getInstance(); const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - this.consoleLog( + console.log( '[QwenAgentManager] CLI supports session/load:', supportsSessionLoad, ); @@ -1095,13 +1082,11 @@ export class QwenAgentManager { // Try ACP method first if supported if (supportsSessionLoad) { try { - this.consoleLog( + console.log( '[QwenAgentManager] Attempting to load session via ACP method', ); await this.loadSessionViaAcp(sessionId); - this.consoleLog( - '[QwenAgentManager] Session loaded successfully via ACP', - ); + 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 @@ -1115,11 +1100,11 @@ export class QwenAgentManager { // Always fall back to file system method try { - this.consoleLog( + console.log( '[QwenAgentManager] Loading session messages from file system', ); const messages = await this.loadSessionMessagesFromFile(sessionId); - this.consoleLog( + console.log( '[QwenAgentManager] Session messages loaded successfully from file system', ); return messages; @@ -1142,7 +1127,7 @@ export class QwenAgentManager { sessionId: string, ): Promise { try { - this.consoleLog( + console.log( '[QwenAgentManager] Loading session from file system:', sessionId, ); @@ -1154,7 +1139,7 @@ export class QwenAgentManager { ); if (!session) { - this.consoleLog( + console.log( '[QwenAgentManager] Session not found in file system:', sessionId, ); @@ -1209,7 +1194,7 @@ export class QwenAgentManager { return this.sessionCreateInFlight; } - this.consoleLog('[QwenAgentManager] Creating new session...'); + console.log('[QwenAgentManager] Creating new session...'); // Prefer the provided authStateManager, otherwise fall back to the one // remembered during connect(). This prevents accidental re-auth in // fallback paths (e.g. session switching) when the handler didn't pass it. @@ -1250,7 +1235,7 @@ export class QwenAgentManager { } } const newSessionId = this.connection.currentSessionId; - this.consoleLog( + console.log( '[QwenAgentManager] New session created with ID:', newSessionId, ); @@ -1276,7 +1261,7 @@ export class QwenAgentManager { * Cancel current prompt */ async cancelCurrentPrompt(): Promise { - this.consoleLog('[QwenAgentManager] Cancelling current prompt'); + console.log('[QwenAgentManager] Cancelling current prompt'); await this.connection.cancelSession(); } diff --git a/packages/vscode-ide-companion/src/utils/logger.ts b/packages/vscode-ide-companion/src/utils/logger.ts index f92cfe8c..b3f8ad1e 100644 --- a/packages/vscode-ide-companion/src/utils/logger.ts +++ b/packages/vscode-ide-companion/src/utils/logger.ts @@ -6,11 +6,6 @@ import * as vscode from 'vscode'; -type ConsoleLogger = (...args: unknown[]) => void; - -// Shared console logger instance, initialized during extension activation. -let sharedConsoleLogger: ConsoleLogger = () => {}; - export function createLogger( context: vscode.ExtensionContext, logger: vscode.OutputChannel, @@ -21,40 +16,3 @@ export function createLogger( } }; } - -/** - * Creates a dev-only logger that writes to the VS Code console (Developer Tools). - */ -export function createConsoleLogger( - context: vscode.ExtensionContext, - scope?: string, -): ConsoleLogger { - return (...args: unknown[]) => { - if (context.extensionMode !== vscode.ExtensionMode.Development) { - return; - } - if (scope) { - console.log(`[${scope}]`, ...args); - return; - } - console.log(...args); - }; -} - -/** - * Initialize the shared console logger so other modules can import it without - * threading the extension context everywhere. - */ -export function initSharedConsoleLogger( - context: vscode.ExtensionContext, - scope?: string, -) { - sharedConsoleLogger = createConsoleLogger(context, scope); -} - -/** - * Get the shared console logger (no-op until initialized). - */ -export function getConsoleLogger(): ConsoleLogger { - return sharedConsoleLogger; -} diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 65d978fd..4bdf6622 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -45,11 +45,9 @@ import { FileIcon, UserIcon } from './components/icons/index.js'; import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js'; import type { ApprovalModeValue } from '../types/acpTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; -import { createWebviewConsoleLogger } from './utils/logger.js'; export const App: React.FC = () => { const vscode = useVSCode(); - const consoleLog = useMemo(() => createWebviewConsoleLogger('App'), []); // Core hooks const sessionManagement = useSessionManagement(vscode); @@ -542,7 +540,7 @@ export const App: React.FC = () => { ); }, [messageHandling.messages, inProgressToolCalls, completedToolCalls]); - consoleLog('[App] Rendering messages:', allMessages); + console.log('[App] Rendering messages:', allMessages); // Render all messages and tool calls const renderMessages = useCallback<() => React.ReactNode>( diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 11054259..faa81226 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -16,7 +16,6 @@ import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; -import { createConsoleLogger } from '../utils/logger.js'; export class WebViewProvider { private panelManager: PanelManager; @@ -33,15 +32,12 @@ export class WebViewProvider { private pendingPermissionResolve: ((optionId: string) => void) | null = null; // Track current ACP mode id to influence permission/diff behavior private currentModeId: ApprovalModeValue | null = null; - private consoleLog: (...args: unknown[]) => void; constructor( context: vscode.ExtensionContext, private extensionUri: vscode.Uri, ) { - const agentConsoleLogger = createConsoleLogger(context, 'QwenAgentManager'); - this.consoleLog = createConsoleLogger(context, 'WebViewProvider'); - this.agentManager = new QwenAgentManager(agentConsoleLogger); + this.agentManager = new QwenAgentManager(); this.conversationStore = new ConversationStore(context); this.authStateManager = AuthStateManager.getInstance(context); this.panelManager = new PanelManager(extensionUri, () => { @@ -384,7 +380,7 @@ export class WebViewProvider { // Set up state serialization newPanel.onDidChangeViewState(() => { - this.consoleLog( + console.log( '[WebViewProvider] Panel view state changed, triggering serialization check', ); }); @@ -514,7 +510,7 @@ export class WebViewProvider { } // Attempt to restore authentication state and initialize connection - this.consoleLog( + console.log( '[WebViewProvider] Attempting to restore auth state and connection...', ); await this.attemptAuthStateRestoration(); @@ -536,26 +532,23 @@ export class WebViewProvider { workingDir, authMethod, ); - this.consoleLog( - '[WebViewProvider] Has valid cached auth:', - hasValidAuth, - ); + console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth); if (hasValidAuth) { - this.consoleLog( + console.log( '[WebViewProvider] Valid auth found, attempting connection...', ); // Try to connect with cached auth await this.initializeAgentConnection(); } else { - this.consoleLog( + console.log( '[WebViewProvider] No valid auth found, rendering empty conversation', ); // Render the chat UI immediately without connecting await this.initializeEmptyConversation(); } } else { - this.consoleLog( + console.log( '[WebViewProvider] No auth state manager, rendering empty conversation', ); await this.initializeEmptyConversation(); @@ -585,11 +578,11 @@ export class WebViewProvider { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - this.consoleLog( + console.log( '[WebViewProvider] Starting initialization, workingDir:', workingDir, ); - this.consoleLog( + console.log( '[WebViewProvider] AuthStateManager available:', !!this.authStateManager, ); @@ -598,10 +591,10 @@ export class WebViewProvider { const cliDetection = await CliDetector.detectQwenCli(); if (!cliDetection.isInstalled) { - this.consoleLog( + console.log( '[WebViewProvider] Qwen CLI not detected, skipping agent connection', ); - this.consoleLog( + console.log( '[WebViewProvider] CLI detection error:', cliDetection.error, ); @@ -612,20 +605,20 @@ export class WebViewProvider { // Initialize empty conversation (can still browse history) await this.initializeEmptyConversation(); } else { - this.consoleLog( + console.log( '[WebViewProvider] Qwen CLI detected, attempting connection...', ); - this.consoleLog('[WebViewProvider] CLI path:', cliDetection.cliPath); - this.consoleLog('[WebViewProvider] CLI version:', cliDetection.version); + console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); + console.log('[WebViewProvider] CLI version:', cliDetection.version); try { - this.consoleLog('[WebViewProvider] Connecting to agent...'); - this.consoleLog( + console.log('[WebViewProvider] Connecting to agent...'); + console.log( '[WebViewProvider] Using authStateManager:', !!this.authStateManager, ); const authInfo = await this.authStateManager.getAuthInfo(); - this.consoleLog('[WebViewProvider] Auth cache status:', authInfo); + console.log('[WebViewProvider] Auth cache status:', authInfo); // Pass the detected CLI path to ensure we use the correct installation await this.agentManager.connect( @@ -633,7 +626,7 @@ export class WebViewProvider { this.authStateManager, cliDetection.cliPath, ); - this.consoleLog('[WebViewProvider] Agent connected successfully'); + console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; // Load messages from the current Qwen session @@ -674,8 +667,8 @@ export class WebViewProvider { * Called when user explicitly uses /login command */ async forceReLogin(): Promise { - this.consoleLog('[WebViewProvider] Force re-login requested'); - this.consoleLog( + console.log('[WebViewProvider] Force re-login requested'); + console.log( '[WebViewProvider] Current authStateManager:', !!this.authStateManager, ); @@ -694,23 +687,20 @@ export class WebViewProvider { // Clear existing auth cache if (this.authStateManager) { await this.authStateManager.clearAuthState(); - this.consoleLog('[WebViewProvider] Auth cache cleared'); + console.log('[WebViewProvider] Auth cache cleared'); } else { - this.consoleLog('[WebViewProvider] No authStateManager to clear'); + console.log('[WebViewProvider] No authStateManager to clear'); } // Disconnect existing connection if any if (this.agentInitialized) { try { this.agentManager.disconnect(); - this.consoleLog( + console.log( '[WebViewProvider] Existing connection disconnected', ); } catch (_error) { - this.consoleLog( - '[WebViewProvider] Error disconnecting:', - _error, - ); + console.log('[WebViewProvider] Error disconnecting:', _error); } this.agentInitialized = false; } @@ -724,7 +714,7 @@ export class WebViewProvider { // Reinitialize connection (will trigger fresh authentication) await this.doInitializeAgentConnection(); - this.consoleLog( + console.log( '[WebViewProvider] Force re-login completed successfully', ); @@ -733,9 +723,7 @@ export class WebViewProvider { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); await this.authStateManager.saveAuthState(workingDir, authMethod); - this.consoleLog( - '[WebViewProvider] Auth state saved after re-login', - ); + console.log('[WebViewProvider] Auth state saved after re-login'); } // Send success notification to WebView @@ -772,15 +760,15 @@ export class WebViewProvider { * Called when restoring WebView after VSCode restart */ async refreshConnection(): Promise { - this.consoleLog('[WebViewProvider] Refresh connection requested'); + console.log('[WebViewProvider] Refresh connection requested'); // Disconnect existing connection if any if (this.agentInitialized) { try { this.agentManager.disconnect(); - this.consoleLog('[WebViewProvider] Existing connection disconnected'); + console.log('[WebViewProvider] Existing connection disconnected'); } catch (_error) { - this.consoleLog('[WebViewProvider] Error disconnecting:', _error); + console.log('[WebViewProvider] Error disconnecting:', _error); } this.agentInitialized = false; } @@ -791,7 +779,7 @@ export class WebViewProvider { // Reinitialize connection (will use cached auth if available) try { await this.initializeAgentConnection(); - this.consoleLog( + console.log( '[WebViewProvider] Connection refresh completed successfully', ); @@ -821,7 +809,7 @@ export class WebViewProvider { */ private async loadCurrentSessionMessages(): Promise { try { - this.consoleLog( + console.log( '[WebViewProvider] Initializing with new session (skipping restoration)', ); @@ -835,12 +823,12 @@ export class WebViewProvider { workingDir, this.authStateManager, ); - this.consoleLog('[WebViewProvider] ACP session created successfully'); + console.log('[WebViewProvider] ACP session created successfully'); // Ensure auth state is saved after successful session creation if (this.authStateManager) { await this.authStateManager.saveAuthState(workingDir, authMethod); - this.consoleLog( + console.log( '[WebViewProvider] Auth state saved after session creation', ); } @@ -854,7 +842,7 @@ export class WebViewProvider { ); } } else { - this.consoleLog( + console.log( '[WebViewProvider] Existing ACP session detected, skipping new session creation', ); } @@ -878,14 +866,14 @@ export class WebViewProvider { */ private async initializeEmptyConversation(): Promise { try { - this.consoleLog('[WebViewProvider] Initializing empty conversation'); + console.log('[WebViewProvider] Initializing empty conversation'); const newConv = await this.conversationStore.createConversation(); this.messageHandler.setCurrentConversationId(newConv.id); this.sendMessageToWebView({ type: 'conversationLoaded', data: newConv, }); - this.consoleLog( + console.log( '[WebViewProvider] Empty conversation initialized:', this.messageHandler.getCurrentConversationId(), ); @@ -1009,7 +997,7 @@ export class WebViewProvider { * Call this when auth cache is cleared to force re-authentication */ resetAgentState(): void { - this.consoleLog('[WebViewProvider] Resetting agent state'); + console.log('[WebViewProvider] Resetting agent state'); this.agentInitialized = false; // Disconnect existing connection this.agentManager.disconnect(); @@ -1019,7 +1007,7 @@ export class WebViewProvider { * Clear authentication cache for this WebViewProvider instance */ async clearAuthCache(): Promise { - this.consoleLog('[WebViewProvider] Clearing auth cache for this instance'); + console.log('[WebViewProvider] Clearing auth cache for this instance'); if (this.authStateManager) { await this.authStateManager.clearAuthState(); this.resetAgentState(); @@ -1031,8 +1019,8 @@ export class WebViewProvider { * This sets up the panel with all event listeners */ async restorePanel(panel: vscode.WebviewPanel): Promise { - this.consoleLog('[WebViewProvider] Restoring WebView panel'); - this.consoleLog( + console.log('[WebViewProvider] Restoring WebView panel'); + console.log( '[WebViewProvider] Current authStateManager in restore:', !!this.authStateManager, ); @@ -1163,10 +1151,10 @@ export class WebViewProvider { // Capture the tab reference on restore this.panelManager.captureTab(); - this.consoleLog('[WebViewProvider] Panel restored successfully'); + console.log('[WebViewProvider] Panel restored successfully'); // Attempt to restore authentication state and initialize connection - this.consoleLog( + console.log( '[WebViewProvider] Attempting to restore auth state and connection after restore...', ); await this.attemptAuthStateRestoration(); @@ -1180,12 +1168,12 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; } { - this.consoleLog('[WebViewProvider] Getting state for serialization'); - this.consoleLog( + console.log('[WebViewProvider] Getting state for serialization'); + console.log( '[WebViewProvider] Current conversationId:', this.messageHandler.getCurrentConversationId(), ); - this.consoleLog( + console.log( '[WebViewProvider] Current agentInitialized:', this.agentInitialized, ); @@ -1193,7 +1181,7 @@ export class WebViewProvider { conversationId: this.messageHandler.getCurrentConversationId(), agentInitialized: this.agentInitialized, }; - this.consoleLog('[WebViewProvider] Returning state:', state); + console.log('[WebViewProvider] Returning state:', state); return state; } @@ -1211,10 +1199,10 @@ export class WebViewProvider { conversationId: string | null; agentInitialized: boolean; }): void { - this.consoleLog('[WebViewProvider] Restoring state:', state); + console.log('[WebViewProvider] Restoring state:', state); this.messageHandler.setCurrentConversationId(state.conversationId); this.agentInitialized = state.agentInitialized; - this.consoleLog( + console.log( '[WebViewProvider] State restored. agentInitialized:', this.agentInitialized, ); diff --git a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts index 4ae9efd6..ceb2cb2b 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts +++ b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts @@ -61,25 +61,6 @@ export const safeTitle = (title: unknown): string => { return ''; }; -/** - * Get icon emoji for a given tool kind - */ -export const getKindIcon = (kind: string): string => { - const kindMap: Record = { - edit: '✏️', - write: '✏️', - read: '📖', - execute: '⚡', - fetch: '🌐', - delete: '🗑️', - move: '📦', - search: '🔍', - think: '💭', - diff: '📝', - }; - return kindMap[kind.toLowerCase()] || '🔧'; -}; - /** * Check if a tool call should be displayed * Hides internal tool calls diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index a00fa0e5..7a3f7e06 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -14,7 +14,6 @@ import type { import type { ToolCallUpdate } from '../../types/chatTypes.js'; import type { ApprovalModeValue } from '../../types/acpTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; -import { createWebviewConsoleLogger } from '../utils/logger.js'; interface UseWebViewMessagesProps { // Session management @@ -130,7 +129,6 @@ export const useWebViewMessages = ({ }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); - const consoleLog = useRef(createWebviewConsoleLogger('WebViewMessages')); // Track active long-running tool calls (execute/bash/command) so we can // keep the bottom "waiting" message visible until all of them complete. const activeExecToolCallsRef = useRef>(new Set()); @@ -753,10 +751,7 @@ export const useWebViewMessages = ({ path: string; }>; if (files) { - consoleLog.current( - '[WebView] Received workspaceFiles:', - files.length, - ); + console.log('[WebView] Received workspaceFiles:', files.length); handlers.fileContext.setWorkspaceFiles(files); } break; diff --git a/packages/vscode-ide-companion/src/webview/utils/logger.ts b/packages/vscode-ide-companion/src/webview/utils/logger.ts deleted file mode 100644 index 40d1793e..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/logger.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Creates a dev-only console logger for the WebView bundle. - * In production builds it becomes a no-op to avoid noisy logs. - */ -export function createWebviewConsoleLogger(scope?: string) { - return (...args: unknown[]) => { - const env = (globalThis as { process?: { env?: Record } }) - .process?.env; - const isProduction = env?.NODE_ENV === 'production'; - if (isProduction) { - return; - } - if (scope) { - console.log(`[${scope}]`, ...args); - return; - } - console.log(...args); - }; -} From 25261ab88d7e061fef2345899829f941be64541b Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 12 Dec 2025 01:14:28 +0800 Subject: [PATCH 3/7] fix(vscode-ide-companion): slight delay to ensure auth state settlement --- .../src/services/authStateManager.ts | 36 ++- .../src/services/qwenAgentManager.ts | 1 + .../src/services/qwenConnectionHandler.ts | 6 +- .../src/webview/WebViewProvider.ts | 4 +- .../webview/handlers/SessionMessageHandler.ts | 221 +++++------------- 5 files changed, 93 insertions(+), 175 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts index 824a642c..e56f9fdf 100644 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ b/packages/vscode-ide-companion/src/services/authStateManager.ts @@ -21,8 +21,8 @@ export class AuthStateManager { private static context: vscode.ExtensionContext | null = null; private static readonly AUTH_STATE_KEY = 'qwen.authState'; private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - // Deduplicate concurrent auth flows (e.g., multiple tabs prompting login) - private static authFlowInFlight: Promise | null = null; + // Deduplicate concurrent auth processes (e.g., multiple tabs prompting login) + private static authProcessInFlight: Promise | null = null; private constructor() {} /** @@ -42,24 +42,38 @@ export class AuthStateManager { } /** - * Run an auth-related flow exclusively. If another flow is already running, - * return the same promise to prevent duplicate login prompts. + * Run an auth-related flow with optional queueing. + * - 默认:复用在跑的 promise,避免重复弹窗。 + * - forceNew: true 时,等待当前 flow 结束后再串行启动新的,用于强制重登。 */ - static runExclusiveAuth(task: () => Promise): Promise { - if (AuthStateManager.authFlowInFlight) { - return AuthStateManager.authFlowInFlight as Promise; + static runExclusiveAuth( + task: () => Promise, + options?: { forceNew?: boolean }, + ): Promise { + if (AuthStateManager.authProcessInFlight) { + if (!options?.forceNew) { + return AuthStateManager.authProcessInFlight as Promise; + } + // queue a new flow after current finishes + const next = AuthStateManager.authProcessInFlight + .catch(() => { + /* ignore previous failure for next run */ + }) + .then(() => + AuthStateManager.runExclusiveAuth(task, { forceNew: false }), + ); + return next as Promise; } const p = Promise.resolve() .then(task) .finally(() => { - // Clear only if this promise is still the active one - if (AuthStateManager.authFlowInFlight === p) { - AuthStateManager.authFlowInFlight = null; + if (AuthStateManager.authProcessInFlight === p) { + AuthStateManager.authProcessInFlight = null; } }); - AuthStateManager.authFlowInFlight = p; + AuthStateManager.authProcessInFlight = p; return p as Promise; } diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 89214139..7851d926 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -1222,6 +1222,7 @@ export class QwenAgentManager { if (effectiveAuth) { await effectiveAuth.saveAuthState(workingDir, authMethod); } + await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled await this.connection.newSession(workingDir); } catch (reauthErr) { // Clear potentially stale cache on failure and rethrow diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 11e7199a..9e2f5a81 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -119,11 +119,6 @@ export class QwenConnectionHandler { // Save auth state if (authStateManager) { - console.log( - '[QwenAgentManager] Saving auth state after successful authentication', - ); - console.log('[QwenAgentManager] Working dir for save:', workingDir); - console.log('[QwenAgentManager] Auth method for save:', authMethod); await authStateManager.saveAuthState(workingDir, authMethod); console.log('[QwenAgentManager] Auth state save completed'); } @@ -145,6 +140,7 @@ export class QwenConnectionHandler { } try { + await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled console.log( '[QwenAgentManager] Creating new session after authentication...', ); diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index faa81226..bfa9a567 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -134,7 +134,7 @@ export class WebViewProvider { // Note: Tool call updates are handled in handleSessionUpdate within QwenAgentManager // and sent via onStreamChunk callback this.agentManager.onToolCall((update) => { - // Always surface tool calls; they are part of the live assistant flow. + // Always surface tool calls; they are part of the live assistant process. // Cast update to access sessionUpdate property const updateData = update as unknown as Record; @@ -673,7 +673,7 @@ export class WebViewProvider { !!this.authStateManager, ); - // If a login/connection flow is already running, reuse it to avoid double prompts + // If a login/connection process is already running, reuse it to avoid double prompts const p = Promise.resolve( vscode.window.withProgress( { diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index a46febcd..0df3e0da 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -149,6 +149,50 @@ export class SessionMessageHandler extends BaseMessageHandler { return this.isSavingCheckpoint; } + /** + * Prompt user to login and invoke the registered login handler/command. + * Returns true if a login was initiated. + */ + private async promptLogin(message: string): Promise { + const result = await vscode.window.showWarningMessage(message, 'Login Now'); + if (result === 'Login Now') { + if (this.loginHandler) { + await this.loginHandler(); + } else { + await vscode.commands.executeCommand('qwen-code.login'); + } + return true; + } + return false; + } + + /** + * Prompt user to login or view offline. Returns 'login', 'offline', or 'dismiss'. + * When login is chosen, it triggers the login handler/command. + */ + private async promptLoginOrOffline( + message: string, + ): Promise<'login' | 'offline' | 'dismiss'> { + const selection = await vscode.window.showWarningMessage( + message, + 'Login Now', + 'View Offline', + ); + + if (selection === 'Login Now') { + if (this.loginHandler) { + await this.loginHandler(); + } else { + await vscode.commands.executeCommand('qwen-code.login'); + } + return 'login'; + } + if (selection === 'View Offline') { + return 'offline'; + } + return 'dismiss'; + } + /** * Handle send message request */ @@ -271,23 +315,7 @@ export class SessionMessageHandler extends BaseMessageHandler { console.warn('[SessionMessageHandler] Agent not connected'); // Show non-modal notification with Login button - const result = await vscode.window.showWarningMessage( - 'You need to login first to use 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('qwen-code.login'); - } - } + await this.promptLogin('You need to login first to use Qwen Code.'); return; } @@ -308,17 +336,9 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('Authentication required') || errorMsg.includes('(code: -32000)') ) { - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } return; } vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); @@ -426,19 +446,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('Invalid token') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -463,17 +474,10 @@ export class SessionMessageHandler extends BaseMessageHandler { // Ensure connection (login) before creating a new session if (!this.agentManager.isConnected) { - const result = await vscode.window.showWarningMessage( + const proceeded = await this.promptLogin( 'You need to login before creating a new session.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else { + if (!proceeded) { return; } } @@ -524,19 +528,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -560,19 +555,11 @@ export class SessionMessageHandler extends BaseMessageHandler { // If not connected yet, offer to login or view offline if (!this.agentManager.isConnected) { - const selection = await vscode.window.showWarningMessage( + const choice = await this.promptLoginOrOffline( 'You are not logged in. Login now to fully restore this session, or view it offline.', - 'Login Now', - 'View Offline', ); - if (selection === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else if (selection === 'View Offline') { + if (choice === 'offline') { // Show messages from local cache only const messages = await this.agentManager.getSessionMessages(sessionId); @@ -585,7 +572,7 @@ export class SessionMessageHandler extends BaseMessageHandler { 'Showing cached session content. Login to interact with the AI.', ); return; - } else { + } else if (choice !== 'login') { // User dismissed; do nothing return; } @@ -672,19 +659,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -741,19 +719,10 @@ export class SessionMessageHandler extends BaseMessageHandler { createErrorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -790,19 +759,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -854,19 +814,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -918,19 +869,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -966,19 +908,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -1031,19 +964,11 @@ export class SessionMessageHandler extends BaseMessageHandler { try { // If not connected, offer to login or view offline if (!this.agentManager.isConnected) { - const selection = await vscode.window.showWarningMessage( + const choice = await this.promptLoginOrOffline( 'You are not logged in. Login now to fully restore this session, or view it offline.', - 'Login Now', - 'View Offline', ); - if (selection === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else if (selection === 'View Offline') { + if (choice === 'offline') { const messages = await this.agentManager.getSessionMessages(sessionId); this.currentConversationId = sessionId; @@ -1055,7 +980,7 @@ export class SessionMessageHandler extends BaseMessageHandler { 'Showing cached session content. Login to interact with the AI.', ); return; - } else { + } else if (choice !== 'login') { return; } } @@ -1089,19 +1014,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -1140,19 +1056,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( '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('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', From 02234f5434228dbc781d42c4cc65abafe5fbb2c4 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 12 Dec 2025 01:17:38 +0800 Subject: [PATCH 4/7] chore(vscode-ide-companion): change comments and delays --- .../vscode-ide-companion/src/services/authStateManager.ts | 4 ++-- .../vscode-ide-companion/src/services/qwenAgentManager.ts | 2 +- .../src/services/qwenConnectionHandler.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts index e56f9fdf..c46bcb83 100644 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ b/packages/vscode-ide-companion/src/services/authStateManager.ts @@ -43,8 +43,8 @@ export class AuthStateManager { /** * Run an auth-related flow with optional queueing. - * - 默认:复用在跑的 promise,避免重复弹窗。 - * - forceNew: true 时,等待当前 flow 结束后再串行启动新的,用于强制重登。 + * - Default: Reuse existing promise to avoid duplicate popups. + * - When forceNew: true, wait for current flow to finish before starting a new one serially, used for forced re-login. */ static runExclusiveAuth( task: () => Promise, diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index 7851d926..dd0712b7 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -1222,7 +1222,7 @@ export class QwenAgentManager { if (effectiveAuth) { await effectiveAuth.saveAuthState(workingDir, authMethod); } - await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled + await setTimeout(() => Promise.resolve(), 300); // slight delay to ensure auth state is settled await this.connection.newSession(workingDir); } catch (reauthErr) { // Clear potentially stale cache on failure and rethrow diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 9e2f5a81..278032a2 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -140,7 +140,7 @@ export class QwenConnectionHandler { } try { - await setTimeout(() => Promise.resolve(), 100); // slight delay to ensure auth state is settled + await setTimeout(() => Promise.resolve(), 300); // slight delay to ensure auth state is settled console.log( '[QwenAgentManager] Creating new session after authentication...', ); From d754767e733de62d865b9a4acf8d0c82335747e3 Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 12 Dec 2025 13:40:18 +0800 Subject: [PATCH 5/7] chore(vscode-ide-companion): rm authState manager in vscode-ide-companion to simplify the login architecture --- .../src/services/acpConnection.ts | 25 +- .../src/services/authStateManager.ts | 253 ------------------ .../src/services/qwenAgentManager.ts | 43 +-- .../src/services/qwenConnectionHandler.ts | 94 +------ .../src/webview/WebViewProvider.ts | 246 ++++++----------- 5 files changed, 102 insertions(+), 559 deletions(-) delete mode 100644 packages/vscode-ide-companion/src/services/authStateManager.ts diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index a9992de4..9fd548cc 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -32,8 +32,6 @@ export class AcpConnection { private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; - // Deduplicate concurrent authenticate calls (across retry paths) - private static authInFlight: Promise | null = null; // Remember the working dir provided at connect() so later ACP calls // that require cwd (e.g. session/list) can include it. private workingDir: string = process.cwd(); @@ -274,23 +272,12 @@ export class AcpConnection { * @returns Authentication response */ async authenticate(methodId?: string): Promise { - if (AcpConnection.authInFlight) { - return AcpConnection.authInFlight; - } - - const p = this.sessionManager - .authenticate( - methodId, - this.child, - this.pendingRequests, - this.nextRequestId, - ) - .finally(() => { - AcpConnection.authInFlight = null; - }); - - AcpConnection.authInFlight = p; - return p; + return this.sessionManager.authenticate( + methodId, + this.child, + this.pendingRequests, + this.nextRequestId, + ); } /** diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts deleted file mode 100644 index c46bcb83..00000000 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type * as vscode from 'vscode'; - -interface AuthState { - isAuthenticated: boolean; - authMethod: string; - timestamp: number; - workingDir?: string; -} - -/** - * Manages authentication state caching to avoid repeated logins - */ -export class AuthStateManager { - private static instance: AuthStateManager | null = null; - private static context: vscode.ExtensionContext | null = null; - private static readonly AUTH_STATE_KEY = 'qwen.authState'; - private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - // Deduplicate concurrent auth processes (e.g., multiple tabs prompting login) - private static authProcessInFlight: Promise | null = null; - private constructor() {} - - /** - * Get singleton instance of AuthStateManager - */ - static getInstance(context?: vscode.ExtensionContext): AuthStateManager { - if (!AuthStateManager.instance) { - AuthStateManager.instance = new AuthStateManager(); - } - - // If a context is provided, update the static context - if (context) { - AuthStateManager.context = context; - } - - return AuthStateManager.instance; - } - - /** - * Run an auth-related flow with optional queueing. - * - Default: Reuse existing promise to avoid duplicate popups. - * - When forceNew: true, wait for current flow to finish before starting a new one serially, used for forced re-login. - */ - static runExclusiveAuth( - task: () => Promise, - options?: { forceNew?: boolean }, - ): Promise { - if (AuthStateManager.authProcessInFlight) { - if (!options?.forceNew) { - return AuthStateManager.authProcessInFlight as Promise; - } - // queue a new flow after current finishes - const next = AuthStateManager.authProcessInFlight - .catch(() => { - /* ignore previous failure for next run */ - }) - .then(() => - AuthStateManager.runExclusiveAuth(task, { forceNew: false }), - ); - return next as Promise; - } - - const p = Promise.resolve() - .then(task) - .finally(() => { - if (AuthStateManager.authProcessInFlight === p) { - AuthStateManager.authProcessInFlight = null; - } - }); - - AuthStateManager.authProcessInFlight = p; - return p as Promise; - } - - /** - * Check if there's a valid cached authentication - */ - async hasValidAuth(workingDir: string, authMethod: string): Promise { - const state = await this.getAuthState(); - - if (!state) { - console.log('[AuthStateManager] No cached auth state found'); - return false; - } - - console.log('[AuthStateManager] Found cached auth state:', { - workingDir: state.workingDir, - authMethod: state.authMethod, - timestamp: new Date(state.timestamp).toISOString(), - isAuthenticated: state.isAuthenticated, - }); - console.log('[AuthStateManager] Checking against:', { - workingDir, - authMethod, - }); - - // Check if auth is still valid (within cache duration) - const now = Date.now(); - const isExpired = - now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; - - if (isExpired) { - console.log('[AuthStateManager] Cached auth expired'); - console.log( - '[AuthStateManager] Cache age:', - Math.floor((now - state.timestamp) / 1000 / 60), - 'minutes', - ); - await this.clearAuthState(); - return false; - } - - // Check if it's for the same working directory and auth method - const isSameContext = - state.workingDir === workingDir && state.authMethod === authMethod; - - if (!isSameContext) { - console.log('[AuthStateManager] Working dir or auth method changed'); - console.log('[AuthStateManager] Cached workingDir:', state.workingDir); - console.log('[AuthStateManager] Current workingDir:', workingDir); - console.log('[AuthStateManager] Cached authMethod:', state.authMethod); - console.log('[AuthStateManager] Current authMethod:', authMethod); - return false; - } - - console.log('[AuthStateManager] Valid cached auth found'); - 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 - */ - async saveAuthState(workingDir: string, authMethod: string): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - throw new Error( - '[AuthStateManager] No context available for saving auth state', - ); - } - - const state: AuthState = { - isAuthenticated: true, - authMethod, - workingDir, - timestamp: Date.now(), - }; - - console.log('[AuthStateManager] Saving auth state:', { - workingDir, - authMethod, - timestamp: new Date(state.timestamp).toISOString(), - }); - - await AuthStateManager.context.globalState.update( - AuthStateManager.AUTH_STATE_KEY, - state, - ); - console.log('[AuthStateManager] Auth state saved'); - - // Verify the state was saved correctly - const savedState = await this.getAuthState(); - console.log('[AuthStateManager] Verified saved state:', savedState); - } - - /** - * Clear authentication state - */ - async clearAuthState(): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - throw new Error( - '[AuthStateManager] No context available for clearing auth state', - ); - } - - console.log('[AuthStateManager] Clearing auth state'); - const currentState = await this.getAuthState(); - console.log( - '[AuthStateManager] Current state before clearing:', - currentState, - ); - - await AuthStateManager.context.globalState.update( - AuthStateManager.AUTH_STATE_KEY, - undefined, - ); - console.log('[AuthStateManager] Auth state cleared'); - - // Verify the state was cleared - const newState = await this.getAuthState(); - console.log('[AuthStateManager] State after clearing:', newState); - } - - /** - * Get current auth state - */ - private async getAuthState(): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - console.log( - '[AuthStateManager] No context available for getting auth state', - ); - return undefined; - } - - const a = AuthStateManager.context.globalState.get( - AuthStateManager.AUTH_STATE_KEY, - ); - console.log('[AuthStateManager] Auth state:', a); - return a; - } - - /** - * Get auth state info for debugging - */ - async getAuthInfo(): Promise { - const state = await this.getAuthState(); - if (!state) { - return 'No cached auth'; - } - - const age = Math.floor((Date.now() - state.timestamp) / 1000 / 60); - return `Auth cached ${age}m ago, method: ${state.authMethod}`; - } -} diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index dd0712b7..5ddd5612 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -11,7 +11,6 @@ import type { } from '../types/acpTypes.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionManager } from './qwenSessionManager.js'; -import type { AuthStateManager } from './authStateManager.js'; import type { ChatMessage, PlanEntry, @@ -42,9 +41,7 @@ export class QwenAgentManager { // session/update notifications. We set this flag to route message chunks // (user/assistant) as discrete chat messages instead of live streaming. private rehydratingSessionId: string | null = null; - // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) - // can reuse it and avoid forcing a fresh authentication unnecessarily. - private defaultAuthStateManager?: AuthStateManager; + // CLI is now the single source of truth for authentication state // Deduplicate concurrent session/new attempts private sessionCreateInFlight: Promise | null = null; @@ -165,22 +162,14 @@ export class QwenAgentManager { * Connect to Qwen service * * @param workingDir - Working directory - * @param authStateManager - Authentication state manager (optional) * @param cliPath - CLI path (optional, if provided will override the path in configuration) */ - async connect( - workingDir: string, - authStateManager?: AuthStateManager, - _cliPath?: string, - ): Promise { + async connect(workingDir: string, _cliPath?: string): Promise { this.currentWorkingDir = workingDir; - // Remember the provided authStateManager for future calls - this.defaultAuthStateManager = authStateManager; await this.connectionHandler.connect( this.connection, this.sessionReader, workingDir, - authStateManager, _cliPath, ); } @@ -1181,10 +1170,7 @@ export class QwenAgentManager { * @param workingDir - Working directory * @returns Newly created session ID */ - async createNewSession( - workingDir: string, - authStateManager?: AuthStateManager, - ): Promise { + async createNewSession(workingDir: string): Promise { // Reuse existing session if present if (this.connection.currentSessionId) { return this.connection.currentSessionId; @@ -1195,15 +1181,10 @@ export class QwenAgentManager { } console.log('[QwenAgentManager] Creating new session...'); - // Prefer the provided authStateManager, otherwise fall back to the one - // remembered during connect(). This prevents accidental re-auth in - // fallback paths (e.g. session switching) when the handler didn't pass it. - const effectiveAuth = authStateManager || this.defaultAuthStateManager; this.sessionCreateInFlight = (async () => { try { - // Try to create a new ACP session. If Qwen asks for auth despite our - // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. + // Try to create a new ACP session. If Qwen asks for auth, let it handle authentication. try { await this.connection.newSession(workingDir); } catch (err) { @@ -1217,18 +1198,16 @@ export class QwenAgentManager { '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', ); try { + // Let CLI handle authentication - it's the single source of truth await this.connection.authenticate(authMethod); - // Persist auth cache so subsequent calls can skip the web flow. - if (effectiveAuth) { - await effectiveAuth.saveAuthState(workingDir, authMethod); - } - await setTimeout(() => Promise.resolve(), 300); // slight delay to ensure auth state is settled + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); await this.connection.newSession(workingDir); } catch (reauthErr) { - // Clear potentially stale cache on failure and rethrow - if (effectiveAuth) { - await effectiveAuth.clearAuthState(); - } + console.error( + '[QwenAgentManager] Re-authentication failed:', + reauthErr, + ); throw reauthErr; } } else { diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 278032a2..e8b6a978 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -13,7 +13,6 @@ import * as vscode from 'vscode'; import type { AcpConnection } from './acpConnection.js'; import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import type { AuthStateManager } from '../services/authStateManager.js'; import { CliVersionManager, MIN_CLI_VERSION_FOR_SESSION_METHODS, @@ -32,14 +31,12 @@ export class QwenConnectionHandler { * @param connection - ACP connection instance * @param sessionReader - Session reader instance * @param workingDir - Working directory - * @param authStateManager - Authentication state manager (optional) * @param cliPath - CLI path (optional, if provided will override the path in configuration) */ async connect( connection: AcpConnection, sessionReader: QwenSessionReader, workingDir: string, - authStateManager?: AuthStateManager, cliPath?: string, ): Promise { const connectId = Date.now(); @@ -72,21 +69,6 @@ export class QwenConnectionHandler { await connection.connect(effectiveCliPath, workingDir, extraArgs); - // Check if we have valid cached authentication - if (authStateManager) { - console.log('[QwenAgentManager] Checking for cached authentication...'); - console.log('[QwenAgentManager] Working dir:', workingDir); - console.log('[QwenAgentManager] Auth method:', authMethod); - - const hasValidAuth = await authStateManager.hasValidAuth( - workingDir, - authMethod, - ); - console.log('[QwenAgentManager] Has valid auth:', hasValidAuth); - } else { - console.log('[QwenAgentManager] No authStateManager provided'); - } - // 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 @@ -99,77 +81,15 @@ export class QwenConnectionHandler { '[QwenAgentManager] no sessionRestored, Creating new session...', ); - // Check if we have valid cached authentication - let hasValidAuth = false; - if (authStateManager) { - hasValidAuth = await authStateManager.hasValidAuth( - workingDir, - authMethod, - ); - } - - // Only authenticate if we don't have valid cached auth - if (!hasValidAuth) { - console.log( - '[QwenAgentManager] Authenticating before creating session...', - ); - try { - await connection.authenticate(authMethod); - console.log('[QwenAgentManager] Authentication successful'); - - // Save auth state - if (authStateManager) { - await authStateManager.saveAuthState(workingDir, authMethod); - console.log('[QwenAgentManager] Auth state save completed'); - } - } catch (authError) { - console.error('[QwenAgentManager] Authentication failed:', authError); - // Clear potentially invalid cache - if (authStateManager) { - console.log( - '[QwenAgentManager] Clearing auth cache due to authentication failure', - ); - await authStateManager.clearAuthState(); - } - throw authError; - } - } else { - console.log( - '[QwenAgentManager] Skipping authentication - using valid cached auth', - ); - } - try { - await setTimeout(() => Promise.resolve(), 300); // slight delay to ensure auth state is settled console.log( - '[QwenAgentManager] Creating new session after authentication...', - ); - await this.newSessionWithRetry( - connection, - workingDir, - 3, - authMethod, - authStateManager, + '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', ); + await this.newSessionWithRetry(connection, workingDir, 3, authMethod); console.log('[QwenAgentManager] New session created successfully'); - - // Ensure auth state is saved (prevent repeated authentication) - if (authStateManager) { - console.log( - '[QwenAgentManager] Saving auth state after successful session creation', - ); - await authStateManager.saveAuthState(workingDir, authMethod); - } } catch (sessionError) { console.log(`\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`); console.log(`[QwenAgentManager] Error details:`, sessionError); - - // Clear cache - if (authStateManager) { - console.log('[QwenAgentManager] Clearing auth cache due to failure'); - await authStateManager.clearAuthState(); - } - throw sessionError; } } @@ -191,7 +111,6 @@ export class QwenConnectionHandler { workingDir: string, maxRetries: number, authMethod: string, - authStateManager?: AuthStateManager, ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { @@ -219,10 +138,10 @@ export class QwenConnectionHandler { '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', ); try { + // Let CLI handle authentication - it's the single source of truth await connection.authenticate(authMethod); - if (authStateManager) { - await authStateManager.saveAuthState(workingDir, authMethod); - } + // 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( @@ -234,9 +153,6 @@ export class QwenConnectionHandler { '[QwenAgentManager] Re-authentication failed:', authErr, ); - if (authStateManager) { - await authStateManager.clearAuthState(); - } // Fall through to retry logic below } } diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index bfa9a567..12b5a99c 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -9,20 +9,18 @@ import { QwenAgentManager } from '../services/qwenAgentManager.js'; import { ConversationStore } from '../services/conversationStore.js'; import type { AcpPermissionRequest } from '../types/acpTypes.js'; import { CliDetector } from '../cli/cliDetector.js'; -import { AuthStateManager } from '../services/authStateManager.js'; import { PanelManager } from '../webview/PanelManager.js'; import { MessageHandler } from '../webview/MessageHandler.js'; import { WebViewContent } from '../webview/WebViewContent.js'; import { CliInstaller } from '../cli/cliInstaller.js'; import { getFileName } from './utils/webviewUtils.js'; -import { authMethod, type ApprovalModeValue } from '../types/acpTypes.js'; +import { type ApprovalModeValue } from '../types/acpTypes.js'; export class WebViewProvider { private panelManager: PanelManager; private messageHandler: MessageHandler; private agentManager: QwenAgentManager; private conversationStore: ConversationStore; - private authStateManager: AuthStateManager; private disposables: vscode.Disposable[] = []; private agentInitialized = false; // Track if agent has been initialized // Track a pending permission request and its resolver so extension commands @@ -39,7 +37,6 @@ export class WebViewProvider { ) { this.agentManager = new QwenAgentManager(); this.conversationStore = new ConversationStore(context); - this.authStateManager = AuthStateManager.getInstance(context); this.panelManager = new PanelManager(extensionUri, () => { // Panel dispose callback this.disposables.forEach((d) => d.dispose()); @@ -519,43 +516,21 @@ export class WebViewProvider { /** * Attempt to restore authentication state and initialize connection * This is called when the webview is first shown + * + * In the new architecture, let CLI handle authentication state management */ 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 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 + console.log( + '[WebViewProvider] Attempting connection (letting CLI handle authentication)...', + ); + // In the new architecture, always attempt connection and let CLI handle authentication + await this.initializeAgentConnection(); + } catch (error) { + console.error( + '[WebViewProvider] Error in attemptAuthStateRestoration:', + error, + ); await this.initializeEmptyConversation(); } } @@ -565,9 +540,8 @@ export class WebViewProvider { * Can be called from show() or via /login command */ async initializeAgentConnection(): Promise { - return AuthStateManager.runExclusiveAuth(() => - this.doInitializeAgentConnection(), - ); + // In the new architecture, let CLI handle authentication without local state caching + return this.doInitializeAgentConnection(); } /** @@ -582,10 +556,7 @@ export class WebViewProvider { '[WebViewProvider] Starting initialization, workingDir:', workingDir, ); - console.log( - '[WebViewProvider] AuthStateManager available:', - !!this.authStateManager, - ); + console.log('[WebViewProvider] Using CLI-managed authentication'); // Check if CLI is installed before attempting to connect const cliDetection = await CliDetector.detectQwenCli(); @@ -613,19 +584,10 @@ export class WebViewProvider { try { console.log('[WebViewProvider] Connecting to agent...'); - console.log( - '[WebViewProvider] Using authStateManager:', - !!this.authStateManager, - ); - const authInfo = await this.authStateManager.getAuthInfo(); - console.log('[WebViewProvider] Auth cache status:', authInfo); // Pass the detected CLI path to ensure we use the correct installation - await this.agentManager.connect( - workingDir, - this.authStateManager, - cliDetection.cliPath, - ); + // In the new architecture, let CLI handle authentication without local state caching + await this.agentManager.connect(workingDir, cliDetection.cliPath); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; @@ -639,8 +601,6 @@ export class WebViewProvider { }); } catch (_error) { console.error('[WebViewProvider] Agent connection error:', _error); - // Clear auth cache on error (might be auth issue) - await this.authStateManager.clearAuthState(); vscode.window.showWarningMessage( `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, ); @@ -668,91 +628,65 @@ export class WebViewProvider { */ async forceReLogin(): Promise { console.log('[WebViewProvider] Force re-login requested'); - console.log( - '[WebViewProvider] Current authStateManager:', - !!this.authStateManager, - ); - // If a login/connection process is already running, reuse it to avoid double prompts - const p = Promise.resolve( - vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - cancellable: false, - }, - async (progress) => { - try { - progress.report({ message: 'Preparing sign-in...' }); + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Logging in to Qwen Code... ', + cancellable: false, + }, + async (progress) => { + try { + progress.report({ message: 'Preparing sign-in...' }); - // Clear existing auth cache - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - console.log('[WebViewProvider] Auth cache cleared'); - } else { - console.log('[WebViewProvider] No authStateManager to clear'); + // Disconnect existing connection if any + if (this.agentInitialized) { + try { + this.agentManager.disconnect(); + console.log('[WebViewProvider] Existing connection disconnected'); + } catch (_error) { + console.log('[WebViewProvider] Error disconnecting:', _error); } - - // Disconnect existing connection if any - if (this.agentInitialized) { - try { - this.agentManager.disconnect(); - console.log( - '[WebViewProvider] Existing connection disconnected', - ); - } catch (_error) { - console.log('[WebViewProvider] Error disconnecting:', _error); - } - this.agentInitialized = false; - } - - // Wait a moment for cleanup to complete - await new Promise((resolve) => setTimeout(resolve, 300)); - - progress.report({ - message: 'Connecting to CLI and starting sign-in...', - }); - - // Reinitialize connection (will trigger fresh authentication) - await this.doInitializeAgentConnection(); - console.log( - '[WebViewProvider] Force re-login completed successfully', - ); - - // Ensure auth state is saved after successful re-login - if (this.authStateManager) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log('[WebViewProvider] Auth state saved after re-login'); - } - - // Send success notification to WebView - this.sendMessageToWebView({ - type: 'loginSuccess', - data: { message: 'Successfully logged in!' }, - }); - } catch (_error) { - console.error('[WebViewProvider] Force re-login failed:', _error); - console.error( - '[WebViewProvider] Error stack:', - _error instanceof Error ? _error.stack : 'N/A', - ); - - // Send error notification to WebView - this.sendMessageToWebView({ - type: 'loginError', - data: { - message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, - }, - }); - - throw _error; + this.agentInitialized = false; } - }, - ), - ); - return AuthStateManager.runExclusiveAuth(() => p); + // Wait a moment for cleanup to complete + await new Promise((resolve) => setTimeout(resolve, 300)); + + progress.report({ + message: 'Connecting to CLI and starting sign-in...', + }); + + // Reinitialize connection (will trigger fresh authentication) + await this.doInitializeAgentConnection(); + console.log( + '[WebViewProvider] Force re-login completed successfully', + ); + + // Send success notification to WebView + this.sendMessageToWebView({ + type: 'loginSuccess', + data: { message: 'Successfully logged in!' }, + }); + } catch (_error) { + console.error('[WebViewProvider] Force re-login failed:', _error); + console.error( + '[WebViewProvider] Error stack:', + _error instanceof Error ? _error.stack : 'N/A', + ); + + // Send error notification to WebView + this.sendMessageToWebView({ + type: 'loginError', + data: { + message: `Login failed: ${_error instanceof Error ? _error.message : String(_error)}`, + }, + }); + + throw _error; + } + }, + ); } /** @@ -819,19 +753,14 @@ export class WebViewProvider { // avoid creating another session if connect() already created one. if (!this.agentManager.currentSessionId) { try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); + await this.agentManager.createNewSession(workingDir); console.log('[WebViewProvider] ACP session created successfully'); - // Ensure auth state is saved after successful session creation - if (this.authStateManager) { - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log( - '[WebViewProvider] Auth state saved after session creation', - ); - } + // In the new architecture, CLI handles authentication state + // No need to save auth state locally anymore + console.log( + '[WebViewProvider] Session created successfully (CLI manages auth state)', + ); } catch (sessionError) { console.error( '[WebViewProvider] Failed to create ACP session:', @@ -1003,17 +932,6 @@ export class WebViewProvider { this.agentManager.disconnect(); } - /** - * Clear authentication cache for this WebViewProvider instance - */ - async clearAuthCache(): Promise { - console.log('[WebViewProvider] Clearing auth cache for this instance'); - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - this.resetAgentState(); - } - } - /** * Restore an existing WebView panel (called during VSCode restart) * This sets up the panel with all event listeners @@ -1021,8 +939,7 @@ export class WebViewProvider { async restorePanel(panel: vscode.WebviewPanel): Promise { console.log('[WebViewProvider] Restoring WebView panel'); console.log( - '[WebViewProvider] Current authStateManager in restore:', - !!this.authStateManager, + '[WebViewProvider] Using CLI-managed authentication in restore', ); this.panelManager.setPanel(panel); @@ -1225,10 +1142,7 @@ export class WebViewProvider { const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); // Create new Qwen session via agent manager - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); + await this.agentManager.createNewSession(workingDir); // Clear current conversation UI this.sendMessageToWebView({ From d812c9dcf22aa0572c09d62cde9aac49f7ae708b Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 12 Dec 2025 13:51:14 +0800 Subject: [PATCH 6/7] chore(vscode-ide-companion): add fixme comment for auth delay --- .../vscode-ide-companion/src/services/qwenConnectionHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index e8b6a978..78731734 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -140,6 +140,8 @@ export class QwenConnectionHandler { try { // Let CLI handle authentication - it's the single source of truth 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 From 89be6edb5ec7eab1a83443b4af2f62827bd0e6be Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Fri, 12 Dec 2025 13:59:05 +0800 Subject: [PATCH 7/7] chore(vscode-ide-companion): add comment --- .../src/services/acpConnection.ts | 1 - .../src/services/qwenConnectionHandler.ts | 1 - .../src/webview/WebViewProvider.ts | 16 +++------------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 9fd548cc..5486e14d 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -31,7 +31,6 @@ export class AcpConnection { private child: ChildProcess | null = null; private pendingRequests = new Map>(); private nextRequestId = { value: 0 }; - // Remember the working dir provided at connect() so later ACP calls // that require cwd (e.g. session/list) can include it. private workingDir: string = process.cwd(); diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 78731734..6a74cd56 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -138,7 +138,6 @@ export class QwenConnectionHandler { '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', ); try { - // Let CLI handle authentication - it's the single source of truth 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 diff --git a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts index 12b5a99c..b4da60ab 100644 --- a/packages/vscode-ide-companion/src/webview/WebViewProvider.ts +++ b/packages/vscode-ide-companion/src/webview/WebViewProvider.ts @@ -131,7 +131,7 @@ export class WebViewProvider { // Note: Tool call updates are handled in handleSessionUpdate within QwenAgentManager // and sent via onStreamChunk callback this.agentManager.onToolCall((update) => { - // Always surface tool calls; they are part of the live assistant process. + // Always surface tool calls; they are part of the live assistant flow. // Cast update to access sessionUpdate property const updateData = update as unknown as Record; @@ -516,15 +516,13 @@ export class WebViewProvider { /** * Attempt to restore authentication state and initialize connection * This is called when the webview is first shown - * - * In the new architecture, let CLI handle authentication state management */ private async attemptAuthStateRestoration(): Promise { try { console.log( - '[WebViewProvider] Attempting connection (letting CLI handle authentication)...', + '[WebViewProvider] Attempting connection (CLI handle authentication)...', ); - // In the new architecture, always attempt connection and let CLI handle authentication + //always attempt connection and let CLI handle authentication await this.initializeAgentConnection(); } catch (error) { console.error( @@ -540,7 +538,6 @@ export class WebViewProvider { * Can be called from show() or via /login command */ async initializeAgentConnection(): Promise { - // In the new architecture, let CLI handle authentication without local state caching return this.doInitializeAgentConnection(); } @@ -586,7 +583,6 @@ export class WebViewProvider { console.log('[WebViewProvider] Connecting to agent...'); // Pass the detected CLI path to ensure we use the correct installation - // In the new architecture, let CLI handle authentication without local state caching await this.agentManager.connect(workingDir, cliDetection.cliPath); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; @@ -755,12 +751,6 @@ export class WebViewProvider { try { await this.agentManager.createNewSession(workingDir); console.log('[WebViewProvider] ACP session created successfully'); - - // In the new architecture, CLI handles authentication state - // No need to save auth state locally anymore - console.log( - '[WebViewProvider] Session created successfully (CLI manages auth state)', - ); } catch (sessionError) { console.error( '[WebViewProvider] Failed to create ACP session:',