mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +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:
@@ -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();
|
||||
|
||||
@@ -55,5 +55,4 @@ export const CLIENT_METHODS = {
|
||||
*/
|
||||
export const CUSTOM_METHODS = {
|
||||
session_list: 'session/list',
|
||||
session_switch: 'session/switch',
|
||||
} as const;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { CliDetector } from '../utils/cliDetector.js';
|
||||
import { CliDetector } from './cliDetector.js';
|
||||
|
||||
/**
|
||||
* CLI 检测和安装处理器
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
@@ -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