chore(vscode-ide-companion): update dependencies in package-lock.json

Added new dependencies including:
- @cfworker/json-schema
- @parcel/watcher and related platform-specific packages
- autoprefixer
- browserslist
- chokidar
- Various other utility packages

These updates likely support enhanced functionality and improved compatibility.
This commit is contained in:
yiliang114
2025-11-25 15:30:36 +08:00
parent 579772197a
commit 0cbf95d6b3
22 changed files with 1477 additions and 352 deletions

View File

@@ -1,163 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import { CliDetector } from '../utils/cliDetector.js';
/**
* CLI 检测和安装处理器
* 负责 Qwen CLI 的检测、安装和提示功能
*/
export class CliInstaller {
/**
* 检查 CLI 安装状态并发送结果到 WebView
* @param sendToWebView 发送消息到 WebView 的回调函数
*/
static async checkInstallation(
sendToWebView: (message: unknown) => void,
): Promise<void> {
try {
const result = await CliDetector.detectQwenCli();
sendToWebView({
type: 'cliDetectionResult',
data: {
isInstalled: result.isInstalled,
cliPath: result.cliPath,
version: result.version,
error: result.error,
installInstructions: result.isInstalled
? undefined
: CliDetector.getInstallationInstructions(),
},
});
if (!result.isInstalled) {
console.log('[CliInstaller] Qwen CLI not detected:', result.error);
} else {
console.log(
'[CliInstaller] Qwen CLI detected:',
result.cliPath,
result.version,
);
}
} catch (error) {
console.error('[CliInstaller] CLI detection error:', error);
}
}
/**
* 提示用户安装 CLI
* 显示警告消息,提供安装选项
*/
static async promptInstallation(): Promise<void> {
const selection = await vscode.window.showWarningMessage(
'Qwen Code CLI is not installed. You can browse conversation history, but cannot send new messages.',
'Install Now',
'View Documentation',
'Remind Me Later',
);
if (selection === 'Install Now') {
await this.install();
} else if (selection === 'View Documentation') {
vscode.env.openExternal(
vscode.Uri.parse('https://github.com/QwenLM/qwen-code#installation'),
);
}
}
/**
* 安装 Qwen CLI
* 通过 npm 安装全局 CLI 包
*/
static async install(): Promise<void> {
try {
// Show progress notification
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: 'Installing Qwen Code CLI',
cancellable: false,
},
async (progress) => {
progress.report({
message: 'Running: npm install -g @qwen-code/qwen-code@latest',
});
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
try {
const { stdout, stderr } = await execAsync(
'npm install -g @qwen-code/qwen-code@latest',
{ timeout: 120000 }, // 2 minutes timeout
);
console.log('[CliInstaller] Installation output:', stdout);
if (stderr) {
console.warn('[CliInstaller] Installation stderr:', stderr);
}
// Clear cache and recheck
CliDetector.clearCache();
const detection = await CliDetector.detectQwenCli();
if (detection.isInstalled) {
vscode.window
.showInformationMessage(
`✅ Qwen Code CLI installed successfully! Version: ${detection.version}`,
'Reload Window',
)
.then((selection) => {
if (selection === 'Reload Window') {
vscode.commands.executeCommand(
'workbench.action.reloadWindow',
);
}
});
} else {
throw new Error(
'Installation completed but CLI still not detected',
);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error('[CliInstaller] Installation failed:', errorMessage);
vscode.window
.showErrorMessage(
`Failed to install Qwen Code CLI: ${errorMessage}`,
'Try Manual Installation',
'View Documentation',
)
.then((selection) => {
if (selection === 'Try Manual Installation') {
const terminal = vscode.window.createTerminal(
'Qwen Code Installation',
);
terminal.show();
terminal.sendText(
'npm install -g @qwen-code/qwen-code@latest',
);
} else if (selection === 'View Documentation') {
vscode.env.openExternal(
vscode.Uri.parse(
'https://github.com/QwenLM/qwen-code#installation',
),
);
}
});
}
},
);
} catch (error) {
console.error('[CliInstaller] Install CLI error:', error);
}
}
}

View File

@@ -141,6 +141,7 @@ export class FileOperations {
newUri,
`${fileName} (Before ↔ After)`,
{
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: false,
},

View File

@@ -30,6 +30,7 @@ export class PanelManager {
* 设置 Panel用于恢复
*/
setPanel(panel: vscode.WebviewPanel): void {
console.log('[PanelManager] Setting panel for restoration');
this.panel = panel;
}
@@ -171,19 +172,6 @@ export class PanelManager {
console.log(
'[PanelManager] Skipping auto-lock to allow multiple Qwen Code tabs',
);
// If you want to enable auto-locking for the first tab, uncomment the following:
// const existingQwenInfo = this.findExistingQwenCodeGroup();
// if (!existingQwenInfo) {
// console.log('[PanelManager] First Qwen Code tab, locking editor group');
// try {
// this.revealPanel(false);
// await vscode.commands.executeCommand('workbench.action.lockEditorGroup');
// console.log('[PanelManager] Editor group locked successfully');
// } catch (error) {
// console.warn('[PanelManager] Failed to lock editor group:', error);
// }
// }
}
/**

View File

@@ -0,0 +1,844 @@
/**
* @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 '../shared/acpTypes.js';
import { CliDetector } from '../utils/cliDetector.js';
import { AuthStateManager } from '../auth/authStateManager.js';
import { PanelManager } from './PanelManager.js';
import { MessageHandler } from './MessageHandler.js';
import { WebViewContent } from './WebViewContent.js';
import { CliInstaller } from '../utils/CliInstaller.js';
import { getFileName } from '../utils/webviewUtils.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,
authStateManager?: AuthStateManager, // 可选的全局AuthStateManager实例
) {
this.agentManager = new QwenAgentManager();
this.conversationStore = new ConversationStore(context);
// 如果提供了全局的authStateManager则使用它否则创建新的实例
this.authStateManager = 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 },
});
});
// 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') {
resolve(message.data.optionId);
}
};
// 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 }) => {
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 },
});
}
// Smart login restore: Check if we have valid cached auth and restore connection if available
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
const config = vscode.workspace.getConfiguration('qwenCode');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
// Check if we have valid cached authentication
let hasValidAuth = false;
if (this.authStateManager) {
hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log(
'[WebViewProvider] Has valid cached auth on show:',
hasValidAuth,
);
}
if (hasValidAuth && !this.agentInitialized) {
console.log(
'[WebViewProvider] Found valid cached auth, attempting to restore connection...',
);
try {
await this.initializeAgentConnection();
console.log('[WebViewProvider] Connection restored successfully');
} catch (error) {
console.error('[WebViewProvider] Failed to restore connection:', error);
// Fall back to empty conversation if restore fails
await this.initializeEmptyConversation();
}
} else if (this.agentInitialized) {
console.log(
'[WebViewProvider] Agent already initialized, reusing existing connection',
);
// Reload current session messages
await this.loadCurrentSessionMessages();
} else {
console.log(
'[WebViewProvider] No valid cached auth or agent already initialized, showing empty conversation',
);
// Just initialize empty conversation for the UI
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,
);
const config = vscode.workspace.getConfiguration('qwenCode');
const qwenEnabled = config.get<boolean>('qwen.enabled', true);
if (qwenEnabled) {
// 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);
await this.agentManager.connect(workingDir, this.authStateManager);
console.log('[WebViewProvider] Agent connected successfully');
this.agentInitialized = true;
// Load messages from the current Qwen session
await this.loadCurrentSessionMessages();
} 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();
}
}
} else {
console.log('[WebViewProvider] Qwen agent is disabled in settings');
// Fallback to ConversationStore
await this.initializeEmptyConversation();
}
}
/**
* 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,
);
// 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, 500));
// Reinitialize connection (will trigger fresh authentication)
try {
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);
// Send error notification to WebView
this.sendMessageToWebView({
type: 'loginError',
data: { message: `Login failed: ${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',
);
} catch (error) {
console.error('[WebViewProvider] Connection refresh failed:', error);
throw error;
}
}
/**
* Load messages from current Qwen session
* Creates a new ACP session for immediate message sending
*/
private async loadCurrentSessionMessages(): Promise<void> {
try {
console.log(
'[WebViewProvider] Initializing with empty conversation and creating ACP session',
);
// Create a new ACP session so user can send messages immediately
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
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.`,
);
}
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();
}
/**
* 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);
panel.webview.html = WebViewContent.generate(panel, this.extensionUri);
// Handle messages from WebView
panel.webview.onDidReceiveMessage(
async (message: { type: string; data?: unknown }) => {
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 },
});
}
// Capture the tab reference on restore
this.panelManager.captureTab();
console.log('[WebViewProvider] Panel restored successfully');
// Smart login restore: Check if we have valid cached auth and restore connection if available
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
const config = vscode.workspace.getConfiguration('qwenCode');
const openaiApiKey = config.get<string>('qwen.openaiApiKey', '');
const authMethod = openaiApiKey ? 'openai' : 'qwen-oauth';
// Check if we have valid cached authentication
let hasValidAuth = false;
if (this.authStateManager) {
hasValidAuth = await this.authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log(
'[WebViewProvider] Has valid cached auth on restore:',
hasValidAuth,
);
}
if (hasValidAuth && !this.agentInitialized) {
console.log(
'[WebViewProvider] Found valid cached auth, attempting to restore connection...',
);
try {
await this.initializeAgentConnection();
console.log('[WebViewProvider] Connection restored successfully');
} catch (error) {
console.error('[WebViewProvider] Failed to restore connection:', error);
// Fall back to empty conversation if restore fails
await this.initializeEmptyConversation();
}
} else if (this.agentInitialized) {
console.log(
'[WebViewProvider] Agent already initialized, refreshing connection...',
);
try {
await this.refreshConnection();
console.log('[WebViewProvider] Connection refreshed successfully');
} catch (error) {
console.error('[WebViewProvider] Failed to refresh connection:', error);
// Fall back to empty conversation if refresh fails
this.agentInitialized = false;
await this.initializeEmptyConversation();
}
} else {
console.log(
'[WebViewProvider] No valid cached auth or agent already initialized, showing empty conversation',
);
// Just initialize empty conversation for the UI
await this.initializeEmptyConversation();
}
}
/**
* 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');
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);
// 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());
}
}

View File

@@ -3,8 +3,8 @@
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* FileLink 组件 - 可点击的文件路径链接
* 支持点击打开文件并跳转到指定行号和列号
* FileLink component - Clickable file path links
* Supports clicking to open files and jump to specified line and column numbers
*/
import type React from 'react';
@@ -15,24 +15,24 @@ import './FileLink.css';
* Props for FileLink
*/
interface FileLinkProps {
/** 文件路径 */
/** File path */
path: string;
/** 可选的行号(从 1 开始) */
/** Optional line number (starting from 1) */
line?: number | null;
/** 可选的列号(从 1 开始) */
/** Optional column number (starting from 1) */
column?: number | null;
/** 是否显示完整路径,默认 false只显示文件名 */
/** Whether to show full path, default false (show filename only) */
showFullPath?: boolean;
/** 可选的自定义类名 */
/** Optional custom class name */
className?: string;
/** 是否禁用点击行为(当父元素已经处理点击时使用) */
/** Whether to disable click behavior (use when parent element handles clicks) */
disableClick?: boolean;
}
/**
* 从完整路径中提取文件名
* @param path 文件路径
* @returns 文件名
* Extract filename from full path
* @param path File path
* @returns Filename
*/
function getFileName(path: string): string {
const segments = path.split(/[/\\]/);
@@ -40,13 +40,13 @@ function getFileName(path: string): string {
}
/**
* FileLink 组件 - 可点击的文件链接
* FileLink component - Clickable file link
*
* 功能:
* - 点击打开文件
* - 支持行号和列号跳转
* - 悬停显示完整路径
* - 可选显示模式(完整路径 vs 仅文件名)
* Features:
* - Click to open file
* - Support line and column number navigation
* - Hover to show full path
* - Optional display mode (full path vs filename only)
*
* @example
* ```tsx
@@ -65,22 +65,22 @@ export const FileLink: React.FC<FileLinkProps> = ({
const vscode = useVSCode();
/**
* 处理点击事件 - 发送消息到 VSCode 打开文件
* Handle click event - Send message to VSCode to open file
*/
const handleClick = (e: React.MouseEvent) => {
// 总是阻止默认行为(防止 <a> 标签的 # 跳转)
// Always prevent default behavior (prevent <a> tag # navigation)
e.preventDefault();
if (disableClick) {
// 如果禁用点击,直接返回,不阻止冒泡
// 这样父元素可以处理点击事件
// If click is disabled, return directly without stopping propagation
// This allows parent elements to handle click events
return;
}
// 如果启用点击,阻止事件冒泡
// If click is enabled, stop event propagation
e.stopPropagation();
// 构建包含行号和列号的完整路径
// Build full path including line and column numbers
let fullPath = path;
if (line !== null && line !== undefined) {
fullPath += `:${line}`;
@@ -97,10 +97,10 @@ export const FileLink: React.FC<FileLinkProps> = ({
});
};
// 构建显示文本
// Build display text
const displayPath = showFullPath ? path : getFileName(path);
// 构建悬停提示(始终显示完整路径)
// Build hover tooltip (always show full path)
const fullDisplayText =
line !== null && line !== undefined
? column !== null && column !== undefined

View File

@@ -94,7 +94,7 @@ export const EditToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<span className="absolute left-2 top-[10px] text-[10px] text-[#74c991]">
</span>
<div className="flex flex-col gap-1 pl-[30px]">
<div className="toolcall-edit-content flex flex-col gap-1 pl-[30px] max-w-full">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">

View File

@@ -39,7 +39,7 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{commandText}</span>
</div>
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3">
<div className="execute-toolcall-error-card bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3 max-w-full overflow-x-auto">
<div className="grid grid-cols-[80px_1fr] gap-3">
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
IN
@@ -73,7 +73,7 @@ export const ExecuteToolCall: React.FC<BaseToolCallProps> = ({ toolCall }) => {
<span className="flex-shrink-0 relative top-[-0.1em]"></span>
<span className="flex-shrink-0 w-full">{commandText}</span>
</div>
<div className="bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3">
<div className="execute-toolcall-output-card bg-[var(--app-input-background)] border border-[var(--app-input-border)] rounded-md p-3 flex flex-col gap-3 max-w-full overflow-x-auto">
<div className="grid grid-cols-[80px_1fr] gap-3">
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
IN

View File

@@ -59,7 +59,7 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
>
</span>
<div className="flex flex-col gap-1 pl-[30px]">
<div className="toolcall-content-wrapper flex flex-col gap-1 pl-[30px] max-w-full">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
{label}
@@ -195,7 +195,7 @@ interface LocationsListProps {
* List of file locations with clickable links
*/
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
<div className="flex flex-col gap-1 pl-[30px]">
<div className="toolcall-locations-list flex flex-col gap-1 pl-[30px] max-w-full">
{locations.map((loc, idx) => (
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
))}

View File

@@ -40,10 +40,12 @@ export class SettingsMessageHandler extends BaseMessageHandler {
*/
private async handleOpenSettings(): Promise<void> {
try {
await vscode.commands.executeCommand(
'workbench.action.openSettings',
'qwenCode',
);
// Open settings in a side panel
await vscode.commands.executeCommand('workbench.action.openSettings', {
// TODO:
// openToSide: true,
query: 'qwenCode',
});
} catch (error) {
console.error('[SettingsMessageHandler] Failed to open settings:', error);
vscode.window.showErrorMessage(`Failed to open settings: ${error}`);

View File

@@ -3,35 +3,35 @@
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*
* Diff 统计计算工具
* Diff statistics calculation tool
*/
/**
* Diff 统计信息
* Diff statistics
*/
export interface DiffStats {
/** 新增行数 */
/** Number of added lines */
added: number;
/** 删除行数 */
/** Number of removed lines */
removed: number;
/** 修改行数(估算值) */
/** Number of changed lines (estimated value) */
changed: number;
/** 总变更行数 */
/** Total number of changed lines */
total: number;
}
/**
* 计算两个文本之间的 diff 统计信息
* Calculate diff statistics between two texts
*
* 使用简单的行对比算法(避免引入重量级 diff 库)
* 算法说明:
* 1. 将文本按行分割
* 2. 比较行的集合差异
* 3. 估算修改行数(同时出现在新增和删除中的行数)
* Using a simple line comparison algorithm (avoiding heavy-weight diff libraries)
* Algorithm explanation:
* 1. Split text by lines
* 2. Compare set differences of lines
* 3. Estimate changed lines (lines that appear in both added and removed)
*
* @param oldText 旧文本内容
* @param newText 新文本内容
* @returns diff 统计信息
* @param oldText Old text content
* @param newText New text content
* @returns Diff statistics
*
* @example
* ```typescript
@@ -46,15 +46,15 @@ export function calculateDiffStats(
oldText: string | null | undefined,
newText: string | undefined,
): DiffStats {
// 处理空值情况
// Handle null values
const oldContent = oldText || '';
const newContent = newText || '';
// 按行分割
// Split by lines
const oldLines = oldContent.split('\n').filter((line) => line.trim() !== '');
const newLines = newContent.split('\n').filter((line) => line.trim() !== '');
// 如果其中一个为空,直接计算
// If one of them is empty, calculate directly
if (oldLines.length === 0) {
return {
added: newLines.length,
@@ -73,18 +73,18 @@ export function calculateDiffStats(
};
}
// 使用 Set 进行快速查找
// Use Set for fast lookup
const oldSet = new Set(oldLines);
const newSet = new Set(newLines);
// 计算新增:在 new 中但不在 old 中的行
// Calculate added: lines in new but not in old
const addedLines = newLines.filter((line) => !oldSet.has(line));
// 计算删除:在 old 中但不在 new 中的行
// Calculate removed: lines in old but not in new
const removedLines = oldLines.filter((line) => !newSet.has(line));
// 估算修改:取较小值(因为修改的行既被删除又被添加)
// 这是一个简化的估算,实际的 diff 算法会更精确
// Estimate changes: take the minimum value (because changed lines are both deleted and added)
// This is a simplified estimation, actual diff algorithms would be more precise
const estimatedChanged = Math.min(addedLines.length, removedLines.length);
const added = addedLines.length - estimatedChanged;
@@ -100,10 +100,10 @@ export function calculateDiffStats(
}
/**
* 格式化 diff 统计信息为人类可读的文本
* Format diff statistics as human-readable text
*
* @param stats diff 统计信息
* @returns 格式化后的文本,例如 "+5 -3 ~2"
* @param stats Diff statistics
* @returns Formatted text, e.g. "+5 -3 ~2"
*
* @example
* ```typescript
@@ -130,10 +130,10 @@ export function formatDiffStats(stats: DiffStats): string {
}
/**
* 格式化详细的 diff 统计信息
* Format detailed diff statistics
*
* @param stats diff 统计信息
* @returns 详细的描述文本
* @param stats Diff statistics
* @returns Detailed description text
*
* @example
* ```typescript

View File

@@ -33,15 +33,15 @@ function getExtensionUri(): string | undefined {
}
/**
* 验证 URL 是否为安全的 VS Code webview 资源 URL
* 防止 XSS 攻击
* Validate if URL is a secure VS Code webview resource URL
* Prevent XSS attacks
*
* @param url - 待验证的 URL
* @returns 是否为安全的 URL
* @param url - URL to validate
* @returns Whether it is a secure URL
*/
function isValidWebviewUrl(url: string): boolean {
try {
// VS Code webview 资源 URL 的合法协议
// Valid protocols for VS Code webview resource URLs
const allowedProtocols = [
'vscode-webview-resource:',
'https-vscode-webview-resource:',
@@ -49,7 +49,7 @@ function isValidWebviewUrl(url: string): boolean {
'https:',
];
// 检查是否以合法协议开头
// Check if it starts with a valid protocol
return allowedProtocols.some((protocol) => url.startsWith(protocol));
} catch {
return false;
@@ -76,7 +76,7 @@ export function generateResourceUrl(relativePath: string): string {
return '';
}
// 验证 extensionUri 是否为安全的 URL
// Validate if extensionUri is a secure URL
if (!isValidWebviewUrl(extensionUri)) {
console.error(
'[resourceUrl] Invalid extension URI - possible security risk:',
@@ -97,7 +97,7 @@ export function generateResourceUrl(relativePath: string): string {
const fullUrl = `${baseUri}${cleanPath}`;
// 验证最终生成的 URL 是否安全
// Validate if the final generated URL is secure
if (!isValidWebviewUrl(fullUrl)) {
console.error('[resourceUrl] Generated URL failed validation:', fullUrl);
return '';