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

@@ -45,7 +45,6 @@ import { AcpSessionManager } from './acpSessionManager.js';
*
* Custom Methods (Not in standard ACP):
* ⚠️ session/list - List available sessions (custom extension)
* ⚠️ session/switch - Switch to different session (custom extension)
*/
export class AcpConnection {
private child: ChildProcess | null = null;
@@ -70,12 +69,12 @@ export class AcpConnection {
}
/**
* 连接到ACP后端
* Connect to ACP backend
*
* @param backend - 后端类型
* @param cliPath - CLI路径
* @param workingDir - 工作目录
* @param extraArgs - 额外的命令行参数
* @param backend - Backend type
* @param cliPath - CLI path
* @param workingDir - Working directory
* @param extraArgs - Extra command line arguments
*/
async connect(
backend: AcpBackend,
@@ -92,8 +91,8 @@ export class AcpConnection {
const isWindows = process.platform === 'win32';
const env = { ...process.env };
// 如果在extraArgs中配置了代理也将其设置为环境变量
// 这确保token刷新请求也使用代理
// If proxy is configured in extraArgs, also set it as environment variable
// This ensures token refresh requests also use the proxy
const proxyArg = extraArgs.find(
(arg, i) => arg === '--proxy' && i + 1 < extraArgs.length,
);
@@ -134,9 +133,9 @@ export class AcpConnection {
}
/**
* 设置子进程处理器
* Set up child process handlers
*
* @param backend - 后端名称
* @param backend - Backend name
*/
private async setupChildProcessHandlers(backend: string): Promise<void> {
let spawnError: Error | null = null;
@@ -163,7 +162,7 @@ export class AcpConnection {
);
});
// 等待进程启动
// Wait for process to start
await new Promise((resolve) => setTimeout(resolve, 1000));
if (spawnError) {
@@ -174,7 +173,7 @@ export class AcpConnection {
throw new Error(`${backend} ACP process failed to start`);
}
// 处理来自ACP服务器的消息
// Handle messages from ACP server
let buffer = '';
this.child.stdout?.on('data', (data) => {
buffer += data.toString();
@@ -191,7 +190,7 @@ export class AcpConnection {
);
this.handleMessage(message);
} catch (_error) {
// 忽略非JSON行
// Ignore non-JSON lines
console.log(
'[ACP] <<< Non-JSON line (ignored):',
line.substring(0, 200),
@@ -212,9 +211,9 @@ export class AcpConnection {
}
/**
* 处理接收到的消息
* Handle received messages
*
* @param message - ACP消息
* @param message - ACP message
*/
private handleMessage(message: AcpMessage): void {
const callbacks: AcpConnectionCallbacks = {
@@ -223,9 +222,9 @@ export class AcpConnection {
onEndTurn: this.onEndTurn,
};
// 处理消息
// Handle message
if ('method' in message) {
// 请求或通知
// Request or notification
this.messageHandler
.handleIncomingRequest(message, callbacks)
.then((result) => {
@@ -260,10 +259,10 @@ export class AcpConnection {
}
/**
* 认证
* Authenticate
*
* @param methodId - 认证方法ID
* @returns 认证响应
* @param methodId - Authentication method ID
* @returns Authentication response
*/
async authenticate(methodId?: string): Promise<AcpResponse> {
return this.sessionManager.authenticate(
@@ -275,10 +274,10 @@ export class AcpConnection {
}
/**
* 创建新会话
* Create new session
*
* @param cwd - 工作目录
* @returns 新会话响应
* @param cwd - Working directory
* @returns New session response
*/
async newSession(cwd: string = process.cwd()): Promise<AcpResponse> {
return this.sessionManager.newSession(
@@ -290,10 +289,10 @@ export class AcpConnection {
}
/**
* 发送提示消息
* Send prompt message
*
* @param prompt - 提示内容
* @returns 响应
* @param prompt - Prompt content
* @returns Response
*/
async sendPrompt(prompt: string): Promise<AcpResponse> {
return this.sessionManager.sendPrompt(
@@ -305,10 +304,10 @@ export class AcpConnection {
}
/**
* 加载已有会话
* Load existing session
*
* @param sessionId - 会话ID
* @returns 加载响应
* @param sessionId - Session ID
* @returns Load response
*/
async loadSession(sessionId: string): Promise<AcpResponse> {
return this.sessionManager.loadSession(
@@ -320,9 +319,9 @@ export class AcpConnection {
}
/**
* 获取会话列表
* Get session list
*
* @returns 会话列表响应
* @returns Session list response
*/
async listSessions(): Promise<AcpResponse> {
return this.sessionManager.listSessions(
@@ -333,27 +332,27 @@ export class AcpConnection {
}
/**
* 切换到指定会话
* Switch to specified session
*
* @param sessionId - 会话ID
* @returns 切换响应
* @param sessionId - Session ID
* @returns Switch response
*/
async switchSession(sessionId: string): Promise<AcpResponse> {
return this.sessionManager.switchSession(sessionId, this.nextRequestId);
}
/**
* 取消当前会话的提示生成
* Cancel current session prompt generation
*/
async cancelSession(): Promise<void> {
await this.sessionManager.cancelSession(this.child);
}
/**
* 保存当前会话
* Save current session
*
* @param tag - 保存标签
* @returns 保存响应
* @param tag - Save tag
* @returns Save response
*/
async saveSession(tag: string): Promise<AcpResponse> {
return this.sessionManager.saveSession(
@@ -365,7 +364,7 @@ export class AcpConnection {
}
/**
* 断开连接
* Disconnect
*/
disconnect(): void {
if (this.child) {
@@ -379,21 +378,21 @@ export class AcpConnection {
}
/**
* 检查是否已连接
* Check if connected
*/
get isConnected(): boolean {
return this.child !== null && !this.child.killed;
}
/**
* 检查是否有活动会话
* Check if there is an active session
*/
get hasActiveSession(): boolean {
return this.sessionManager.getCurrentSessionId() !== null;
}
/**
* 获取当前会话ID
* Get current session ID
*/
get currentSessionId(): string | null {
return this.sessionManager.getCurrentSessionId();

View File

@@ -55,5 +55,4 @@ export const CLIENT_METHODS = {
*/
export const CUSTOM_METHODS = {
session_list: 'session/list',
session_switch: 'session/switch',
} as const;

View File

@@ -70,13 +70,21 @@ export class QwenConnectionHandler {
// Check if we have valid cached authentication
if (authStateManager) {
console.log('[QwenAgentManager] Checking for cached authentication...');
console.log('[QwenAgentManager] Working dir:', workingDir);
console.log('[QwenAgentManager] Auth method:', authMethod);
const hasValidAuth = await authStateManager.hasValidAuth(
workingDir,
authMethod,
);
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
if (hasValidAuth) {
console.log('[QwenAgentManager] Using cached authentication');
} else {
console.log('[QwenAgentManager] No valid cached authentication found');
}
} else {
console.log('[QwenAgentManager] No authStateManager provided');
}
// Try to restore existing session or create new session
@@ -156,7 +164,10 @@ export class QwenConnectionHandler {
console.log(
'[QwenAgentManager] Saving auth state after successful authentication',
);
console.log('[QwenAgentManager] Working dir for save:', workingDir);
console.log('[QwenAgentManager] Auth method for save:', authMethod);
await authStateManager.saveAuthState(workingDir, authMethod);
console.log('[QwenAgentManager] Auth state save completed');
}
} catch (authError) {
console.error('[QwenAgentManager] Authentication failed:', authError);

View File

@@ -37,6 +37,7 @@ export class AuthStateManager {
workingDir: state.workingDir,
authMethod: state.authMethod,
timestamp: new Date(state.timestamp).toISOString(),
isAuthenticated: state.isAuthenticated,
});
console.log('[AuthStateManager] Checking against:', {
workingDir,
@@ -50,6 +51,11 @@ export class AuthStateManager {
if (isExpired) {
console.log('[AuthStateManager] Cached auth expired');
console.log(
'[AuthStateManager] Cache age:',
Math.floor((now - state.timestamp) / 1000 / 60),
'minutes',
);
await this.clearAuthState();
return false;
}
@@ -60,6 +66,10 @@ export class AuthStateManager {
if (!isSameContext) {
console.log('[AuthStateManager] Working dir or auth method changed');
console.log('[AuthStateManager] Cached workingDir:', state.workingDir);
console.log('[AuthStateManager] Current workingDir:', workingDir);
console.log('[AuthStateManager] Cached authMethod:', state.authMethod);
console.log('[AuthStateManager] Current authMethod:', authMethod);
return false;
}
@@ -78,31 +88,54 @@ export class AuthStateManager {
timestamp: Date.now(),
};
console.log('[AuthStateManager] Saving auth state:', {
workingDir,
authMethod,
timestamp: new Date(state.timestamp).toISOString(),
});
await this.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
state,
);
console.log('[AuthStateManager] Auth state saved');
// Verify the state was saved correctly
const savedState = await this.getAuthState();
console.log('[AuthStateManager] Verified saved state:', savedState);
}
/**
* Clear authentication state
*/
async clearAuthState(): Promise<void> {
console.log('[AuthStateManager] Clearing auth state');
const currentState = await this.getAuthState();
console.log(
'[AuthStateManager] Current state before clearing:',
currentState,
);
await this.context.globalState.update(
AuthStateManager.AUTH_STATE_KEY,
undefined,
);
console.log('[AuthStateManager] Auth state cleared');
// Verify the state was cleared
const newState = await this.getAuthState();
console.log('[AuthStateManager] State after clearing:', newState);
}
/**
* Get current auth state
*/
private async getAuthState(): Promise<AuthState | undefined> {
return this.context.globalState.get<AuthState>(
const a = this.context.globalState.get<AuthState>(
AuthStateManager.AUTH_STATE_KEY,
);
console.log('[AuthStateManager] Auth state:', a);
return a;
}
/**

View File

@@ -81,8 +81,6 @@ export class DiffManager {
* @param newContent The modified content (right side)
*/
async showDiff(filePath: string, oldContent: string, newContent: string) {
const _fileUri = vscode.Uri.file(filePath);
// Left side: old content using qwen-diff scheme
const leftDocUri = vscode.Uri.from({
scheme: DIFF_SCHEME,
@@ -118,6 +116,7 @@ export class DiffManager {
rightDocUri,
diffTitle,
{
viewColumn: vscode.ViewColumn.Beside,
preview: false,
preserveFocus: true,
},

View File

@@ -14,7 +14,7 @@ import {
IDE_DEFINITIONS,
type IdeInfo,
} from '@qwen-code/qwen-code-core/src/ide/detect-ide.js';
import { WebViewProvider } from './WebViewProvider.js';
import { WebViewProvider } from './webview/WebViewProvider.js';
import { AuthStateManager } from './auth/authStateManager.js';
const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion';
@@ -115,12 +115,29 @@ export async function activate(context: vscode.ExtensionContext) {
const diffManager = new DiffManager(log, diffContentProvider);
// Initialize Auth State Manager
console.log('[Extension] Initializing global AuthStateManager');
authStateManager = new AuthStateManager(context);
console.log(
'[Extension] Global AuthStateManager initialized:',
!!authStateManager,
);
// Helper function to create a new WebView provider instance
const createWebViewProvider = (): WebViewProvider => {
const provider = new WebViewProvider(context, context.extensionUri);
console.log(
'[Extension] Creating WebViewProvider with global AuthStateManager:',
!!authStateManager,
);
const provider = new WebViewProvider(
context,
context.extensionUri,
authStateManager,
);
webViewProviders.push(provider);
console.log(
'[Extension] WebViewProvider created, total providers:',
webViewProviders.length,
);
return provider;
};
@@ -138,19 +155,26 @@ export async function activate(context: vscode.ExtensionContext) {
// Create a new provider for the restored panel
const provider = createWebViewProvider();
provider.restorePanel(webviewPanel);
console.log('[Extension] Provider created for deserialization');
// Restore state if available
// Restore state if available BEFORE restoring the panel
if (state && typeof state === 'object') {
console.log('[Extension] Restoring state:', state);
provider.restoreState(
state as {
conversationId: string | null;
agentInitialized: boolean;
},
);
} else {
console.log('[Extension] No state to restore or invalid state');
}
await provider.restorePanel(webviewPanel);
console.log('[Extension] Panel restore completed');
log('WebView panel restored from serialization');
return true;
},
}),
);

View File

@@ -1,42 +0,0 @@
// Type declarations for tailwindUtils.js
export function buttonClasses(
variant?: 'primary' | 'secondary' | 'ghost' | 'icon',
disabled?: boolean,
): string;
export function inputClasses(): string;
export function cardClasses(): string;
export function dialogClasses(): string;
export function qwenColorClasses(
color: 'orange' | 'clay-orange' | 'ivory' | 'slate' | 'green',
): string;
export function spacingClasses(
size?: 'small' | 'medium' | 'large' | 'xlarge',
direction?: 'all' | 'x' | 'y' | 't' | 'r' | 'b' | 'l',
): string;
export function borderRadiusClasses(
size?: 'small' | 'medium' | 'large',
): string;
export const commonClasses: {
flexCenter: string;
flexBetween: string;
flexCol: string;
textMuted: string;
textSmall: string;
textLarge: string;
fontWeightMedium: string;
fontWeightSemibold: string;
marginAuto: string;
fullWidth: string;
fullHeight: string;
truncate: string;
srOnly: string;
transition: string;
};

View File

@@ -1,131 +0,0 @@
// Tailwind CSS 工具类集合
// 用于封装常用的样式组合,便于在组件中复用
/**
* 生成按钮样式类
* @param {string} variant - 按钮变体: 'primary', 'secondary', 'ghost', 'icon'
* @param {boolean} disabled - 是否禁用
* @returns {string} Tailwind类字符串
*/
export const buttonClasses = (variant = 'primary', disabled = false) => {
const baseClasses = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
const variantClasses = {
primary: 'bg-qwen-orange text-qwen-ivory hover:bg-qwen-clay-orange shadow-sm',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700',
ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800',
icon: 'hover:bg-gray-100 dark:hover:bg-gray-800 p-1'
};
const disabledClasses = disabled ? 'opacity-50 pointer-events-none' : '';
return `${baseClasses} ${variantClasses[variant] || variantClasses.primary} ${disabledClasses}`;
};
/**
* 生成输入框样式类
* @returns {string} Tailwind类字符串
*/
export const inputClasses = () => {
return 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50';
};
/**
* 生成卡片样式类
* @returns {string} Tailwind类字符串
*/
export const cardClasses = () => {
return 'rounded-lg border bg-card text-card-foreground shadow-sm';
};
/**
* 生成对话框样式类
* @returns {string} Tailwind类字符串
*/
export const dialogClasses = () => {
return 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50';
};
/**
* 生成Qwen品牌颜色类
* @param {string} color - 颜色名称: 'orange', 'clay-orange', 'ivory', 'slate', 'green'
* @returns {string} Tailwind类字符串
*/
export const qwenColorClasses = (color) => {
const colorMap = {
'orange': 'text-qwen-orange',
'clay-orange': 'text-qwen-clay-orange',
'ivory': 'text-qwen-ivory',
'slate': 'text-qwen-slate',
'green': 'text-qwen-green'
};
return colorMap[color] || 'text-qwen-orange';
};
/**
* 生成间距类
* @param {string} size - 尺寸: 'small', 'medium', 'large', 'xlarge'
* @param {string} direction - 方向: 'all', 'x', 'y', 't', 'r', 'b', 'l'
* @returns {string} Tailwind类字符串
*/
export const spacingClasses = (size = 'medium', direction = 'all') => {
const sizeMap = {
'small': 'small',
'medium': 'medium',
'large': 'large',
'xlarge': 'xlarge'
};
const directionMap = {
'all': 'p',
'x': 'px',
'y': 'py',
't': 'pt',
'r': 'pr',
'b': 'pb',
'l': 'pl'
};
return `${directionMap[direction]}-${sizeMap[size]}`;
};
/**
* 生成圆角类
* @param {string} size - 尺寸: 'small', 'medium', 'large'
* @returns {string} Tailwind类字符串
*/
export const borderRadiusClasses = (size = 'medium') => {
const sizeMap = {
'small': 'rounded-small',
'medium': 'rounded-medium',
'large': 'rounded-large'
};
return sizeMap[size] || 'rounded-medium';
};
// 导出常用的类组合
export const commonClasses = {
// 布局类
flexCenter: 'flex items-center justify-center',
flexBetween: 'flex items-center justify-between',
flexCol: 'flex flex-col',
// 文本类
textMuted: 'text-gray-500 dark:text-gray-400',
textSmall: 'text-sm',
textLarge: 'text-lg',
fontWeightMedium: 'font-medium',
fontWeightSemibold: 'font-semibold',
// 间距类
marginAuto: 'm-auto',
fullWidth: 'w-full',
fullHeight: 'h-full',
// 其他常用类
truncate: 'truncate',
srOnly: 'sr-only',
transition: 'transition-all duration-200 ease-in-out'
};

View File

@@ -42,7 +42,7 @@ export class QwenSessionReader {
}
/**
* 获取所有会话列表(可选:仅当前项目或所有项目)
* Get all session list (optional: current project only or all projects)
*/
async getAllSessions(
workingDir?: string,
@@ -52,13 +52,13 @@ export class QwenSessionReader {
const sessions: QwenSession[] = [];
if (!allProjects && workingDir) {
// 仅当前项目
// Current project only
const projectHash = await this.getProjectHash(workingDir);
const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
const projectSessions = await this.readSessionsFromDir(chatsDir);
sessions.push(...projectSessions);
} else {
// 所有项目
// All projects
const tmpDir = path.join(this.qwenDir, 'tmp');
if (!fs.existsSync(tmpDir)) {
console.log('[QwenSessionReader] Tmp directory not found:', tmpDir);
@@ -73,7 +73,7 @@ export class QwenSessionReader {
}
}
// 按最后更新时间排序
// Sort by last updated time
sessions.sort(
(a, b) =>
new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime(),
@@ -87,7 +87,7 @@ export class QwenSessionReader {
}
/**
* 从指定目录读取所有会话
* Read all sessions from specified directory
*/
private async readSessionsFromDir(chatsDir: string): Promise<QwenSession[]> {
const sessions: QwenSession[] = [];
@@ -120,7 +120,7 @@ export class QwenSessionReader {
}
/**
* 获取特定会话的详情
* Get details of specific session
*/
async getSession(
sessionId: string,
@@ -132,8 +132,8 @@ export class QwenSessionReader {
}
/**
* 计算项目 hash需要与 Qwen CLI 一致)
* Qwen CLI 使用项目路径的 SHA256 hash
* Calculate project hash (needs to be consistent with Qwen CLI)
* Qwen CLI uses SHA256 hash of project path
*/
private async getProjectHash(workingDir: string): Promise<string> {
const crypto = await import('crypto');
@@ -141,12 +141,12 @@ export class QwenSessionReader {
}
/**
* 获取会话的标题(基于第一条用户消息)
* Get session title (based on first user message)
*/
getSessionTitle(session: QwenSession): string {
const firstUserMessage = session.messages.find((m) => m.type === 'user');
if (firstUserMessage) {
// 截取前50个字符作为标题
// Extract first 50 characters as title
return (
firstUserMessage.content.substring(0, 50) +
(firstUserMessage.content.length > 50 ? '...' : '')
@@ -156,7 +156,7 @@ export class QwenSessionReader {
}
/**
* 删除会话文件
* Delete session file
*/
async deleteSession(
sessionId: string,

View File

@@ -5,7 +5,7 @@
*/
import * as vscode from 'vscode';
import { CliDetector } from '../utils/cliDetector.js';
import { CliDetector } from './cliDetector.js';
/**
* CLI

View File

@@ -5,21 +5,21 @@
*/
/**
* 从完整路径中提取文件名
* @param fsPath 文件的完整路径
* @returns 文件名(不含路径)
* Extract filename from full path
* @param fsPath Full path of the file
* @returns Filename (without path)
*/
export function getFileName(fsPath: string): string {
// 使用 path.basename 的逻辑:找到最后一个路径分隔符后的部分
// Use path.basename logic: find the part after the last path separator
const lastSlash = Math.max(fsPath.lastIndexOf('/'), fsPath.lastIndexOf('\\'));
return lastSlash >= 0 ? fsPath.substring(lastSlash + 1) : fsPath;
}
/**
* HTML 转义函数,防止 XSS 攻击
* 将特殊字符转换为 HTML 实体
* @param text 需要转义的文本
* @returns 转义后的文本
* HTML escape function to prevent XSS attacks
* Convert special characters to HTML entities
* @param text Text to escape
* @returns Escaped text
*/
export function escapeHtml(text: string): string {
return text

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

@@ -5,16 +5,16 @@
*/
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 './webview/PanelManager.js';
import { MessageHandler } from './webview/MessageHandler.js';
import { WebViewContent } from './webview/WebViewContent.js';
import { CliInstaller } from './webview/CliInstaller.js';
import { getFileName } from './utils/webviewUtils.js';
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;
@@ -28,10 +28,12 @@ export class WebViewProvider {
constructor(
context: vscode.ExtensionContext,
private extensionUri: vscode.Uri,
authStateManager?: AuthStateManager, // 可选的全局AuthStateManager实例
) {
this.agentManager = new QwenAgentManager();
this.conversationStore = new ConversationStore(context);
this.authStateManager = new AuthStateManager(context);
// 如果提供了全局的authStateManager则使用它否则创建新的实例
this.authStateManager = authStateManager || new AuthStateManager(context);
this.panelManager = new PanelManager(extensionUri, () => {
// Panel dispose callback
this.disposables.forEach((d) => d.dispose());
@@ -174,6 +176,13 @@ export class WebViewProvider {
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();
@@ -293,19 +302,50 @@ export class WebViewProvider {
});
}
// Don't auto-login; user must use /login command
// Just initialize empty conversation for the UI
if (!this.agentInitialized) {
console.log(
'[WebViewProvider] Agent not initialized, waiting for /login command',
// 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,
);
await this.initializeEmptyConversation();
} else {
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();
}
}
@@ -352,6 +392,10 @@ export class WebViewProvider {
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);
@@ -385,10 +429,18 @@ export class WebViewProvider {
*/
async forceReLogin(): Promise<void> {
console.log('[WebViewProvider] Force re-login requested');
console.log(
'[WebViewProvider] Current authStateManager:',
!!this.authStateManager,
);
// Clear existing auth cache
await this.authStateManager.clearAuthState();
console.log('[WebViewProvider] Auth cache cleared');
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) {
@@ -555,6 +607,10 @@ export class WebViewProvider {
*/
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);
@@ -643,10 +699,41 @@ export class WebViewProvider {
console.log('[WebViewProvider] Panel restored successfully');
// Refresh connection on restore (will use cached auth if available)
if (this.agentInitialized) {
// 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] Agent was initialized, refreshing connection...',
'[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();
@@ -659,8 +746,9 @@ export class WebViewProvider {
}
} else {
console.log(
'[WebViewProvider] Agent not initialized, waiting for /login command',
'[WebViewProvider] No valid cached auth or agent already initialized, showing empty conversation',
);
// Just initialize empty conversation for the UI
await this.initializeEmptyConversation();
}
}
@@ -673,10 +761,28 @@ export class WebViewProvider {
conversationId: string | null;
agentInitialized: boolean;
} {
return {
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();
}
/**
@@ -689,6 +795,10 @@ export class WebViewProvider {
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();

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 '';