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 566a4afb..00000000 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ /dev/null @@ -1,215 +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 - 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; - } - - /** - * 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 a57d15b7..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,9 @@ 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; // Callback storage private callbacks: QwenAgentCallbacks = {}; @@ -163,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, ); } @@ -1179,97 +1170,62 @@ 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; + } + // Deduplicate concurrent session/new attempts + if (this.sessionCreateInFlight) { + return this.sessionCreateInFlight; + } + console.log('[QwenAgentManager] Creating new session...'); - // Check if we have valid cached authentication - let hasValidAuth = false; - // 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, let it handle authentication. 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 { + // Let CLI handle authentication - it's the single source of truth + await this.connection.authenticate(authMethod); + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); + await this.connection.newSession(workingDir); + } catch (reauthErr) { + console.error( + '[QwenAgentManager] Re-authentication failed:', + reauthErr, + ); + throw reauthErr; + } + } else { + throw err; } - throw reauthErr; } - } else { - throw err; + const newSessionId = this.connection.currentSessionId; + console.log( + '[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; } /** diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 11e7199a..6a74cd56 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,81 +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) { - 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'); - } - } 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 { 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; } } @@ -195,7 +111,6 @@ export class QwenConnectionHandler { workingDir: string, maxRetries: number, authMethod: string, - authStateManager?: AuthStateManager, ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { @@ -224,9 +139,10 @@ export class QwenConnectionHandler { ); try { await connection.authenticate(authMethod); - if (authStateManager) { - await authStateManager.saveAuthState(workingDir, authMethod); - } + // FIXME: @yiliang114 If there is no delay for a while, immediately executing + // newSession may cause the cli authorization jump to be triggered again + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); // Retry immediately after successful auth await connection.newSession(workingDir); console.log( @@ -238,9 +154,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/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 4b51d6b6..4bdf6622 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -167,7 +167,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 +487,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<{ @@ -686,7 +702,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..b4da60ab 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()); @@ -522,40 +519,16 @@ export class WebViewProvider { */ 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 (CLI handle authentication)...', + ); + //always attempt connection and let CLI handle authentication + await this.initializeAgentConnection(); + } catch (error) { + console.error( + '[WebViewProvider] Error in attemptAuthStateRestoration:', + error, + ); await this.initializeEmptyConversation(); } } @@ -565,84 +538,84 @@ 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(); + return this.doInitializeAgentConnection(); + } - console.log( - '[WebViewProvider] Starting initialization, workingDir:', - workingDir, - ); - console.log( - '[WebViewProvider] AuthStateManager available:', - !!this.authStateManager, - ); + /** + * 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(); - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); - - if (!cliDetection.isInstalled) { console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + '[WebViewProvider] Starting initialization, workingDir:', + workingDir, ); - console.log('[WebViewProvider] CLI detection error:', cliDetection.error); + console.log('[WebViewProvider] Using CLI-managed authentication'); - // Show VSCode notification with installation option - await CliInstaller.promptInstallation(); + // Check if CLI is installed before attempting to connect + const cliDetection = await CliDetector.detectQwenCli(); - // Initialize empty conversation (can still browse history) - await this.initializeEmptyConversation(); - } else { - console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', - ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); - - try { - console.log('[WebViewProvider] Connecting to agent...'); + if (!cliDetection.isInstalled) { console.log( - '[WebViewProvider] Using authStateManager:', - !!this.authStateManager, + '[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, + console.log( + '[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 { + console.log( + '[WebViewProvider] Qwen CLI detected, attempting connection...', + ); + console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); + console.log('[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 { + console.log('[WebViewProvider] Connecting to agent...'); + + // Pass the detected CLI path to ensure we use the correct installation + await this.agentManager.connect(workingDir, cliDetection.cliPath); + console.log('[WebViewProvider] Agent connected successfully'); + this.agentInitialized = true; + + // Load messages from the current Qwen session + await this.loadCurrentSessionMessages(); + + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } catch (_error) { + console.error('[WebViewProvider] Agent connection error:', _error); + vscode.window.showWarningMessage( + `Failed to 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(); } /** @@ -651,12 +624,8 @@ export class WebViewProvider { */ async forceReLogin(): Promise { console.log('[WebViewProvider] Force re-login requested'); - console.log( - '[WebViewProvider] Current authStateManager:', - !!this.authStateManager, - ); - await vscode.window.withProgress( + return vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: 'Logging in to Qwen Code... ', @@ -666,14 +635,6 @@ export class WebViewProvider { 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 { @@ -693,19 +654,11 @@ export class WebViewProvider { }); // Reinitialize connection (will trigger fresh authentication) - await this.initializeAgentConnection(); + 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', @@ -793,28 +746,23 @@ export class WebViewProvider { 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'); - - // 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', + // avoid creating another session if connect() already created one. + if (!this.agentManager.currentSessionId) { + try { + await this.agentManager.createNewSession(workingDir); + console.log('[WebViewProvider] ACP session created successfully'); + } catch (sessionError) { + console.error( + '[WebViewProvider] Failed to create ACP session:', + sessionError, + ); + vscode.window.showWarningMessage( + `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, ); } - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, + } else { + console.log( + '[WebViewProvider] Existing ACP session detected, skipping new session creation', ); } @@ -974,17 +922,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 @@ -992,8 +929,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); @@ -1196,18 +1132,13 @@ 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({ 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/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/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 741d9684..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,26 +315,37 @@ 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; } + // 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)') + ) { + await this.promptLogin( + 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', + ); + return; + } + vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); + return; + } + } + // Send to agent try { this.resetStreamContent(); @@ -391,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', @@ -428,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; } } @@ -489,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', @@ -525,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); @@ -550,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; } @@ -637,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', @@ -706,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', @@ -755,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', @@ -819,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', @@ -883,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', @@ -931,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', @@ -996,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; @@ -1020,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; } } @@ -1054,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', @@ -1105,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', diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index cd312361..7a3f7e06 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -227,40 +227,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 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/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 }; -}