mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,7 @@ export class FileOperations {
|
||||
newUri,
|
||||
`${fileName} (Before ↔ After)`,
|
||||
{
|
||||
viewColumn: vscode.ViewColumn.Beside,
|
||||
preview: false,
|
||||
preserveFocus: false,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
844
packages/vscode-ide-companion/src/webview/WebViewProvider.ts
Normal file
844
packages/vscode-ide-companion/src/webview/WebViewProvider.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)]">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '';
|
||||
|
||||
Reference in New Issue
Block a user