/** * @license * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ import * as vscode from 'vscode'; import { QwenAgentManager } from '../agents/qwenAgentManager.js'; import { ConversationStore } from '../storage/conversationStore.js'; import type { AcpPermissionRequest } from '../constants/acpTypes.js'; import { CliDetector } from '../cli/cliDetector.js'; import { AuthStateManager } from '../auth/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 } from '../auth/index.js'; import { runQwenCodeCommand } from '../commands/index.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 // can "simulate" user choice from the command palette (e.g. after accepting // a diff, auto-allow read/execute, or auto-reject on cancel). private pendingPermissionRequest: AcpPermissionRequest | null = null; private pendingPermissionResolve: ((optionId: string) => void) | null = null; // Track current ACP mode id to influence permission/diff behavior private currentModeId: 'plan' | 'default' | 'auto-edit' | 'yolo' | null = null; constructor( context: vscode.ExtensionContext, private extensionUri: vscode.Uri, ) { this.agentManager = new QwenAgentManager(); this.conversationStore = new ConversationStore(context); this.authStateManager = new AuthStateManager(context); this.panelManager = new PanelManager(extensionUri, () => { // Panel dispose callback this.disposables.forEach((d) => d.dispose()); }); this.messageHandler = new MessageHandler( this.agentManager, this.conversationStore, null, (message) => this.sendMessageToWebView(message), ); // Set login handler for /login command - direct force re-login this.messageHandler.setLoginHandler(async () => { await this.forceReLogin(); }); // Setup agent callbacks this.agentManager.onStreamChunk((chunk: string) => { // Ignore stream chunks from background /chat save commands if (this.messageHandler.getIsSavingCheckpoint()) { console.log( '[WebViewProvider] Ignoring stream chunk from /chat save command', ); return; } this.messageHandler.appendStreamContent(chunk); this.sendMessageToWebView({ type: 'streamChunk', data: { chunk }, }); }); // Setup thought chunk handler this.agentManager.onThoughtChunk((chunk: string) => { // Ignore thought chunks from background /chat save commands if (this.messageHandler.getIsSavingCheckpoint()) { console.log( '[WebViewProvider] Ignoring thought chunk from /chat save command', ); return; } this.messageHandler.appendStreamContent(chunk); this.sendMessageToWebView({ type: 'thoughtChunk', data: { chunk }, }); }); // Surface available modes and current mode (from ACP initialize) this.agentManager.onModeInfo((info) => { try { const current = (info?.currentModeId || null) as | 'plan' | 'default' | 'auto-edit' | 'yolo' | null; this.currentModeId = current; } catch (_error) { // Ignore error when parsing mode info } this.sendMessageToWebView({ type: 'modeInfo', data: info || {}, }); }); // Surface mode changes (from ACP or immediate set_mode response) this.agentManager.onModeChanged((modeId) => { try { this.currentModeId = modeId; } catch (_error) { // Ignore error when setting mode id } this.sendMessageToWebView({ type: 'modeChanged', data: { modeId }, }); }); // Setup end-turn handler from ACP stopReason=end_turn this.agentManager.onEndTurn(() => { // Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere this.sendMessageToWebView({ type: 'streamEnd', data: { timestamp: Date.now(), reason: 'end_turn' }, }); }); // Note: Tool call updates are handled in handleSessionUpdate within QwenAgentManager // and sent via onStreamChunk callback this.agentManager.onToolCall((update) => { // Ignore tool calls from background /chat save commands if (this.messageHandler.getIsSavingCheckpoint()) { console.log( '[WebViewProvider] Ignoring tool call from /chat save command', ); return; } // Cast update to access sessionUpdate property const updateData = update as unknown as Record; // Determine message type from sessionUpdate field // If sessionUpdate is missing, infer from content: // - If has kind/title/rawInput, it's likely initial tool_call // - If only has status/content updates, it's tool_call_update let messageType = updateData.sessionUpdate as string | undefined; if (!messageType) { // Infer type: if has kind or title, assume initial call; otherwise update if (updateData.kind || updateData.title || updateData.rawInput) { messageType = 'tool_call'; } else { messageType = 'tool_call_update'; } } this.sendMessageToWebView({ type: 'toolCall', data: { type: messageType, ...updateData, }, }); }); // Setup plan handler this.agentManager.onPlan((entries) => { this.sendMessageToWebView({ type: 'plan', data: { entries }, }); }); this.agentManager.onPermissionRequest( async (request: AcpPermissionRequest) => { // Auto-approve in auto/yolo mode (no UI, no diff) if (this.isAutoMode()) { const options = request.options || []; const pick = (substr: string) => options.find((o) => (o.optionId || '').toLowerCase().includes(substr), )?.optionId; const pickByKind = (k: string) => options.find((o) => (o.kind || '').toLowerCase().includes(k)) ?.optionId; const optionId = pick('allow_once') || pickByKind('allow') || pick('proceed') || options[0]?.optionId || 'allow_once'; return optionId; } // Send permission request to WebView this.sendMessageToWebView({ type: 'permissionRequest', data: request, }); // Wait for user response return new Promise((resolve) => { // cache the pending request and its resolver so commands can resolve it this.pendingPermissionRequest = request; this.pendingPermissionResolve = (optionId: string) => { try { resolve(optionId); } finally { // Always clear pending state this.pendingPermissionRequest = null; this.pendingPermissionResolve = null; // Also instruct the webview UI to close its drawer if it is open this.sendMessageToWebView({ type: 'permissionResolved', data: { optionId }, }); // If allowed/proceeded, close any open qwen-diff editors and suppress re-open briefly const isCancel = optionId === 'cancel' || optionId.toLowerCase().includes('reject'); if (!isCancel) { try { void vscode.commands.executeCommand('qwen.diff.closeAll'); } catch (err) { console.warn( '[WebViewProvider] Failed to close diffs after allow (resolver):', err, ); } try { void vscode.commands.executeCommand( 'qwen.diff.suppressBriefly', ); } catch (err) { console.warn( '[WebViewProvider] Failed to suppress diffs briefly:', err, ); } } } }; const handler = (message: { type: string; data: { optionId: string }; }) => { if (message.type !== 'permissionResponse') { return; } const optionId = message.data.optionId || ''; // 1) First resolve the optionId back to ACP so the agent isn't blocked this.pendingPermissionResolve?.(optionId); // 2) If user cancelled/rejected, proactively stop current generation const isCancel = optionId === 'cancel' || optionId.toLowerCase().includes('reject'); if (isCancel) { // Fire and forget – do not block the ACP resolve (async () => { try { // Stop server-side generation await this.agentManager.cancelCurrentPrompt(); } catch (err) { console.warn( '[WebViewProvider] cancelCurrentPrompt error:', err, ); } // Ensure the webview exits streaming state immediately this.sendMessageToWebView({ type: 'streamEnd', data: { timestamp: Date.now(), reason: 'user_cancelled' }, }); // Synthesize a failed tool_call_update to match Claude/CLI UX try { const toolCallId = (request.toolCall as { toolCallId?: string } | undefined) ?.toolCallId || ''; const title = (request.toolCall as { title?: string } | undefined) ?.title || ''; // Normalize kind for UI – fall back to 'execute' let kind = (( request.toolCall as { kind?: string } | undefined )?.kind || 'execute') as string; if (!kind && title) { const t = title.toLowerCase(); if (t.includes('read') || t.includes('cat')) { kind = 'read'; } else if (t.includes('write') || t.includes('edit')) { kind = 'edit'; } else { kind = 'execute'; } } this.sendMessageToWebView({ type: 'toolCall', data: { type: 'tool_call_update', toolCallId, title, kind, status: 'failed', // Best-effort pass-through (used by UI hints) rawInput: (request.toolCall as { rawInput?: unknown }) ?.rawInput, locations: ( request.toolCall as { locations?: Array<{ path: string; line?: number | null; }>; } )?.locations, }, }); } catch (err) { console.warn( '[WebViewProvider] failed to synthesize failed tool_call_update:', err, ); } })(); } // If user allowed/proceeded, proactively close any open qwen-diff editors and suppress re-open briefly else { try { void vscode.commands.executeCommand('qwen.diff.closeAll'); } catch (err) { console.warn( '[WebViewProvider] Failed to close diffs after allow:', err, ); } try { void vscode.commands.executeCommand( 'qwen.diff.suppressBriefly', ); } catch (err) { console.warn( '[WebViewProvider] Failed to suppress diffs briefly:', err, ); } } }; // Store handler in message handler this.messageHandler.setPermissionHandler(handler); }); }, ); } async show(): Promise { const panel = this.panelManager.getPanel(); if (panel) { // Reveal the existing panel this.panelManager.revealPanel(true); this.panelManager.captureTab(); return; } // Create new panel const isNewPanel = await this.panelManager.createPanel(); if (!isNewPanel) { return; // Failed to create panel } const newPanel = this.panelManager.getPanel(); if (!newPanel) { return; } // Set up state serialization newPanel.onDidChangeViewState(() => { console.log( '[WebViewProvider] Panel view state changed, triggering serialization check', ); }); // Capture the Tab that corresponds to our WebviewPanel this.panelManager.captureTab(); // Auto-lock editor group when opened in new column await this.panelManager.autoLockEditorGroup(); newPanel.webview.html = WebViewContent.generate( newPanel, this.extensionUri, ); // Handle messages from WebView newPanel.webview.onDidReceiveMessage( async (message: { type: string; data?: unknown }) => { // Suppress UI-originated diff opens in auto/yolo mode if (message.type === 'openDiff' && this.isAutoMode()) { return; } // Allow webview to request updating the VS Code tab title if (message.type === 'updatePanelTitle') { const title = String( (message.data as { title?: unknown } | undefined)?.title ?? '', ).trim(); const panelRef = this.panelManager.getPanel(); if (panelRef) { panelRef.title = title || 'Qwen Code'; } return; } await this.messageHandler.route(message); }, null, this.disposables, ); // Listen for view state changes (no pin/lock; just keep tab reference fresh) this.panelManager.registerViewStateChangeHandler(this.disposables); // Register panel dispose handler this.panelManager.registerDisposeHandler(this.disposables); // Listen for active editor changes and notify WebView const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( (editor) => { // If switching to a non-text editor (like webview), keep the last state if (!editor) { // Don't update - keep previous state return; } const filePath = editor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; // Get selection info if there is any selected text let selectionInfo = null; if (editor && !editor.selection.isEmpty) { const selection = editor.selection; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } // Update last known state this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); }, ); this.disposables.push(editorChangeDisposable); // Listen for text selection changes const selectionChangeDisposable = vscode.window.onDidChangeTextEditorSelection((event) => { const editor = event.textEditor; if (editor === vscode.window.activeTextEditor) { const filePath = editor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; // Get selection info if there is any selected text let selectionInfo = null; if (!event.selections[0].isEmpty) { const selection = event.selections[0]; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } // Update last known state this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); // Mode callbacks are registered in constructor; no-op here } }); this.disposables.push(selectionChangeDisposable); // Send initial active editor state to WebView const initialEditor = vscode.window.activeTextEditor; if (initialEditor) { const filePath = initialEditor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; let selectionInfo = null; if (!initialEditor.selection.isEmpty) { const selection = initialEditor.selection; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); } // Attempt to restore authentication state and initialize connection console.log( '[WebViewProvider] Attempting to restore auth state and connection...', ); await this.attemptAuthStateRestoration(); } /** * Attempt to restore authentication state and initialize connection * This is called when the webview is first shown */ private async attemptAuthStateRestoration(): Promise { 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 await this.initializeEmptyConversation(); } } /** * Initialize agent connection and session * 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, ); // 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', ); 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...', ); 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, ); 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] 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), }, }); } } } /** * Force re-login by clearing auth cache and reconnecting * Called when user explicitly uses /login command */ async forceReLogin(): Promise { console.log('[WebViewProvider] Force re-login requested'); console.log( '[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...' }); // 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); } 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.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; } }, ); } /** * Refresh connection without clearing auth cache * Called when restoring WebView after VSCode restart */ async refreshConnection(): Promise { console.log('[WebViewProvider] Refresh connection requested'); // 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, 500)); // Reinitialize connection (will use cached auth if available) try { await this.initializeAgentConnection(); console.log( '[WebViewProvider] Connection refresh completed successfully', ); // Notify webview that agent is connected after refresh this.sendMessageToWebView({ type: 'agentConnected', data: {}, }); } catch (_error) { console.error('[WebViewProvider] Connection refresh failed:', _error); // Notify webview that agent connection failed after refresh this.sendMessageToWebView({ type: 'agentConnectionError', data: { message: _error instanceof Error ? _error.message : String(_error), }, }); throw _error; } } /** * Load messages from current Qwen session * Skips session restoration and creates a new session directly */ private async loadCurrentSessionMessages(): Promise { try { console.log( '[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'); // 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', ); } } catch (sessionError) { console.error( '[WebViewProvider] Failed to create ACP session:', sessionError, ); vscode.window.showWarningMessage( `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, ); } await this.initializeEmptyConversation(); } catch (_error) { console.error( '[WebViewProvider] Failed to load session messages:', _error, ); vscode.window.showErrorMessage( `Failed to load session messages: ${_error}`, ); await this.initializeEmptyConversation(); } } /** * Initialize an empty conversation * Creates a new conversation and notifies WebView */ private async initializeEmptyConversation(): Promise { try { console.log('[WebViewProvider] Initializing empty conversation'); const newConv = await this.conversationStore.createConversation(); this.messageHandler.setCurrentConversationId(newConv.id); this.sendMessageToWebView({ type: 'conversationLoaded', data: newConv, }); console.log( '[WebViewProvider] Empty conversation initialized:', this.messageHandler.getCurrentConversationId(), ); } catch (_error) { console.error( '[WebViewProvider] Failed to initialize conversation:', _error, ); // Send empty state to WebView as fallback this.sendMessageToWebView({ type: 'conversationLoaded', data: { id: 'temp', messages: [] }, }); } } /** * Send message to WebView */ private sendMessageToWebView(message: unknown): void { const panel = this.panelManager.getPanel(); panel?.webview.postMessage(message); } /** * Whether there is a pending permission decision awaiting an option. */ hasPendingPermission(): boolean { return !!this.pendingPermissionResolve; } /** Get current ACP mode id (if known). */ getCurrentModeId(): 'plan' | 'default' | 'auto-edit' | 'yolo' | null { return this.currentModeId; } /** True if diffs/permissions should be auto-handled without prompting. */ isAutoMode(): boolean { return this.currentModeId === 'auto-edit' || this.currentModeId === 'yolo'; } /** Used by extension to decide if diffs should be suppressed. */ shouldSuppressDiff(): boolean { return this.isAutoMode(); } /** * Simulate selecting a permission option while a request drawer is open. * The choice can be a concrete optionId or a shorthand intent. */ respondToPendingPermission( choice: { optionId: string } | 'accept' | 'allow' | 'reject' | 'cancel', ): void { if (!this.pendingPermissionResolve || !this.pendingPermissionRequest) { return; // nothing to do } const options = this.pendingPermissionRequest.options || []; const pickByKind = (substr: string, preferOnce = false) => { const lc = substr.toLowerCase(); const filtered = options.filter((o) => (o.kind || '').toLowerCase().includes(lc), ); if (preferOnce) { const once = filtered.find((o) => (o.optionId || '').toLowerCase().includes('once'), ); if (once) { return once.optionId; } } return filtered[0]?.optionId; }; const pickByOptionId = (substr: string) => options.find((o) => (o.optionId || '').toLowerCase().includes(substr)) ?.optionId; let optionId: string | undefined; if (typeof choice === 'object') { optionId = choice.optionId; } else { const c = choice.toLowerCase(); if (c === 'accept' || c === 'allow') { // Prefer an allow_once/proceed_once style option, then any allow/proceed optionId = pickByKind('allow', true) || pickByOptionId('proceed_once') || pickByKind('allow') || pickByOptionId('proceed') || options[0]?.optionId; // last resort: first option } else if (c === 'cancel' || c === 'reject') { // Prefer explicit cancel, then a reject option optionId = options.find((o) => o.optionId === 'cancel')?.optionId || pickByKind('reject') || pickByOptionId('cancel') || pickByOptionId('reject') || 'cancel'; } } if (!optionId) { return; } try { this.pendingPermissionResolve(optionId); } catch (_error) { console.warn( '[WebViewProvider] respondToPendingPermission failed:', _error, ); } } /** * Reset agent initialization state * Call this when auth cache is cleared to force re-authentication */ resetAgentState(): void { console.log('[WebViewProvider] Resetting agent state'); this.agentInitialized = false; // Disconnect existing connection 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 */ async restorePanel(panel: vscode.WebviewPanel): Promise { console.log('[WebViewProvider] Restoring WebView panel'); console.log( '[WebViewProvider] Current authStateManager in restore:', !!this.authStateManager, ); this.panelManager.setPanel(panel); // Ensure restored tab title starts from default label try { panel.title = 'Qwen Code'; } catch (e) { console.warn( '[WebViewProvider] Failed to reset restored panel title:', e, ); } panel.webview.html = WebViewContent.generate(panel, this.extensionUri); // Handle messages from WebView (restored panel) panel.webview.onDidReceiveMessage( async (message: { type: string; data?: unknown }) => { // Suppress UI-originated diff opens in auto/yolo mode if (message.type === 'openDiff' && this.isAutoMode()) { return; } if (message.type === 'updatePanelTitle') { const title = String( (message.data as { title?: unknown } | undefined)?.title ?? '', ).trim(); const panelRef = this.panelManager.getPanel(); if (panelRef) { panelRef.title = title || 'Qwen Code'; } return; } await this.messageHandler.route(message); }, null, this.disposables, ); // Register view state change handler this.panelManager.registerViewStateChangeHandler(this.disposables); // Register dispose handler this.panelManager.registerDisposeHandler(this.disposables); // Listen for active editor changes and notify WebView const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( (editor) => { // If switching to a non-text editor (like webview), keep the last state if (!editor) { // Don't update - keep previous state return; } const filePath = editor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; // Get selection info if there is any selected text let selectionInfo = null; if (editor && !editor.selection.isEmpty) { const selection = editor.selection; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } // Update last known state this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); }, ); this.disposables.push(editorChangeDisposable); // Send initial active editor state to WebView const initialEditor = vscode.window.activeTextEditor; if (initialEditor) { const filePath = initialEditor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; let selectionInfo = null; if (!initialEditor.selection.isEmpty) { const selection = initialEditor.selection; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); } // Listen for text selection changes (restore path) const selectionChangeDisposableRestore = vscode.window.onDidChangeTextEditorSelection((event) => { const editor = event.textEditor; if (editor === vscode.window.activeTextEditor) { const filePath = editor.document.uri.fsPath || null; const fileName = filePath ? getFileName(filePath) : null; // Get selection info if there is any selected text let selectionInfo = null; if (!event.selections[0].isEmpty) { const selection = event.selections[0]; selectionInfo = { startLine: selection.start.line + 1, endLine: selection.end.line + 1, }; } // Update last known state this.sendMessageToWebView({ type: 'activeEditorChanged', data: { fileName, filePath, selection: selectionInfo }, }); } }); this.disposables.push(selectionChangeDisposableRestore); // Capture the tab reference on restore this.panelManager.captureTab(); console.log('[WebViewProvider] Panel restored successfully'); // Attempt to restore authentication state and initialize connection console.log( '[WebViewProvider] Attempting to restore auth state and connection after restore...', ); await this.attemptAuthStateRestoration(); } /** * Get the current state for serialization * This is used when VSCode restarts to restore the WebView */ getState(): { conversationId: string | null; agentInitialized: boolean; } { console.log('[WebViewProvider] Getting state for serialization'); console.log( '[WebViewProvider] Current conversationId:', this.messageHandler.getCurrentConversationId(), ); console.log( '[WebViewProvider] Current agentInitialized:', this.agentInitialized, ); const state = { conversationId: this.messageHandler.getCurrentConversationId(), agentInitialized: this.agentInitialized, }; console.log('[WebViewProvider] Returning state:', state); return state; } /** * Get the current panel */ getPanel(): vscode.WebviewPanel | null { return this.panelManager.getPanel(); } /** * Restore state after VSCode restart */ restoreState(state: { conversationId: string | null; agentInitialized: boolean; }): void { console.log('[WebViewProvider] Restoring state:', state); this.messageHandler.setCurrentConversationId(state.conversationId); this.agentInitialized = state.agentInitialized; console.log( '[WebViewProvider] State restored. agentInitialized:', this.agentInitialized, ); // Reload content after restore const panel = this.panelManager.getPanel(); if (panel) { panel.webview.html = WebViewContent.generate(panel, this.extensionUri); } } /** * Create a new session in the current panel * This is called when the user clicks the "New Session" button */ async createNewSession(): Promise { console.log('[WebViewProvider] Creating new session in current panel'); // Check if terminal mode is enabled const config = vscode.workspace.getConfiguration('qwenCode'); const useTerminal = config.get('useTerminal', false); if (useTerminal) { // In terminal mode, execute the runQwenCode command to open a new terminal try { await vscode.commands.executeCommand(runQwenCodeCommand); console.log('[WebViewProvider] Opened new terminal session'); } catch (_error) { console.error( '[WebViewProvider] Failed to open new terminal session:', _error, ); vscode.window.showErrorMessage( `Failed to open new terminal session: ${_error}`, ); } return; } // WebView mode - create new session via agent manager try { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); // Create new Qwen session via agent manager await this.agentManager.createNewSession( workingDir, this.authStateManager, ); // 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}`); } } /** * Dispose the WebView provider and clean up resources */ dispose(): void { this.panelManager.dispose(); this.agentManager.disconnect(); this.disposables.forEach((d) => d.dispose()); } }