Files
qwen-code/packages/vscode-ide-companion/src/webview/WebViewProvider.ts

1118 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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
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 },
});
});
// 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<string, unknown>;
// 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) => {
// Send permission request to WebView
this.sendMessageToWebView({
type: 'permissionRequest',
data: request,
});
// Wait for user response
return new Promise((resolve) => {
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
resolve(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,
);
}
})();
}
};
// Store handler in message handler
this.messageHandler.setPermissionHandler(handler);
});
},
);
}
async show(): Promise<void> {
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 }) => {
// 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);
// Track last known editor state (to preserve when switching to webview)
let _lastEditorState: {
fileName: string | null;
filePath: string | null;
selection: {
startLine: number;
endLine: number;
} | null;
} | null = null;
// 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
_lastEditorState = { fileName, filePath, selection: selectionInfo };
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
_lastEditorState = { fileName, filePath, selection: selectionInfo };
this.sendMessageToWebView({
type: 'activeEditorChanged',
data: { fileName, filePath, selection: selectionInfo },
});
}
});
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<void> {
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<void> {
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<void> {
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',
);
// 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<void> {
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
* Attempts to restore an existing session before creating a new one
*/
private async loadCurrentSessionMessages(): Promise<void> {
try {
console.log(
'[WebViewProvider] Initializing with session restoration attempt',
);
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
// First, try to restore an existing session if we have cached auth
if (this.authStateManager) {
const hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
);
if (hasValidAuth) {
console.log(
'[WebViewProvider] Found valid cached auth, attempting session restoration',
);
try {
// Try to create a session (this will use cached auth)
const sessionId = await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
if (sessionId) {
console.log(
'[WebViewProvider] ACP session restored successfully with ID:',
sessionId,
);
} else {
console.log(
'[WebViewProvider] ACP session restoration returned no session ID',
);
}
} catch (restoreError) {
console.warn(
'[WebViewProvider] Failed to restore ACP session:',
restoreError,
);
// Clear invalid auth cache
await this.authStateManager.clearAuthState();
// Fall back to creating a new session
try {
await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
console.log(
'[WebViewProvider] ACP session created successfully after restore failure',
);
} catch (sessionError) {
console.error(
'[WebViewProvider] Failed to create ACP session:',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
);
}
}
} else {
console.log(
'[WebViewProvider] No valid cached auth found, creating new session',
);
// No valid auth, create a new session (will trigger auth if needed)
try {
await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
console.log('[WebViewProvider] ACP session created successfully');
} catch (sessionError) {
console.error(
'[WebViewProvider] Failed to create ACP session:',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
);
}
}
} else {
// No auth state manager, create a new session
console.log(
'[WebViewProvider] No auth state manager, creating new session',
);
try {
await this.agentManager.createNewSession(
workingDir,
this.authStateManager,
);
console.log('[WebViewProvider] ACP session created successfully');
} catch (sessionError) {
console.error(
'[WebViewProvider] Failed to create ACP session:',
sessionError,
);
vscode.window.showWarningMessage(
`Failed to create ACP session: ${sessionError}. You may need to authenticate first.`,
);
}
}
await this.initializeEmptyConversation();
} 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<void> {
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);
}
/**
* 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<void> {
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<void> {
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 }) => {
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);
// Track last known editor state (to preserve when switching to webview)
let _lastEditorState: {
fileName: string | null;
filePath: string | null;
selection: {
startLine: number;
endLine: number;
} | null;
} | null = null;
// 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
_lastEditorState = { fileName, filePath, selection: selectionInfo };
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
_lastEditorState = { fileName, filePath, selection: selectionInfo };
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<void> {
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<boolean>('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());
}
}