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:
1132
package-lock.json
generated
1132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,6 @@ import { AcpSessionManager } from './acpSessionManager.js';
|
|||||||
*
|
*
|
||||||
* Custom Methods (Not in standard ACP):
|
* Custom Methods (Not in standard ACP):
|
||||||
* ⚠️ session/list - List available sessions (custom extension)
|
* ⚠️ session/list - List available sessions (custom extension)
|
||||||
* ⚠️ session/switch - Switch to different session (custom extension)
|
|
||||||
*/
|
*/
|
||||||
export class AcpConnection {
|
export class AcpConnection {
|
||||||
private child: ChildProcess | null = null;
|
private child: ChildProcess | null = null;
|
||||||
@@ -70,12 +69,12 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 连接到ACP后端
|
* Connect to ACP backend
|
||||||
*
|
*
|
||||||
* @param backend - 后端类型
|
* @param backend - Backend type
|
||||||
* @param cliPath - CLI路径
|
* @param cliPath - CLI path
|
||||||
* @param workingDir - 工作目录
|
* @param workingDir - Working directory
|
||||||
* @param extraArgs - 额外的命令行参数
|
* @param extraArgs - Extra command line arguments
|
||||||
*/
|
*/
|
||||||
async connect(
|
async connect(
|
||||||
backend: AcpBackend,
|
backend: AcpBackend,
|
||||||
@@ -92,8 +91,8 @@ export class AcpConnection {
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const env = { ...process.env };
|
const env = { ...process.env };
|
||||||
|
|
||||||
// 如果在extraArgs中配置了代理,也将其设置为环境变量
|
// If proxy is configured in extraArgs, also set it as environment variable
|
||||||
// 这确保token刷新请求也使用代理
|
// This ensures token refresh requests also use the proxy
|
||||||
const proxyArg = extraArgs.find(
|
const proxyArg = extraArgs.find(
|
||||||
(arg, i) => arg === '--proxy' && i + 1 < extraArgs.length,
|
(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> {
|
private async setupChildProcessHandlers(backend: string): Promise<void> {
|
||||||
let spawnError: Error | null = null;
|
let spawnError: Error | null = null;
|
||||||
@@ -163,7 +162,7 @@ export class AcpConnection {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 等待进程启动
|
// Wait for process to start
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
if (spawnError) {
|
if (spawnError) {
|
||||||
@@ -174,7 +173,7 @@ export class AcpConnection {
|
|||||||
throw new Error(`${backend} ACP process failed to start`);
|
throw new Error(`${backend} ACP process failed to start`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理来自ACP服务器的消息
|
// Handle messages from ACP server
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
this.child.stdout?.on('data', (data) => {
|
this.child.stdout?.on('data', (data) => {
|
||||||
buffer += data.toString();
|
buffer += data.toString();
|
||||||
@@ -191,7 +190,7 @@ export class AcpConnection {
|
|||||||
);
|
);
|
||||||
this.handleMessage(message);
|
this.handleMessage(message);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// 忽略非JSON行
|
// Ignore non-JSON lines
|
||||||
console.log(
|
console.log(
|
||||||
'[ACP] <<< Non-JSON line (ignored):',
|
'[ACP] <<< Non-JSON line (ignored):',
|
||||||
line.substring(0, 200),
|
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 {
|
private handleMessage(message: AcpMessage): void {
|
||||||
const callbacks: AcpConnectionCallbacks = {
|
const callbacks: AcpConnectionCallbacks = {
|
||||||
@@ -223,9 +222,9 @@ export class AcpConnection {
|
|||||||
onEndTurn: this.onEndTurn,
|
onEndTurn: this.onEndTurn,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理消息
|
// Handle message
|
||||||
if ('method' in message) {
|
if ('method' in message) {
|
||||||
// 请求或通知
|
// Request or notification
|
||||||
this.messageHandler
|
this.messageHandler
|
||||||
.handleIncomingRequest(message, callbacks)
|
.handleIncomingRequest(message, callbacks)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@@ -260,10 +259,10 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 认证
|
* Authenticate
|
||||||
*
|
*
|
||||||
* @param methodId - 认证方法ID
|
* @param methodId - Authentication method ID
|
||||||
* @returns 认证响应
|
* @returns Authentication response
|
||||||
*/
|
*/
|
||||||
async authenticate(methodId?: string): Promise<AcpResponse> {
|
async authenticate(methodId?: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.authenticate(
|
return this.sessionManager.authenticate(
|
||||||
@@ -275,10 +274,10 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新会话
|
* Create new session
|
||||||
*
|
*
|
||||||
* @param cwd - 工作目录
|
* @param cwd - Working directory
|
||||||
* @returns 新会话响应
|
* @returns New session response
|
||||||
*/
|
*/
|
||||||
async newSession(cwd: string = process.cwd()): Promise<AcpResponse> {
|
async newSession(cwd: string = process.cwd()): Promise<AcpResponse> {
|
||||||
return this.sessionManager.newSession(
|
return this.sessionManager.newSession(
|
||||||
@@ -290,10 +289,10 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送提示消息
|
* Send prompt message
|
||||||
*
|
*
|
||||||
* @param prompt - 提示内容
|
* @param prompt - Prompt content
|
||||||
* @returns 响应
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
async sendPrompt(prompt: string): Promise<AcpResponse> {
|
async sendPrompt(prompt: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.sendPrompt(
|
return this.sessionManager.sendPrompt(
|
||||||
@@ -305,10 +304,10 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载已有会话
|
* Load existing session
|
||||||
*
|
*
|
||||||
* @param sessionId - 会话ID
|
* @param sessionId - Session ID
|
||||||
* @returns 加载响应
|
* @returns Load response
|
||||||
*/
|
*/
|
||||||
async loadSession(sessionId: string): Promise<AcpResponse> {
|
async loadSession(sessionId: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.loadSession(
|
return this.sessionManager.loadSession(
|
||||||
@@ -320,9 +319,9 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话列表
|
* Get session list
|
||||||
*
|
*
|
||||||
* @returns 会话列表响应
|
* @returns Session list response
|
||||||
*/
|
*/
|
||||||
async listSessions(): Promise<AcpResponse> {
|
async listSessions(): Promise<AcpResponse> {
|
||||||
return this.sessionManager.listSessions(
|
return this.sessionManager.listSessions(
|
||||||
@@ -333,27 +332,27 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换到指定会话
|
* Switch to specified session
|
||||||
*
|
*
|
||||||
* @param sessionId - 会话ID
|
* @param sessionId - Session ID
|
||||||
* @returns 切换响应
|
* @returns Switch response
|
||||||
*/
|
*/
|
||||||
async switchSession(sessionId: string): Promise<AcpResponse> {
|
async switchSession(sessionId: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.switchSession(sessionId, this.nextRequestId);
|
return this.sessionManager.switchSession(sessionId, this.nextRequestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消当前会话的提示生成
|
* Cancel current session prompt generation
|
||||||
*/
|
*/
|
||||||
async cancelSession(): Promise<void> {
|
async cancelSession(): Promise<void> {
|
||||||
await this.sessionManager.cancelSession(this.child);
|
await this.sessionManager.cancelSession(this.child);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存当前会话
|
* Save current session
|
||||||
*
|
*
|
||||||
* @param tag - 保存标签
|
* @param tag - Save tag
|
||||||
* @returns 保存响应
|
* @returns Save response
|
||||||
*/
|
*/
|
||||||
async saveSession(tag: string): Promise<AcpResponse> {
|
async saveSession(tag: string): Promise<AcpResponse> {
|
||||||
return this.sessionManager.saveSession(
|
return this.sessionManager.saveSession(
|
||||||
@@ -365,7 +364,7 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 断开连接
|
* Disconnect
|
||||||
*/
|
*/
|
||||||
disconnect(): void {
|
disconnect(): void {
|
||||||
if (this.child) {
|
if (this.child) {
|
||||||
@@ -379,21 +378,21 @@ export class AcpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否已连接
|
* Check if connected
|
||||||
*/
|
*/
|
||||||
get isConnected(): boolean {
|
get isConnected(): boolean {
|
||||||
return this.child !== null && !this.child.killed;
|
return this.child !== null && !this.child.killed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否有活动会话
|
* Check if there is an active session
|
||||||
*/
|
*/
|
||||||
get hasActiveSession(): boolean {
|
get hasActiveSession(): boolean {
|
||||||
return this.sessionManager.getCurrentSessionId() !== null;
|
return this.sessionManager.getCurrentSessionId() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前会话ID
|
* Get current session ID
|
||||||
*/
|
*/
|
||||||
get currentSessionId(): string | null {
|
get currentSessionId(): string | null {
|
||||||
return this.sessionManager.getCurrentSessionId();
|
return this.sessionManager.getCurrentSessionId();
|
||||||
|
|||||||
@@ -55,5 +55,4 @@ export const CLIENT_METHODS = {
|
|||||||
*/
|
*/
|
||||||
export const CUSTOM_METHODS = {
|
export const CUSTOM_METHODS = {
|
||||||
session_list: 'session/list',
|
session_list: 'session/list',
|
||||||
session_switch: 'session/switch',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -70,13 +70,21 @@ export class QwenConnectionHandler {
|
|||||||
|
|
||||||
// Check if we have valid cached authentication
|
// Check if we have valid cached authentication
|
||||||
if (authStateManager) {
|
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(
|
const hasValidAuth = await authStateManager.hasValidAuth(
|
||||||
workingDir,
|
workingDir,
|
||||||
authMethod,
|
authMethod,
|
||||||
);
|
);
|
||||||
|
console.log('[QwenAgentManager] Has valid auth:', hasValidAuth);
|
||||||
if (hasValidAuth) {
|
if (hasValidAuth) {
|
||||||
console.log('[QwenAgentManager] Using cached authentication');
|
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
|
// Try to restore existing session or create new session
|
||||||
@@ -156,7 +164,10 @@ export class QwenConnectionHandler {
|
|||||||
console.log(
|
console.log(
|
||||||
'[QwenAgentManager] Saving auth state after successful authentication',
|
'[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);
|
await authStateManager.saveAuthState(workingDir, authMethod);
|
||||||
|
console.log('[QwenAgentManager] Auth state save completed');
|
||||||
}
|
}
|
||||||
} catch (authError) {
|
} catch (authError) {
|
||||||
console.error('[QwenAgentManager] Authentication failed:', authError);
|
console.error('[QwenAgentManager] Authentication failed:', authError);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class AuthStateManager {
|
|||||||
workingDir: state.workingDir,
|
workingDir: state.workingDir,
|
||||||
authMethod: state.authMethod,
|
authMethod: state.authMethod,
|
||||||
timestamp: new Date(state.timestamp).toISOString(),
|
timestamp: new Date(state.timestamp).toISOString(),
|
||||||
|
isAuthenticated: state.isAuthenticated,
|
||||||
});
|
});
|
||||||
console.log('[AuthStateManager] Checking against:', {
|
console.log('[AuthStateManager] Checking against:', {
|
||||||
workingDir,
|
workingDir,
|
||||||
@@ -50,6 +51,11 @@ export class AuthStateManager {
|
|||||||
|
|
||||||
if (isExpired) {
|
if (isExpired) {
|
||||||
console.log('[AuthStateManager] Cached auth expired');
|
console.log('[AuthStateManager] Cached auth expired');
|
||||||
|
console.log(
|
||||||
|
'[AuthStateManager] Cache age:',
|
||||||
|
Math.floor((now - state.timestamp) / 1000 / 60),
|
||||||
|
'minutes',
|
||||||
|
);
|
||||||
await this.clearAuthState();
|
await this.clearAuthState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -60,6 +66,10 @@ export class AuthStateManager {
|
|||||||
|
|
||||||
if (!isSameContext) {
|
if (!isSameContext) {
|
||||||
console.log('[AuthStateManager] Working dir or auth method changed');
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,31 +88,54 @@ export class AuthStateManager {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[AuthStateManager] Saving auth state:', {
|
||||||
|
workingDir,
|
||||||
|
authMethod,
|
||||||
|
timestamp: new Date(state.timestamp).toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
await this.context.globalState.update(
|
await this.context.globalState.update(
|
||||||
AuthStateManager.AUTH_STATE_KEY,
|
AuthStateManager.AUTH_STATE_KEY,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
console.log('[AuthStateManager] Auth state saved');
|
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
|
* Clear authentication state
|
||||||
*/
|
*/
|
||||||
async clearAuthState(): Promise<void> {
|
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(
|
await this.context.globalState.update(
|
||||||
AuthStateManager.AUTH_STATE_KEY,
|
AuthStateManager.AUTH_STATE_KEY,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
console.log('[AuthStateManager] Auth state cleared');
|
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
|
* Get current auth state
|
||||||
*/
|
*/
|
||||||
private async getAuthState(): Promise<AuthState | undefined> {
|
private async getAuthState(): Promise<AuthState | undefined> {
|
||||||
return this.context.globalState.get<AuthState>(
|
const a = this.context.globalState.get<AuthState>(
|
||||||
AuthStateManager.AUTH_STATE_KEY,
|
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)
|
* @param newContent The modified content (right side)
|
||||||
*/
|
*/
|
||||||
async showDiff(filePath: string, oldContent: string, newContent: string) {
|
async showDiff(filePath: string, oldContent: string, newContent: string) {
|
||||||
const _fileUri = vscode.Uri.file(filePath);
|
|
||||||
|
|
||||||
// Left side: old content using qwen-diff scheme
|
// Left side: old content using qwen-diff scheme
|
||||||
const leftDocUri = vscode.Uri.from({
|
const leftDocUri = vscode.Uri.from({
|
||||||
scheme: DIFF_SCHEME,
|
scheme: DIFF_SCHEME,
|
||||||
@@ -118,6 +116,7 @@ export class DiffManager {
|
|||||||
rightDocUri,
|
rightDocUri,
|
||||||
diffTitle,
|
diffTitle,
|
||||||
{
|
{
|
||||||
|
viewColumn: vscode.ViewColumn.Beside,
|
||||||
preview: false,
|
preview: false,
|
||||||
preserveFocus: true,
|
preserveFocus: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
IDE_DEFINITIONS,
|
IDE_DEFINITIONS,
|
||||||
type IdeInfo,
|
type IdeInfo,
|
||||||
} from '@qwen-code/qwen-code-core/src/ide/detect-ide.js';
|
} 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';
|
import { AuthStateManager } from './auth/authStateManager.js';
|
||||||
|
|
||||||
const CLI_IDE_COMPANION_IDENTIFIER = 'qwenlm.qwen-code-vscode-ide-companion';
|
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);
|
const diffManager = new DiffManager(log, diffContentProvider);
|
||||||
|
|
||||||
// Initialize Auth State Manager
|
// Initialize Auth State Manager
|
||||||
|
console.log('[Extension] Initializing global AuthStateManager');
|
||||||
authStateManager = new AuthStateManager(context);
|
authStateManager = new AuthStateManager(context);
|
||||||
|
console.log(
|
||||||
|
'[Extension] Global AuthStateManager initialized:',
|
||||||
|
!!authStateManager,
|
||||||
|
);
|
||||||
|
|
||||||
// Helper function to create a new WebView provider instance
|
// Helper function to create a new WebView provider instance
|
||||||
const createWebViewProvider = (): WebViewProvider => {
|
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);
|
webViewProviders.push(provider);
|
||||||
|
console.log(
|
||||||
|
'[Extension] WebViewProvider created, total providers:',
|
||||||
|
webViewProviders.length,
|
||||||
|
);
|
||||||
return provider;
|
return provider;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,19 +155,26 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
// Create a new provider for the restored panel
|
// Create a new provider for the restored panel
|
||||||
const provider = createWebViewProvider();
|
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') {
|
if (state && typeof state === 'object') {
|
||||||
|
console.log('[Extension] Restoring state:', state);
|
||||||
provider.restoreState(
|
provider.restoreState(
|
||||||
state as {
|
state as {
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
agentInitialized: boolean;
|
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');
|
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(
|
async getAllSessions(
|
||||||
workingDir?: string,
|
workingDir?: string,
|
||||||
@@ -52,13 +52,13 @@ export class QwenSessionReader {
|
|||||||
const sessions: QwenSession[] = [];
|
const sessions: QwenSession[] = [];
|
||||||
|
|
||||||
if (!allProjects && workingDir) {
|
if (!allProjects && workingDir) {
|
||||||
// 仅当前项目
|
// Current project only
|
||||||
const projectHash = await this.getProjectHash(workingDir);
|
const projectHash = await this.getProjectHash(workingDir);
|
||||||
const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
const chatsDir = path.join(this.qwenDir, 'tmp', projectHash, 'chats');
|
||||||
const projectSessions = await this.readSessionsFromDir(chatsDir);
|
const projectSessions = await this.readSessionsFromDir(chatsDir);
|
||||||
sessions.push(...projectSessions);
|
sessions.push(...projectSessions);
|
||||||
} else {
|
} else {
|
||||||
// 所有项目
|
// All projects
|
||||||
const tmpDir = path.join(this.qwenDir, 'tmp');
|
const tmpDir = path.join(this.qwenDir, 'tmp');
|
||||||
if (!fs.existsSync(tmpDir)) {
|
if (!fs.existsSync(tmpDir)) {
|
||||||
console.log('[QwenSessionReader] Tmp directory not found:', tmpDir);
|
console.log('[QwenSessionReader] Tmp directory not found:', tmpDir);
|
||||||
@@ -73,7 +73,7 @@ export class QwenSessionReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按最后更新时间排序
|
// Sort by last updated time
|
||||||
sessions.sort(
|
sessions.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime(),
|
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[]> {
|
private async readSessionsFromDir(chatsDir: string): Promise<QwenSession[]> {
|
||||||
const sessions: QwenSession[] = [];
|
const sessions: QwenSession[] = [];
|
||||||
@@ -120,7 +120,7 @@ export class QwenSessionReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取特定会话的详情
|
* Get details of specific session
|
||||||
*/
|
*/
|
||||||
async getSession(
|
async getSession(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
@@ -132,8 +132,8 @@ export class QwenSessionReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算项目 hash(需要与 Qwen CLI 一致)
|
* Calculate project hash (needs to be consistent with Qwen CLI)
|
||||||
* Qwen CLI 使用项目路径的 SHA256 hash
|
* Qwen CLI uses SHA256 hash of project path
|
||||||
*/
|
*/
|
||||||
private async getProjectHash(workingDir: string): Promise<string> {
|
private async getProjectHash(workingDir: string): Promise<string> {
|
||||||
const crypto = await import('crypto');
|
const crypto = await import('crypto');
|
||||||
@@ -141,12 +141,12 @@ export class QwenSessionReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取会话的标题(基于第一条用户消息)
|
* Get session title (based on first user message)
|
||||||
*/
|
*/
|
||||||
getSessionTitle(session: QwenSession): string {
|
getSessionTitle(session: QwenSession): string {
|
||||||
const firstUserMessage = session.messages.find((m) => m.type === 'user');
|
const firstUserMessage = session.messages.find((m) => m.type === 'user');
|
||||||
if (firstUserMessage) {
|
if (firstUserMessage) {
|
||||||
// 截取前50个字符作为标题
|
// Extract first 50 characters as title
|
||||||
return (
|
return (
|
||||||
firstUserMessage.content.substring(0, 50) +
|
firstUserMessage.content.substring(0, 50) +
|
||||||
(firstUserMessage.content.length > 50 ? '...' : '')
|
(firstUserMessage.content.length > 50 ? '...' : '')
|
||||||
@@ -156,7 +156,7 @@ export class QwenSessionReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除会话文件
|
* Delete session file
|
||||||
*/
|
*/
|
||||||
async deleteSession(
|
async deleteSession(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CliDetector } from '../utils/cliDetector.js';
|
import { CliDetector } from './cliDetector.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLI 检测和安装处理器
|
* CLI 检测和安装处理器
|
||||||
@@ -5,21 +5,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从完整路径中提取文件名
|
* Extract filename from full path
|
||||||
* @param fsPath 文件的完整路径
|
* @param fsPath Full path of the file
|
||||||
* @returns 文件名(不含路径)
|
* @returns Filename (without path)
|
||||||
*/
|
*/
|
||||||
export function getFileName(fsPath: string): string {
|
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('\\'));
|
const lastSlash = Math.max(fsPath.lastIndexOf('/'), fsPath.lastIndexOf('\\'));
|
||||||
return lastSlash >= 0 ? fsPath.substring(lastSlash + 1) : fsPath;
|
return lastSlash >= 0 ? fsPath.substring(lastSlash + 1) : fsPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML 转义函数,防止 XSS 攻击
|
* HTML escape function to prevent XSS attacks
|
||||||
* 将特殊字符转换为 HTML 实体
|
* Convert special characters to HTML entities
|
||||||
* @param text 需要转义的文本
|
* @param text Text to escape
|
||||||
* @returns 转义后的文本
|
* @returns Escaped text
|
||||||
*/
|
*/
|
||||||
export function escapeHtml(text: string): string {
|
export function escapeHtml(text: string): string {
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export class FileOperations {
|
|||||||
newUri,
|
newUri,
|
||||||
`${fileName} (Before ↔ After)`,
|
`${fileName} (Before ↔ After)`,
|
||||||
{
|
{
|
||||||
|
viewColumn: vscode.ViewColumn.Beside,
|
||||||
preview: false,
|
preview: false,
|
||||||
preserveFocus: false,
|
preserveFocus: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class PanelManager {
|
|||||||
* 设置 Panel(用于恢复)
|
* 设置 Panel(用于恢复)
|
||||||
*/
|
*/
|
||||||
setPanel(panel: vscode.WebviewPanel): void {
|
setPanel(panel: vscode.WebviewPanel): void {
|
||||||
|
console.log('[PanelManager] Setting panel for restoration');
|
||||||
this.panel = panel;
|
this.panel = panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +172,6 @@ export class PanelManager {
|
|||||||
console.log(
|
console.log(
|
||||||
'[PanelManager] Skipping auto-lock to allow multiple Qwen Code tabs',
|
'[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 * as vscode from 'vscode';
|
||||||
import { QwenAgentManager } from './agents/qwenAgentManager.js';
|
import { QwenAgentManager } from '../agents/qwenAgentManager.js';
|
||||||
import { ConversationStore } from './storage/conversationStore.js';
|
import { ConversationStore } from '../storage/conversationStore.js';
|
||||||
import type { AcpPermissionRequest } from './shared/acpTypes.js';
|
import type { AcpPermissionRequest } from '../shared/acpTypes.js';
|
||||||
import { CliDetector } from './utils/cliDetector.js';
|
import { CliDetector } from '../utils/cliDetector.js';
|
||||||
import { AuthStateManager } from './auth/authStateManager.js';
|
import { AuthStateManager } from '../auth/authStateManager.js';
|
||||||
import { PanelManager } from './webview/PanelManager.js';
|
import { PanelManager } from './PanelManager.js';
|
||||||
import { MessageHandler } from './webview/MessageHandler.js';
|
import { MessageHandler } from './MessageHandler.js';
|
||||||
import { WebViewContent } from './webview/WebViewContent.js';
|
import { WebViewContent } from './WebViewContent.js';
|
||||||
import { CliInstaller } from './webview/CliInstaller.js';
|
import { CliInstaller } from '../utils/CliInstaller.js';
|
||||||
import { getFileName } from './utils/webviewUtils.js';
|
import { getFileName } from '../utils/webviewUtils.js';
|
||||||
|
|
||||||
export class WebViewProvider {
|
export class WebViewProvider {
|
||||||
private panelManager: PanelManager;
|
private panelManager: PanelManager;
|
||||||
@@ -28,10 +28,12 @@ export class WebViewProvider {
|
|||||||
constructor(
|
constructor(
|
||||||
context: vscode.ExtensionContext,
|
context: vscode.ExtensionContext,
|
||||||
private extensionUri: vscode.Uri,
|
private extensionUri: vscode.Uri,
|
||||||
|
authStateManager?: AuthStateManager, // 可选的全局AuthStateManager实例
|
||||||
) {
|
) {
|
||||||
this.agentManager = new QwenAgentManager();
|
this.agentManager = new QwenAgentManager();
|
||||||
this.conversationStore = new ConversationStore(context);
|
this.conversationStore = new ConversationStore(context);
|
||||||
this.authStateManager = new AuthStateManager(context);
|
// 如果提供了全局的authStateManager,则使用它,否则创建新的实例
|
||||||
|
this.authStateManager = authStateManager || new AuthStateManager(context);
|
||||||
this.panelManager = new PanelManager(extensionUri, () => {
|
this.panelManager = new PanelManager(extensionUri, () => {
|
||||||
// Panel dispose callback
|
// Panel dispose callback
|
||||||
this.disposables.forEach((d) => d.dispose());
|
this.disposables.forEach((d) => d.dispose());
|
||||||
@@ -174,6 +176,13 @@ export class WebViewProvider {
|
|||||||
return;
|
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
|
// Capture the Tab that corresponds to our WebviewPanel
|
||||||
this.panelManager.captureTab();
|
this.panelManager.captureTab();
|
||||||
|
|
||||||
@@ -293,19 +302,50 @@ export class WebViewProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't auto-login; user must use /login command
|
// Smart login restore: Check if we have valid cached auth and restore connection if available
|
||||||
// Just initialize empty conversation for the UI
|
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
|
||||||
if (!this.agentInitialized) {
|
const workingDir = workspaceFolder?.uri.fsPath || process.cwd();
|
||||||
console.log(
|
const config = vscode.workspace.getConfiguration('qwenCode');
|
||||||
'[WebViewProvider] Agent not initialized, waiting for /login command',
|
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();
|
console.log(
|
||||||
} else {
|
'[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(
|
console.log(
|
||||||
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
'[WebViewProvider] Agent already initialized, reusing existing connection',
|
||||||
);
|
);
|
||||||
// Reload current session messages
|
// Reload current session messages
|
||||||
await this.loadCurrentSessionMessages();
|
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 {
|
try {
|
||||||
console.log('[WebViewProvider] Connecting to agent...');
|
console.log('[WebViewProvider] Connecting to agent...');
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Using authStateManager:',
|
||||||
|
!!this.authStateManager,
|
||||||
|
);
|
||||||
const authInfo = await this.authStateManager.getAuthInfo();
|
const authInfo = await this.authStateManager.getAuthInfo();
|
||||||
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
console.log('[WebViewProvider] Auth cache status:', authInfo);
|
||||||
|
|
||||||
@@ -385,10 +429,18 @@ export class WebViewProvider {
|
|||||||
*/
|
*/
|
||||||
async forceReLogin(): Promise<void> {
|
async forceReLogin(): Promise<void> {
|
||||||
console.log('[WebViewProvider] Force re-login requested');
|
console.log('[WebViewProvider] Force re-login requested');
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Current authStateManager:',
|
||||||
|
!!this.authStateManager,
|
||||||
|
);
|
||||||
|
|
||||||
// Clear existing auth cache
|
// Clear existing auth cache
|
||||||
await this.authStateManager.clearAuthState();
|
if (this.authStateManager) {
|
||||||
console.log('[WebViewProvider] Auth cache cleared');
|
await this.authStateManager.clearAuthState();
|
||||||
|
console.log('[WebViewProvider] Auth cache cleared');
|
||||||
|
} else {
|
||||||
|
console.log('[WebViewProvider] No authStateManager to clear');
|
||||||
|
}
|
||||||
|
|
||||||
// Disconnect existing connection if any
|
// Disconnect existing connection if any
|
||||||
if (this.agentInitialized) {
|
if (this.agentInitialized) {
|
||||||
@@ -555,6 +607,10 @@ export class WebViewProvider {
|
|||||||
*/
|
*/
|
||||||
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
|
async restorePanel(panel: vscode.WebviewPanel): Promise<void> {
|
||||||
console.log('[WebViewProvider] Restoring WebView panel');
|
console.log('[WebViewProvider] Restoring WebView panel');
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] Current authStateManager in restore:',
|
||||||
|
!!this.authStateManager,
|
||||||
|
);
|
||||||
this.panelManager.setPanel(panel);
|
this.panelManager.setPanel(panel);
|
||||||
|
|
||||||
panel.webview.html = WebViewContent.generate(panel, this.extensionUri);
|
panel.webview.html = WebViewContent.generate(panel, this.extensionUri);
|
||||||
@@ -643,10 +699,41 @@ export class WebViewProvider {
|
|||||||
|
|
||||||
console.log('[WebViewProvider] Panel restored successfully');
|
console.log('[WebViewProvider] Panel restored successfully');
|
||||||
|
|
||||||
// Refresh connection on restore (will use cached auth if available)
|
// Smart login restore: Check if we have valid cached auth and restore connection if available
|
||||||
if (this.agentInitialized) {
|
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(
|
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 {
|
try {
|
||||||
await this.refreshConnection();
|
await this.refreshConnection();
|
||||||
@@ -659,8 +746,9 @@ export class WebViewProvider {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
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();
|
await this.initializeEmptyConversation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,10 +761,28 @@ export class WebViewProvider {
|
|||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
agentInitialized: boolean;
|
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(),
|
conversationId: this.messageHandler.getCurrentConversationId(),
|
||||||
agentInitialized: this.agentInitialized,
|
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);
|
console.log('[WebViewProvider] Restoring state:', state);
|
||||||
this.messageHandler.setCurrentConversationId(state.conversationId);
|
this.messageHandler.setCurrentConversationId(state.conversationId);
|
||||||
this.agentInitialized = state.agentInitialized;
|
this.agentInitialized = state.agentInitialized;
|
||||||
|
console.log(
|
||||||
|
'[WebViewProvider] State restored. agentInitialized:',
|
||||||
|
this.agentInitialized,
|
||||||
|
);
|
||||||
|
|
||||||
// Reload content after restore
|
// Reload content after restore
|
||||||
const panel = this.panelManager.getPanel();
|
const panel = this.panelManager.getPanel();
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
* Copyright 2025 Qwen Team
|
* Copyright 2025 Qwen Team
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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';
|
import type React from 'react';
|
||||||
@@ -15,24 +15,24 @@ import './FileLink.css';
|
|||||||
* Props for FileLink
|
* Props for FileLink
|
||||||
*/
|
*/
|
||||||
interface FileLinkProps {
|
interface FileLinkProps {
|
||||||
/** 文件路径 */
|
/** File path */
|
||||||
path: string;
|
path: string;
|
||||||
/** 可选的行号(从 1 开始) */
|
/** Optional line number (starting from 1) */
|
||||||
line?: number | null;
|
line?: number | null;
|
||||||
/** 可选的列号(从 1 开始) */
|
/** Optional column number (starting from 1) */
|
||||||
column?: number | null;
|
column?: number | null;
|
||||||
/** 是否显示完整路径,默认 false(只显示文件名) */
|
/** Whether to show full path, default false (show filename only) */
|
||||||
showFullPath?: boolean;
|
showFullPath?: boolean;
|
||||||
/** 可选的自定义类名 */
|
/** Optional custom class name */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** 是否禁用点击行为(当父元素已经处理点击时使用) */
|
/** Whether to disable click behavior (use when parent element handles clicks) */
|
||||||
disableClick?: boolean;
|
disableClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从完整路径中提取文件名
|
* Extract filename from full path
|
||||||
* @param path 文件路径
|
* @param path File path
|
||||||
* @returns 文件名
|
* @returns Filename
|
||||||
*/
|
*/
|
||||||
function getFileName(path: string): string {
|
function getFileName(path: string): string {
|
||||||
const segments = path.split(/[/\\]/);
|
const segments = path.split(/[/\\]/);
|
||||||
@@ -40,13 +40,13 @@ function getFileName(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileLink 组件 - 可点击的文件链接
|
* FileLink component - Clickable file link
|
||||||
*
|
*
|
||||||
* 功能:
|
* Features:
|
||||||
* - 点击打开文件
|
* - Click to open file
|
||||||
* - 支持行号和列号跳转
|
* - Support line and column number navigation
|
||||||
* - 悬停显示完整路径
|
* - Hover to show full path
|
||||||
* - 可选显示模式(完整路径 vs 仅文件名)
|
* - Optional display mode (full path vs filename only)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
@@ -65,22 +65,22 @@ export const FileLink: React.FC<FileLinkProps> = ({
|
|||||||
const vscode = useVSCode();
|
const vscode = useVSCode();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理点击事件 - 发送消息到 VSCode 打开文件
|
* Handle click event - Send message to VSCode to open file
|
||||||
*/
|
*/
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
// 总是阻止默认行为(防止 <a> 标签的 # 跳转)
|
// Always prevent default behavior (prevent <a> tag # navigation)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (disableClick) {
|
if (disableClick) {
|
||||||
// 如果禁用点击,直接返回,不阻止冒泡
|
// If click is disabled, return directly without stopping propagation
|
||||||
// 这样父元素可以处理点击事件
|
// This allows parent elements to handle click events
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果启用点击,阻止事件冒泡
|
// If click is enabled, stop event propagation
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 构建包含行号和列号的完整路径
|
// Build full path including line and column numbers
|
||||||
let fullPath = path;
|
let fullPath = path;
|
||||||
if (line !== null && line !== undefined) {
|
if (line !== null && line !== undefined) {
|
||||||
fullPath += `:${line}`;
|
fullPath += `:${line}`;
|
||||||
@@ -97,10 +97,10 @@ export const FileLink: React.FC<FileLinkProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 构建显示文本
|
// Build display text
|
||||||
const displayPath = showFullPath ? path : getFileName(path);
|
const displayPath = showFullPath ? path : getFileName(path);
|
||||||
|
|
||||||
// 构建悬停提示(始终显示完整路径)
|
// Build hover tooltip (always show full path)
|
||||||
const fullDisplayText =
|
const fullDisplayText =
|
||||||
line !== null && line !== undefined
|
line !== null && line !== undefined
|
||||||
? column !== null && column !== 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 className="absolute left-2 top-[10px] text-[10px] text-[#74c991]">
|
||||||
●
|
●
|
||||||
</span>
|
</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 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
<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 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||||
</div>
|
</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="grid grid-cols-[80px_1fr] gap-3">
|
||||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||||
IN
|
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 relative top-[-0.1em]">⎿</span>
|
||||||
<span className="flex-shrink-0 w-full">{commandText}</span>
|
<span className="flex-shrink-0 w-full">{commandText}</span>
|
||||||
</div>
|
</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="grid grid-cols-[80px_1fr] gap-3">
|
||||||
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
<div className="text-xs text-[var(--app-secondary-foreground)] font-medium pt-[2px]">
|
||||||
IN
|
IN
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const ToolCallContainer: React.FC<ToolCallContainerProps> = ({
|
|||||||
>
|
>
|
||||||
●
|
●
|
||||||
</span>
|
</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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
<span className="text-[13px] font-medium text-[var(--app-primary-foreground)]">
|
||||||
{label}
|
{label}
|
||||||
@@ -195,7 +195,7 @@ interface LocationsListProps {
|
|||||||
* List of file locations with clickable links
|
* List of file locations with clickable links
|
||||||
*/
|
*/
|
||||||
export const LocationsList: React.FC<LocationsListProps> = ({ locations }) => (
|
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) => (
|
{locations.map((loc, idx) => (
|
||||||
<FileLink key={idx} path={loc.path} line={loc.line} showFullPath={true} />
|
<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> {
|
private async handleOpenSettings(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await vscode.commands.executeCommand(
|
// Open settings in a side panel
|
||||||
'workbench.action.openSettings',
|
await vscode.commands.executeCommand('workbench.action.openSettings', {
|
||||||
'qwenCode',
|
// TODO:
|
||||||
);
|
// openToSide: true,
|
||||||
|
query: 'qwenCode',
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[SettingsMessageHandler] Failed to open settings:', error);
|
console.error('[SettingsMessageHandler] Failed to open settings:', error);
|
||||||
vscode.window.showErrorMessage(`Failed to open settings: ${error}`);
|
vscode.window.showErrorMessage(`Failed to open settings: ${error}`);
|
||||||
|
|||||||
@@ -3,35 +3,35 @@
|
|||||||
* Copyright 2025 Qwen Team
|
* Copyright 2025 Qwen Team
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*
|
*
|
||||||
* Diff 统计计算工具
|
* Diff statistics calculation tool
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diff 统计信息
|
* Diff statistics
|
||||||
*/
|
*/
|
||||||
export interface DiffStats {
|
export interface DiffStats {
|
||||||
/** 新增行数 */
|
/** Number of added lines */
|
||||||
added: number;
|
added: number;
|
||||||
/** 删除行数 */
|
/** Number of removed lines */
|
||||||
removed: number;
|
removed: number;
|
||||||
/** 修改行数(估算值) */
|
/** Number of changed lines (estimated value) */
|
||||||
changed: number;
|
changed: number;
|
||||||
/** 总变更行数 */
|
/** Total number of changed lines */
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算两个文本之间的 diff 统计信息
|
* Calculate diff statistics between two texts
|
||||||
*
|
*
|
||||||
* 使用简单的行对比算法(避免引入重量级 diff 库)
|
* Using a simple line comparison algorithm (avoiding heavy-weight diff libraries)
|
||||||
* 算法说明:
|
* Algorithm explanation:
|
||||||
* 1. 将文本按行分割
|
* 1. Split text by lines
|
||||||
* 2. 比较行的集合差异
|
* 2. Compare set differences of lines
|
||||||
* 3. 估算修改行数(同时出现在新增和删除中的行数)
|
* 3. Estimate changed lines (lines that appear in both added and removed)
|
||||||
*
|
*
|
||||||
* @param oldText 旧文本内容
|
* @param oldText Old text content
|
||||||
* @param newText 新文本内容
|
* @param newText New text content
|
||||||
* @returns diff 统计信息
|
* @returns Diff statistics
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -46,15 +46,15 @@ export function calculateDiffStats(
|
|||||||
oldText: string | null | undefined,
|
oldText: string | null | undefined,
|
||||||
newText: string | undefined,
|
newText: string | undefined,
|
||||||
): DiffStats {
|
): DiffStats {
|
||||||
// 处理空值情况
|
// Handle null values
|
||||||
const oldContent = oldText || '';
|
const oldContent = oldText || '';
|
||||||
const newContent = newText || '';
|
const newContent = newText || '';
|
||||||
|
|
||||||
// 按行分割
|
// Split by lines
|
||||||
const oldLines = oldContent.split('\n').filter((line) => line.trim() !== '');
|
const oldLines = oldContent.split('\n').filter((line) => line.trim() !== '');
|
||||||
const newLines = newContent.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) {
|
if (oldLines.length === 0) {
|
||||||
return {
|
return {
|
||||||
added: newLines.length,
|
added: newLines.length,
|
||||||
@@ -73,18 +73,18 @@ export function calculateDiffStats(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Set 进行快速查找
|
// Use Set for fast lookup
|
||||||
const oldSet = new Set(oldLines);
|
const oldSet = new Set(oldLines);
|
||||||
const newSet = new Set(newLines);
|
const newSet = new Set(newLines);
|
||||||
|
|
||||||
// 计算新增:在 new 中但不在 old 中的行
|
// Calculate added: lines in new but not in old
|
||||||
const addedLines = newLines.filter((line) => !oldSet.has(line));
|
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));
|
const removedLines = oldLines.filter((line) => !newSet.has(line));
|
||||||
|
|
||||||
// 估算修改:取较小值(因为修改的行既被删除又被添加)
|
// Estimate changes: take the minimum value (because changed lines are both deleted and added)
|
||||||
// 这是一个简化的估算,实际的 diff 算法会更精确
|
// This is a simplified estimation, actual diff algorithms would be more precise
|
||||||
const estimatedChanged = Math.min(addedLines.length, removedLines.length);
|
const estimatedChanged = Math.min(addedLines.length, removedLines.length);
|
||||||
|
|
||||||
const added = addedLines.length - estimatedChanged;
|
const added = addedLines.length - estimatedChanged;
|
||||||
@@ -100,10 +100,10 @@ export function calculateDiffStats(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化 diff 统计信息为人类可读的文本
|
* Format diff statistics as human-readable text
|
||||||
*
|
*
|
||||||
* @param stats diff 统计信息
|
* @param stats Diff statistics
|
||||||
* @returns 格式化后的文本,例如 "+5 -3 ~2"
|
* @returns Formatted text, e.g. "+5 -3 ~2"
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -130,10 +130,10 @@ export function formatDiffStats(stats: DiffStats): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化详细的 diff 统计信息
|
* Format detailed diff statistics
|
||||||
*
|
*
|
||||||
* @param stats diff 统计信息
|
* @param stats Diff statistics
|
||||||
* @returns 详细的描述文本
|
* @returns Detailed description text
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
|||||||
@@ -33,15 +33,15 @@ function getExtensionUri(): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证 URL 是否为安全的 VS Code webview 资源 URL
|
* Validate if URL is a secure VS Code webview resource URL
|
||||||
* 防止 XSS 攻击
|
* Prevent XSS attacks
|
||||||
*
|
*
|
||||||
* @param url - 待验证的 URL
|
* @param url - URL to validate
|
||||||
* @returns 是否为安全的 URL
|
* @returns Whether it is a secure URL
|
||||||
*/
|
*/
|
||||||
function isValidWebviewUrl(url: string): boolean {
|
function isValidWebviewUrl(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
// VS Code webview 资源 URL 的合法协议
|
// Valid protocols for VS Code webview resource URLs
|
||||||
const allowedProtocols = [
|
const allowedProtocols = [
|
||||||
'vscode-webview-resource:',
|
'vscode-webview-resource:',
|
||||||
'https-vscode-webview-resource:',
|
'https-vscode-webview-resource:',
|
||||||
@@ -49,7 +49,7 @@ function isValidWebviewUrl(url: string): boolean {
|
|||||||
'https:',
|
'https:',
|
||||||
];
|
];
|
||||||
|
|
||||||
// 检查是否以合法协议开头
|
// Check if it starts with a valid protocol
|
||||||
return allowedProtocols.some((protocol) => url.startsWith(protocol));
|
return allowedProtocols.some((protocol) => url.startsWith(protocol));
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -76,7 +76,7 @@ export function generateResourceUrl(relativePath: string): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 extensionUri 是否为安全的 URL
|
// Validate if extensionUri is a secure URL
|
||||||
if (!isValidWebviewUrl(extensionUri)) {
|
if (!isValidWebviewUrl(extensionUri)) {
|
||||||
console.error(
|
console.error(
|
||||||
'[resourceUrl] Invalid extension URI - possible security risk:',
|
'[resourceUrl] Invalid extension URI - possible security risk:',
|
||||||
@@ -97,7 +97,7 @@ export function generateResourceUrl(relativePath: string): string {
|
|||||||
|
|
||||||
const fullUrl = `${baseUri}${cleanPath}`;
|
const fullUrl = `${baseUri}${cleanPath}`;
|
||||||
|
|
||||||
// 验证最终生成的 URL 是否安全
|
// Validate if the final generated URL is secure
|
||||||
if (!isValidWebviewUrl(fullUrl)) {
|
if (!isValidWebviewUrl(fullUrl)) {
|
||||||
console.error('[resourceUrl] Generated URL failed validation:', fullUrl);
|
console.error('[resourceUrl] Generated URL failed validation:', fullUrl);
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user