mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
wip(vscode-ide-companion): 实现 quick win 功能
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@@ -8,36 +8,71 @@ import { JSONRPC_VERSION } from '../shared/acpTypes.js';
|
||||
import type {
|
||||
AcpBackend,
|
||||
AcpMessage,
|
||||
AcpNotification,
|
||||
AcpPermissionRequest,
|
||||
AcpRequest,
|
||||
AcpResponse,
|
||||
AcpSessionUpdate,
|
||||
} from '../shared/acpTypes.js';
|
||||
import type { ChildProcess, SpawnOptions } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import type { PendingRequest, AcpConnectionCallbacks } from './AcpTypes.js';
|
||||
import { AcpMessageHandler } from './AcpMessageHandler.js';
|
||||
import { AcpSessionManager } from './AcpSessionManager.js';
|
||||
|
||||
interface PendingRequest<T = unknown> {
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeoutId?: NodeJS.Timeout;
|
||||
method: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP Connection Handler for VSCode Extension
|
||||
*
|
||||
* This class implements the client side of the ACP (Agent Communication Protocol).
|
||||
*
|
||||
* Implementation Status:
|
||||
*
|
||||
* Client Methods (Methods this class implements, called by CLI):
|
||||
* ✅ session/update - Handle session updates via onSessionUpdate callback
|
||||
* ✅ session/request_permission - Request user permission for tool execution
|
||||
* ✅ fs/read_text_file - Read file from workspace
|
||||
* ✅ fs/write_text_file - Write file to workspace
|
||||
*
|
||||
* Agent Methods (Methods CLI implements, called by this class):
|
||||
* ✅ initialize - Initialize ACP protocol connection
|
||||
* ✅ authenticate - Authenticate with selected auth method
|
||||
* ✅ session/new - Create new chat session
|
||||
* ✅ session/prompt - Send user message to agent
|
||||
* ✅ session/cancel - Cancel current generation
|
||||
* ✅ session/load - Load previous session
|
||||
*
|
||||
* 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;
|
||||
private pendingRequests = new Map<number, PendingRequest<unknown>>();
|
||||
private nextRequestId = 0;
|
||||
private sessionId: string | null = null;
|
||||
private isInitialized = false;
|
||||
private nextRequestId = { value: 0 };
|
||||
private backend: AcpBackend | null = null;
|
||||
|
||||
// 模块实例
|
||||
private messageHandler: AcpMessageHandler;
|
||||
private sessionManager: AcpSessionManager;
|
||||
|
||||
// 回调函数
|
||||
onSessionUpdate: (data: AcpSessionUpdate) => void = () => {};
|
||||
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
||||
optionId: string;
|
||||
}> = () => Promise.resolve({ optionId: 'allow' });
|
||||
onEndTurn: () => void = () => {};
|
||||
|
||||
constructor() {
|
||||
this.messageHandler = new AcpMessageHandler();
|
||||
this.sessionManager = new AcpSessionManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到ACP后端
|
||||
*
|
||||
* @param backend - 后端类型
|
||||
* @param cliPath - CLI路径
|
||||
* @param workingDir - 工作目录
|
||||
* @param extraArgs - 额外的命令行参数
|
||||
*/
|
||||
async connect(
|
||||
backend: AcpBackend,
|
||||
cliPath: string,
|
||||
@@ -53,8 +88,8 @@ export class AcpConnection {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const env = { ...process.env };
|
||||
|
||||
// If proxy is configured in extraArgs, also set it as environment variables
|
||||
// This ensures token refresh requests also use the proxy
|
||||
// 如果在extraArgs中配置了代理,也将其设置为环境变量
|
||||
// 这确保token刷新请求也使用代理
|
||||
const proxyArg = extraArgs.find(
|
||||
(arg, i) => arg === '--proxy' && i + 1 < extraArgs.length,
|
||||
);
|
||||
@@ -63,18 +98,10 @@ export class AcpConnection {
|
||||
const proxyUrl = extraArgs[proxyIndex + 1];
|
||||
console.log('[ACP] Setting proxy environment variables:', proxyUrl);
|
||||
|
||||
// Set standard proxy env vars
|
||||
env.HTTP_PROXY = proxyUrl;
|
||||
env.HTTPS_PROXY = proxyUrl;
|
||||
env.http_proxy = proxyUrl;
|
||||
env.https_proxy = proxyUrl;
|
||||
|
||||
// For Node.js fetch (undici), we need to use NODE_OPTIONS with a custom agent
|
||||
// Or use the global-agent package, but for now we'll rely on the --proxy flag
|
||||
// and hope the CLI handles it properly for all requests
|
||||
|
||||
// Alternative: disable TLS verification for proxy (not recommended for production)
|
||||
// env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
}
|
||||
|
||||
let spawnCommand: string;
|
||||
@@ -102,13 +129,16 @@ export class AcpConnection {
|
||||
await this.setupChildProcessHandlers(backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子进程处理器
|
||||
*
|
||||
* @param backend - 后端名称
|
||||
*/
|
||||
private async setupChildProcessHandlers(backend: string): Promise<void> {
|
||||
let spawnError: Error | null = null;
|
||||
|
||||
this.child!.stderr?.on('data', (data) => {
|
||||
const message = data.toString();
|
||||
// Many CLIs output informational messages to stderr, so use console.log instead of console.error
|
||||
// Only treat it as error if it contains actual error keywords
|
||||
if (
|
||||
message.toLowerCase().includes('error') &&
|
||||
!message.includes('Loaded cached')
|
||||
@@ -129,7 +159,7 @@ export class AcpConnection {
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for process to start
|
||||
// 等待进程启动
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (spawnError) {
|
||||
@@ -140,7 +170,7 @@ export class AcpConnection {
|
||||
throw new Error(`${backend} ACP process failed to start`);
|
||||
}
|
||||
|
||||
// Handle messages from ACP server
|
||||
// 处理来自ACP服务器的消息
|
||||
let buffer = '';
|
||||
this.child.stdout?.on('data', (data) => {
|
||||
buffer += data.toString();
|
||||
@@ -153,373 +183,161 @@ export class AcpConnection {
|
||||
const message = JSON.parse(line) as AcpMessage;
|
||||
this.handleMessage(message);
|
||||
} catch (_error) {
|
||||
// Ignore non-JSON lines
|
||||
// 忽略非JSON行
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize protocol
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
private sendRequest<T = unknown>(
|
||||
method: string,
|
||||
params?: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
const id = this.nextRequestId++;
|
||||
const message: AcpRequest = {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
method,
|
||||
...(params && { params }),
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutDuration = method === 'session/prompt' ? 120000 : 60000;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRequests.delete(id);
|
||||
reject(new Error(`Request ${method} timed out`));
|
||||
}, timeoutDuration);
|
||||
|
||||
const pendingRequest: PendingRequest<T> = {
|
||||
resolve: (value: T) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(value);
|
||||
},
|
||||
reject: (error: Error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
},
|
||||
timeoutId,
|
||||
method,
|
||||
};
|
||||
|
||||
this.pendingRequests.set(id, pendingRequest as PendingRequest<unknown>);
|
||||
this.sendMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
private sendMessage(message: AcpRequest | AcpNotification): void {
|
||||
if (this.child?.stdin) {
|
||||
const jsonString = JSON.stringify(message);
|
||||
const lineEnding = process.platform === 'win32' ? '\r\n' : '\n';
|
||||
this.child.stdin.write(jsonString + lineEnding);
|
||||
}
|
||||
}
|
||||
|
||||
private sendResponseMessage(response: AcpResponse): void {
|
||||
if (this.child?.stdin) {
|
||||
const jsonString = JSON.stringify(response);
|
||||
const lineEnding = process.platform === 'win32' ? '\r\n' : '\n';
|
||||
this.child.stdin.write(jsonString + lineEnding);
|
||||
}
|
||||
// 初始化协议
|
||||
await this.sessionManager.initialize(
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*
|
||||
* @param message - ACP消息
|
||||
*/
|
||||
private handleMessage(message: AcpMessage): void {
|
||||
try {
|
||||
if ('method' in message) {
|
||||
// Request or notification
|
||||
this.handleIncomingRequest(message).catch(() => {});
|
||||
} else if (
|
||||
'id' in message &&
|
||||
typeof message.id === 'number' &&
|
||||
this.pendingRequests.has(message.id)
|
||||
) {
|
||||
// Response
|
||||
const pendingRequest = this.pendingRequests.get(message.id)!;
|
||||
const { resolve, reject, method } = pendingRequest;
|
||||
this.pendingRequests.delete(message.id);
|
||||
|
||||
if ('result' in message) {
|
||||
console.log(
|
||||
`[ACP] Response for ${method}:`,
|
||||
JSON.stringify(message.result).substring(0, 200),
|
||||
);
|
||||
if (
|
||||
message.result &&
|
||||
typeof message.result === 'object' &&
|
||||
'stopReason' in message.result &&
|
||||
message.result.stopReason === 'end_turn'
|
||||
) {
|
||||
this.onEndTurn();
|
||||
}
|
||||
resolve(message.result);
|
||||
} else if ('error' in message) {
|
||||
const errorCode = message.error?.code || 'unknown';
|
||||
const errorMsg = message.error?.message || 'Unknown ACP error';
|
||||
const errorData = message.error?.data
|
||||
? JSON.stringify(message.error.data)
|
||||
: '';
|
||||
console.error(`[ACP] Error response for ${method}:`, {
|
||||
code: errorCode,
|
||||
message: errorMsg,
|
||||
data: errorData,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`${errorMsg} (code: ${errorCode})${errorData ? '\nData: ' + errorData : ''}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ACP] Error handling message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleIncomingRequest(
|
||||
message: AcpRequest | AcpNotification,
|
||||
): Promise<void> {
|
||||
const { method, params } = message;
|
||||
|
||||
try {
|
||||
let result = null;
|
||||
|
||||
switch (method) {
|
||||
case 'session/update':
|
||||
this.onSessionUpdate(params as AcpSessionUpdate);
|
||||
break;
|
||||
case 'session/request_permission':
|
||||
result = await this.handlePermissionRequest(
|
||||
params as AcpPermissionRequest,
|
||||
);
|
||||
break;
|
||||
case 'fs/read_text_file':
|
||||
result = await this.handleReadTextFile(
|
||||
params as {
|
||||
path: string;
|
||||
sessionId: string;
|
||||
line: number | null;
|
||||
limit: number | null;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'fs/write_text_file':
|
||||
result = await this.handleWriteTextFile(
|
||||
params as { path: string; content: string; sessionId: string },
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ACP] Unhandled method: ${method}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if ('id' in message && typeof message.id === 'number') {
|
||||
this.sendResponseMessage({
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id: message.id,
|
||||
result,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if ('id' in message && typeof message.id === 'number') {
|
||||
this.sendResponseMessage({
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id: message.id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePermissionRequest(params: AcpPermissionRequest): Promise<{
|
||||
outcome: { outcome: string; optionId: string };
|
||||
}> {
|
||||
try {
|
||||
const response = await this.onPermissionRequest(params);
|
||||
const optionId = response.optionId;
|
||||
|
||||
// Handle cancel, reject, or allow
|
||||
let outcome: string;
|
||||
if (optionId.includes('reject') || optionId === 'cancel') {
|
||||
outcome = 'rejected';
|
||||
} else {
|
||||
outcome = 'selected';
|
||||
}
|
||||
|
||||
return {
|
||||
outcome: {
|
||||
outcome,
|
||||
optionId: optionId === 'cancel' ? 'reject_once' : optionId,
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: 'rejected',
|
||||
optionId: 'reject_once',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async handleReadTextFile(params: {
|
||||
path: string;
|
||||
sessionId: string;
|
||||
line: number | null;
|
||||
limit: number | null;
|
||||
}): Promise<{ content: string }> {
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
console.log(`[ACP] fs/read_text_file request received for: ${params.path}`);
|
||||
console.log(`[ACP] Parameters:`, {
|
||||
line: params.line,
|
||||
limit: params.limit,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(params.path, 'utf-8');
|
||||
console.log(
|
||||
`[ACP] Successfully read file: ${params.path} (${content.length} bytes)`,
|
||||
);
|
||||
|
||||
// Handle line offset and limit if specified
|
||||
if (params.line !== null || params.limit !== null) {
|
||||
const lines = content.split('\n');
|
||||
const startLine = params.line || 0;
|
||||
const endLine = params.limit ? startLine + params.limit : lines.length;
|
||||
const selectedLines = lines.slice(startLine, endLine);
|
||||
const result = { content: selectedLines.join('\n') };
|
||||
console.log(`[ACP] Returning ${selectedLines.length} lines`);
|
||||
return result;
|
||||
}
|
||||
|
||||
const result = { content };
|
||||
console.log(`[ACP] Returning full file content`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ACP] Failed to read file ${params.path}:`, errorMsg);
|
||||
|
||||
// Throw a proper error that will be caught by handleIncomingRequest
|
||||
throw new Error(`Failed to read file '${params.path}': ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWriteTextFile(params: {
|
||||
path: string;
|
||||
content: string;
|
||||
sessionId: string;
|
||||
}): Promise<null> {
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
|
||||
console.log(
|
||||
`[ACP] fs/write_text_file request received for: ${params.path}`,
|
||||
);
|
||||
console.log(`[ACP] Content size: ${params.content.length} bytes`);
|
||||
|
||||
try {
|
||||
// Ensure directory exists
|
||||
const dirName = path.dirname(params.path);
|
||||
console.log(`[ACP] Ensuring directory exists: ${dirName}`);
|
||||
await fs.mkdir(dirName, { recursive: true });
|
||||
|
||||
// Write file
|
||||
await fs.writeFile(params.path, params.content, 'utf-8');
|
||||
|
||||
console.log(`[ACP] Successfully wrote file: ${params.path}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ACP] Failed to write file ${params.path}:`, errorMsg);
|
||||
|
||||
// Throw a proper error that will be caught by handleIncomingRequest
|
||||
throw new Error(`Failed to write file '${params.path}': ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(): Promise<AcpResponse> {
|
||||
const initializeParams = {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true,
|
||||
},
|
||||
},
|
||||
const callbacks: AcpConnectionCallbacks = {
|
||||
onSessionUpdate: this.onSessionUpdate,
|
||||
onPermissionRequest: this.onPermissionRequest,
|
||||
onEndTurn: this.onEndTurn,
|
||||
};
|
||||
|
||||
console.log('[ACP] Sending initialize request...');
|
||||
const response = await this.sendRequest<AcpResponse>(
|
||||
'initialize',
|
||||
initializeParams,
|
||||
);
|
||||
this.isInitialized = true;
|
||||
console.log('[ACP] Initialize successful');
|
||||
return response;
|
||||
}
|
||||
|
||||
async authenticate(methodId?: string): Promise<AcpResponse> {
|
||||
// New version requires methodId to be provided
|
||||
const authMethodId = methodId || 'default';
|
||||
console.log(
|
||||
'[ACP] Sending authenticate request with methodId:',
|
||||
authMethodId,
|
||||
);
|
||||
const response = await this.sendRequest<AcpResponse>('authenticate', {
|
||||
methodId: authMethodId,
|
||||
});
|
||||
console.log('[ACP] Authenticate successful');
|
||||
return response;
|
||||
}
|
||||
|
||||
async newSession(cwd: string = process.cwd()): Promise<AcpResponse> {
|
||||
console.log('[ACP] Sending session/new request with cwd:', cwd);
|
||||
const response = await this.sendRequest<
|
||||
AcpResponse & { sessionId?: string }
|
||||
>('session/new', {
|
||||
cwd,
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
this.sessionId = response.sessionId || null;
|
||||
console.log('[ACP] Session created with ID:', this.sessionId);
|
||||
return response;
|
||||
}
|
||||
|
||||
async sendPrompt(prompt: string): Promise<AcpResponse> {
|
||||
if (!this.sessionId) {
|
||||
throw new Error('No active ACP session');
|
||||
}
|
||||
|
||||
return await this.sendRequest('session/prompt', {
|
||||
sessionId: this.sessionId,
|
||||
prompt: [{ type: 'text', text: prompt }],
|
||||
});
|
||||
}
|
||||
|
||||
async listSessions(): Promise<AcpResponse> {
|
||||
console.log('[ACP] Requesting session list...');
|
||||
try {
|
||||
const response = await this.sendRequest<AcpResponse>('session/list', {});
|
||||
console.log(
|
||||
'[ACP] Session list response:',
|
||||
JSON.stringify(response).substring(0, 200),
|
||||
// 处理消息
|
||||
if ('method' in message) {
|
||||
// 请求或通知
|
||||
this.messageHandler
|
||||
.handleIncomingRequest(message, callbacks)
|
||||
.then((result) => {
|
||||
if ('id' in message && typeof message.id === 'number') {
|
||||
this.messageHandler.sendResponseMessage(this.child, {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id: message.id,
|
||||
result,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if ('id' in message && typeof message.id === 'number') {
|
||||
this.messageHandler.sendResponseMessage(this.child, {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id: message.id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 响应
|
||||
this.messageHandler.handleMessage(
|
||||
message,
|
||||
this.pendingRequests,
|
||||
callbacks,
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[ACP] Failed to get session list:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async switchSession(sessionId: string): Promise<AcpResponse> {
|
||||
console.log('[ACP] Switching to session:', sessionId);
|
||||
this.sessionId = sessionId;
|
||||
const response = await this.sendRequest<AcpResponse>('session/switch', {
|
||||
sessionId,
|
||||
});
|
||||
console.log('[ACP] Session switched successfully');
|
||||
return response;
|
||||
/**
|
||||
* 认证
|
||||
*
|
||||
* @param methodId - 认证方法ID
|
||||
* @returns 认证响应
|
||||
*/
|
||||
async authenticate(methodId?: string): Promise<AcpResponse> {
|
||||
return this.sessionManager.authenticate(
|
||||
methodId,
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*
|
||||
* @param cwd - 工作目录
|
||||
* @returns 新会话响应
|
||||
*/
|
||||
async newSession(cwd: string = process.cwd()): Promise<AcpResponse> {
|
||||
return this.sessionManager.newSession(
|
||||
cwd,
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送提示消息
|
||||
*
|
||||
* @param prompt - 提示内容
|
||||
* @returns 响应
|
||||
*/
|
||||
async sendPrompt(prompt: string): Promise<AcpResponse> {
|
||||
return this.sessionManager.sendPrompt(
|
||||
prompt,
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已有会话
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
* @returns 加载响应
|
||||
*/
|
||||
async loadSession(sessionId: string): Promise<AcpResponse> {
|
||||
return this.sessionManager.loadSession(
|
||||
sessionId,
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
* @returns 会话列表响应
|
||||
*/
|
||||
async listSessions(): Promise<AcpResponse> {
|
||||
return this.sessionManager.listSessions(
|
||||
this.child,
|
||||
this.pendingRequests,
|
||||
this.nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定会话
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
* @returns 切换响应
|
||||
*/
|
||||
async switchSession(sessionId: string): Promise<AcpResponse> {
|
||||
return this.sessionManager.switchSession(sessionId, this.nextRequestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前会话的提示生成
|
||||
*/
|
||||
async cancelSession(): Promise<void> {
|
||||
await this.sessionManager.cancelSession(this.child);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.child) {
|
||||
this.child.kill();
|
||||
@@ -527,20 +345,28 @@ export class AcpConnection {
|
||||
}
|
||||
|
||||
this.pendingRequests.clear();
|
||||
this.sessionId = null;
|
||||
this.isInitialized = false;
|
||||
this.sessionManager.reset();
|
||||
this.backend = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.child !== null && !this.child.killed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有活动会话
|
||||
*/
|
||||
get hasActiveSession(): boolean {
|
||||
return this.sessionId !== null;
|
||||
return this.sessionManager.getCurrentSessionId() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话ID
|
||||
*/
|
||||
get currentSessionId(): string | null {
|
||||
return this.sessionId;
|
||||
return this.sessionManager.getCurrentSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
111
packages/vscode-ide-companion/src/acp/AcpFileHandler.ts
Normal file
111
packages/vscode-ide-companion/src/acp/AcpFileHandler.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACP文件操作处理器
|
||||
*
|
||||
* 负责处理ACP协议中的文件读写操作
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* ACP文件操作处理器类
|
||||
* 提供文件读写功能,符合ACP协议规范
|
||||
*/
|
||||
export class AcpFileHandler {
|
||||
/**
|
||||
* 处理读取文本文件请求
|
||||
*
|
||||
* @param params - 文件读取参数
|
||||
* @param params.path - 文件路径
|
||||
* @param params.sessionId - 会话ID
|
||||
* @param params.line - 起始行号(可选)
|
||||
* @param params.limit - 读取行数限制(可选)
|
||||
* @returns 文件内容
|
||||
* @throws 当文件读取失败时抛出错误
|
||||
*/
|
||||
async handleReadTextFile(params: {
|
||||
path: string;
|
||||
sessionId: string;
|
||||
line: number | null;
|
||||
limit: number | null;
|
||||
}): Promise<{ content: string }> {
|
||||
console.log(`[ACP] fs/read_text_file request received for: ${params.path}`);
|
||||
console.log(`[ACP] Parameters:`, {
|
||||
line: params.line,
|
||||
limit: params.limit,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(params.path, 'utf-8');
|
||||
console.log(
|
||||
`[ACP] Successfully read file: ${params.path} (${content.length} bytes)`,
|
||||
);
|
||||
|
||||
// 处理行偏移和限制
|
||||
if (params.line !== null || params.limit !== null) {
|
||||
const lines = content.split('\n');
|
||||
const startLine = params.line || 0;
|
||||
const endLine = params.limit ? startLine + params.limit : lines.length;
|
||||
const selectedLines = lines.slice(startLine, endLine);
|
||||
const result = { content: selectedLines.join('\n') };
|
||||
console.log(`[ACP] Returning ${selectedLines.length} lines`);
|
||||
return result;
|
||||
}
|
||||
|
||||
const result = { content };
|
||||
console.log(`[ACP] Returning full file content`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ACP] Failed to read file ${params.path}:`, errorMsg);
|
||||
|
||||
throw new Error(`Failed to read file '${params.path}': ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理写入文本文件请求
|
||||
*
|
||||
* @param params - 文件写入参数
|
||||
* @param params.path - 文件路径
|
||||
* @param params.content - 文件内容
|
||||
* @param params.sessionId - 会话ID
|
||||
* @returns null表示成功
|
||||
* @throws 当文件写入失败时抛出错误
|
||||
*/
|
||||
async handleWriteTextFile(params: {
|
||||
path: string;
|
||||
content: string;
|
||||
sessionId: string;
|
||||
}): Promise<null> {
|
||||
console.log(
|
||||
`[ACP] fs/write_text_file request received for: ${params.path}`,
|
||||
);
|
||||
console.log(`[ACP] Content size: ${params.content.length} bytes`);
|
||||
|
||||
try {
|
||||
// 确保目录存在
|
||||
const dirName = path.dirname(params.path);
|
||||
console.log(`[ACP] Ensuring directory exists: ${dirName}`);
|
||||
await fs.mkdir(dirName, { recursive: true });
|
||||
|
||||
// 写入文件
|
||||
await fs.writeFile(params.path, params.content, 'utf-8');
|
||||
|
||||
console.log(`[ACP] Successfully wrote file: ${params.path}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[ACP] Failed to write file ${params.path}:`, errorMsg);
|
||||
|
||||
throw new Error(`Failed to write file '${params.path}': ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
225
packages/vscode-ide-companion/src/acp/AcpMessageHandler.ts
Normal file
225
packages/vscode-ide-companion/src/acp/AcpMessageHandler.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACP消息处理器
|
||||
*
|
||||
* 负责处理ACP协议中的消息接收、解析和分发
|
||||
*/
|
||||
|
||||
import type {
|
||||
AcpMessage,
|
||||
AcpRequest,
|
||||
AcpNotification,
|
||||
AcpResponse,
|
||||
AcpSessionUpdate,
|
||||
AcpPermissionRequest,
|
||||
} from '../shared/acpTypes.js';
|
||||
import { CLIENT_METHODS } from './schema.js';
|
||||
import type { PendingRequest, AcpConnectionCallbacks } from './AcpTypes.js';
|
||||
import { AcpFileHandler } from './AcpFileHandler.js';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
/**
|
||||
* ACP消息处理器类
|
||||
* 负责消息的接收、解析和处理
|
||||
*/
|
||||
export class AcpMessageHandler {
|
||||
private fileHandler: AcpFileHandler;
|
||||
|
||||
constructor() {
|
||||
this.fileHandler = new AcpFileHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送响应消息到子进程
|
||||
*
|
||||
* @param child - 子进程实例
|
||||
* @param response - 响应消息
|
||||
*/
|
||||
sendResponseMessage(child: ChildProcess | null, response: AcpResponse): void {
|
||||
if (child?.stdin) {
|
||||
const jsonString = JSON.stringify(response);
|
||||
const lineEnding = process.platform === 'win32' ? '\r\n' : '\n';
|
||||
child.stdin.write(jsonString + lineEnding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
*
|
||||
* @param message - ACP消息
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param callbacks - 回调函数集合
|
||||
*/
|
||||
handleMessage(
|
||||
message: AcpMessage,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
callbacks: AcpConnectionCallbacks,
|
||||
): void {
|
||||
try {
|
||||
if ('method' in message) {
|
||||
// 请求或通知
|
||||
this.handleIncomingRequest(message, callbacks).catch(() => {});
|
||||
} else if (
|
||||
'id' in message &&
|
||||
typeof message.id === 'number' &&
|
||||
pendingRequests.has(message.id)
|
||||
) {
|
||||
// 响应
|
||||
this.handleResponse(message, pendingRequests, callbacks);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ACP] Error handling message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应消息
|
||||
*
|
||||
* @param message - 响应消息
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param callbacks - 回调函数集合
|
||||
*/
|
||||
private handleResponse(
|
||||
message: AcpMessage,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
callbacks: AcpConnectionCallbacks,
|
||||
): void {
|
||||
if (!('id' in message) || typeof message.id !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingRequest = pendingRequests.get(message.id);
|
||||
if (!pendingRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { resolve, reject, method } = pendingRequest;
|
||||
pendingRequests.delete(message.id);
|
||||
|
||||
if ('result' in message) {
|
||||
console.log(
|
||||
`[ACP] Response for ${method}:`,
|
||||
JSON.stringify(message.result).substring(0, 200),
|
||||
);
|
||||
if (
|
||||
message.result &&
|
||||
typeof message.result === 'object' &&
|
||||
'stopReason' in message.result &&
|
||||
message.result.stopReason === 'end_turn'
|
||||
) {
|
||||
callbacks.onEndTurn();
|
||||
}
|
||||
resolve(message.result);
|
||||
} else if ('error' in message) {
|
||||
const errorCode = message.error?.code || 'unknown';
|
||||
const errorMsg = message.error?.message || 'Unknown ACP error';
|
||||
const errorData = message.error?.data
|
||||
? JSON.stringify(message.error.data)
|
||||
: '';
|
||||
console.error(`[ACP] Error response for ${method}:`, {
|
||||
code: errorCode,
|
||||
message: errorMsg,
|
||||
data: errorData,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`${errorMsg} (code: ${errorCode})${errorData ? '\nData: ' + errorData : ''}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理进入的请求
|
||||
*
|
||||
* @param message - 请求或通知消息
|
||||
* @param callbacks - 回调函数集合
|
||||
* @returns 请求处理结果
|
||||
*/
|
||||
async handleIncomingRequest(
|
||||
message: AcpRequest | AcpNotification,
|
||||
callbacks: AcpConnectionCallbacks,
|
||||
): Promise<unknown> {
|
||||
const { method, params } = message;
|
||||
|
||||
let result = null;
|
||||
|
||||
switch (method) {
|
||||
case CLIENT_METHODS.session_update:
|
||||
callbacks.onSessionUpdate(params as AcpSessionUpdate);
|
||||
break;
|
||||
case CLIENT_METHODS.session_request_permission:
|
||||
result = await this.handlePermissionRequest(
|
||||
params as AcpPermissionRequest,
|
||||
callbacks,
|
||||
);
|
||||
break;
|
||||
case CLIENT_METHODS.fs_read_text_file:
|
||||
result = await this.fileHandler.handleReadTextFile(
|
||||
params as {
|
||||
path: string;
|
||||
sessionId: string;
|
||||
line: number | null;
|
||||
limit: number | null;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case CLIENT_METHODS.fs_write_text_file:
|
||||
result = await this.fileHandler.handleWriteTextFile(
|
||||
params as { path: string; content: string; sessionId: string },
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.warn(`[ACP] Unhandled method: ${method}`);
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理权限请求
|
||||
*
|
||||
* @param params - 权限请求参数
|
||||
* @param callbacks - 回调函数集合
|
||||
* @returns 权限请求结果
|
||||
*/
|
||||
private async handlePermissionRequest(
|
||||
params: AcpPermissionRequest,
|
||||
callbacks: AcpConnectionCallbacks,
|
||||
): Promise<{
|
||||
outcome: { outcome: string; optionId: string };
|
||||
}> {
|
||||
try {
|
||||
const response = await callbacks.onPermissionRequest(params);
|
||||
const optionId = response.optionId;
|
||||
|
||||
// 处理取消、拒绝或允许
|
||||
let outcome: string;
|
||||
if (optionId.includes('reject') || optionId === 'cancel') {
|
||||
outcome = 'rejected';
|
||||
} else {
|
||||
outcome = 'selected';
|
||||
}
|
||||
|
||||
return {
|
||||
outcome: {
|
||||
outcome,
|
||||
optionId: optionId === 'cancel' ? 'reject_once' : optionId,
|
||||
},
|
||||
};
|
||||
} catch (_error) {
|
||||
return {
|
||||
outcome: {
|
||||
outcome: 'rejected',
|
||||
optionId: 'reject_once',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
373
packages/vscode-ide-companion/src/acp/AcpSessionManager.ts
Normal file
373
packages/vscode-ide-companion/src/acp/AcpSessionManager.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACP会话管理器
|
||||
*
|
||||
* 负责管理ACP协议的会话操作,包括初始化、认证、会话创建和切换等
|
||||
*/
|
||||
|
||||
import { JSONRPC_VERSION } from '../shared/acpTypes.js';
|
||||
import type {
|
||||
AcpRequest,
|
||||
AcpNotification,
|
||||
AcpResponse,
|
||||
} from '../shared/acpTypes.js';
|
||||
import { AGENT_METHODS, CUSTOM_METHODS } from './schema.js';
|
||||
import type { PendingRequest } from './AcpTypes.js';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
/**
|
||||
* ACP会话管理器类
|
||||
* 提供会话的初始化、认证、创建、加载和切换功能
|
||||
*/
|
||||
export class AcpSessionManager {
|
||||
private sessionId: string | null = null;
|
||||
private isInitialized = false;
|
||||
|
||||
/**
|
||||
* 发送请求到ACP服务器
|
||||
*
|
||||
* @param method - 请求方法名
|
||||
* @param params - 请求参数
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 请求响应
|
||||
*/
|
||||
private sendRequest<T = unknown>(
|
||||
method: string,
|
||||
params: Record<string, unknown> | undefined,
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<T> {
|
||||
const id = nextRequestId.value++;
|
||||
const message: AcpRequest = {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
method,
|
||||
...(params && { params }),
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutDuration =
|
||||
method === AGENT_METHODS.session_prompt ? 120000 : 60000;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
pendingRequests.delete(id);
|
||||
reject(new Error(`Request ${method} timed out`));
|
||||
}, timeoutDuration);
|
||||
|
||||
const pendingRequest: PendingRequest<T> = {
|
||||
resolve: (value: T) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(value);
|
||||
},
|
||||
reject: (error: Error) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
},
|
||||
timeoutId,
|
||||
method,
|
||||
};
|
||||
|
||||
pendingRequests.set(id, pendingRequest as PendingRequest<unknown>);
|
||||
this.sendMessage(message, child);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到子进程
|
||||
*
|
||||
* @param message - 请求或通知消息
|
||||
* @param child - 子进程实例
|
||||
*/
|
||||
private sendMessage(
|
||||
message: AcpRequest | AcpNotification,
|
||||
child: ChildProcess | null,
|
||||
): void {
|
||||
if (child?.stdin) {
|
||||
const jsonString = JSON.stringify(message);
|
||||
const lineEnding = process.platform === 'win32' ? '\r\n' : '\n';
|
||||
child.stdin.write(jsonString + lineEnding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ACP协议连接
|
||||
*
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 初始化响应
|
||||
*/
|
||||
async initialize(
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
const initializeParams = {
|
||||
protocolVersion: 1,
|
||||
clientCapabilities: {
|
||||
fs: {
|
||||
readTextFile: true,
|
||||
writeTextFile: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[ACP] Sending initialize request...');
|
||||
const response = await this.sendRequest<AcpResponse>(
|
||||
AGENT_METHODS.initialize,
|
||||
initializeParams,
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
this.isInitialized = true;
|
||||
|
||||
console.log('[ACP] Initialize successful');
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行认证
|
||||
*
|
||||
* @param methodId - 认证方法ID
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 认证响应
|
||||
*/
|
||||
async authenticate(
|
||||
methodId: string | undefined,
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
const authMethodId = methodId || 'default';
|
||||
console.log(
|
||||
'[ACP] Sending authenticate request with methodId:',
|
||||
authMethodId,
|
||||
);
|
||||
const response = await this.sendRequest<AcpResponse>(
|
||||
AGENT_METHODS.authenticate,
|
||||
{
|
||||
methodId: authMethodId,
|
||||
},
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
console.log('[ACP] Authenticate successful');
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*
|
||||
* @param cwd - 工作目录
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 新会话响应
|
||||
*/
|
||||
async newSession(
|
||||
cwd: string,
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
console.log('[ACP] Sending session/new request with cwd:', cwd);
|
||||
const response = await this.sendRequest<
|
||||
AcpResponse & { sessionId?: string }
|
||||
>(
|
||||
AGENT_METHODS.session_new,
|
||||
{
|
||||
cwd,
|
||||
mcpServers: [],
|
||||
},
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
|
||||
this.sessionId = response.sessionId || null;
|
||||
console.log('[ACP] Session created with ID:', this.sessionId);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送提示消息
|
||||
*
|
||||
* @param prompt - 提示内容
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 响应
|
||||
* @throws 当没有活动会话时抛出错误
|
||||
*/
|
||||
async sendPrompt(
|
||||
prompt: string,
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
if (!this.sessionId) {
|
||||
throw new Error('No active ACP session');
|
||||
}
|
||||
|
||||
return await this.sendRequest(
|
||||
AGENT_METHODS.session_prompt,
|
||||
{
|
||||
sessionId: this.sessionId,
|
||||
prompt: [{ type: 'text', text: prompt }],
|
||||
},
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载已有会话
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 加载响应
|
||||
*/
|
||||
async loadSession(
|
||||
sessionId: string,
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
console.log('[ACP] Loading session:', sessionId);
|
||||
const response = await this.sendRequest<AcpResponse>(
|
||||
AGENT_METHODS.session_load,
|
||||
{
|
||||
sessionId,
|
||||
cwd: process.cwd(),
|
||||
mcpServers: [],
|
||||
},
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
console.log('[ACP] Session load response:', response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
* @param child - 子进程实例
|
||||
* @param pendingRequests - 待处理请求映射表
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 会话列表响应
|
||||
*/
|
||||
async listSessions(
|
||||
child: ChildProcess | null,
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
console.log('[ACP] Requesting session list...');
|
||||
try {
|
||||
const response = await this.sendRequest<AcpResponse>(
|
||||
CUSTOM_METHODS.session_list,
|
||||
{},
|
||||
child,
|
||||
pendingRequests,
|
||||
nextRequestId,
|
||||
);
|
||||
console.log(
|
||||
'[ACP] Session list response:',
|
||||
JSON.stringify(response).substring(0, 200),
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[ACP] Failed to get session list:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定会话
|
||||
*
|
||||
* @param sessionId - 会话ID
|
||||
* @param nextRequestId - 请求ID计数器
|
||||
* @returns 切换响应
|
||||
*/
|
||||
async switchSession(
|
||||
sessionId: string,
|
||||
nextRequestId: { value: number },
|
||||
): Promise<AcpResponse> {
|
||||
console.log('[ACP] Switching to session:', sessionId);
|
||||
this.sessionId = sessionId;
|
||||
|
||||
const mockResponse: AcpResponse = {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id: nextRequestId.value++,
|
||||
result: { sessionId },
|
||||
};
|
||||
console.log(
|
||||
'[ACP] Session ID updated locally (switch not supported by CLI)',
|
||||
);
|
||||
return mockResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前会话的提示生成
|
||||
*
|
||||
* @param child - 子进程实例
|
||||
*/
|
||||
async cancelSession(child: ChildProcess | null): Promise<void> {
|
||||
if (!this.sessionId) {
|
||||
console.warn('[ACP] No active session to cancel');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ACP] Cancelling session:', this.sessionId);
|
||||
|
||||
const cancelParams = {
|
||||
sessionId: this.sessionId,
|
||||
};
|
||||
|
||||
const message: AcpNotification = {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
method: AGENT_METHODS.session_cancel,
|
||||
params: cancelParams,
|
||||
};
|
||||
|
||||
this.sendMessage(message, child);
|
||||
console.log('[ACP] Cancel notification sent');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置会话管理器状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.sessionId = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话ID
|
||||
*/
|
||||
getCurrentSessionId(): string | null {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已初始化
|
||||
*/
|
||||
getIsInitialized(): boolean {
|
||||
return this.isInitialized;
|
||||
}
|
||||
}
|
||||
63
packages/vscode-ide-companion/src/acp/AcpTypes.ts
Normal file
63
packages/vscode-ide-companion/src/acp/AcpTypes.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACP连接类型定义
|
||||
*
|
||||
* 包含了ACP连接所需的所有类型和接口定义
|
||||
*/
|
||||
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import type {
|
||||
AcpSessionUpdate,
|
||||
AcpPermissionRequest,
|
||||
} from '../shared/acpTypes.js';
|
||||
|
||||
/**
|
||||
* 待处理的请求信息
|
||||
*/
|
||||
export interface PendingRequest<T = unknown> {
|
||||
/** 成功回调 */
|
||||
resolve: (value: T) => void;
|
||||
/** 失败回调 */
|
||||
reject: (error: Error) => void;
|
||||
/** 超时定时器ID */
|
||||
timeoutId?: NodeJS.Timeout;
|
||||
/** 请求方法名 */
|
||||
method: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP连接回调函数类型
|
||||
*/
|
||||
export interface AcpConnectionCallbacks {
|
||||
/** 会话更新回调 */
|
||||
onSessionUpdate: (data: AcpSessionUpdate) => void;
|
||||
/** 权限请求回调 */
|
||||
onPermissionRequest: (data: AcpPermissionRequest) => Promise<{
|
||||
optionId: string;
|
||||
}>;
|
||||
/** 回合结束回调 */
|
||||
onEndTurn: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* ACP连接状态
|
||||
*/
|
||||
export interface AcpConnectionState {
|
||||
/** 子进程实例 */
|
||||
child: ChildProcess | null;
|
||||
/** 待处理的请求映射表 */
|
||||
pendingRequests: Map<number, PendingRequest<unknown>>;
|
||||
/** 下一个请求ID */
|
||||
nextRequestId: number;
|
||||
/** 当前会话ID */
|
||||
sessionId: string | null;
|
||||
/** 是否已初始化 */
|
||||
isInitialized: boolean;
|
||||
/** 后端类型 */
|
||||
backend: string | null;
|
||||
}
|
||||
57
packages/vscode-ide-companion/src/acp/schema.ts
Normal file
57
packages/vscode-ide-companion/src/acp/schema.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* ACP (Agent Communication Protocol) Method Definitions
|
||||
*
|
||||
* This file defines the protocol methods for communication between
|
||||
* the VSCode extension (Client) and the qwen CLI (Agent/Server).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Methods that the Agent (CLI) implements and receives from Client (VSCode)
|
||||
*
|
||||
* Status in qwen CLI:
|
||||
* ✅ initialize - Protocol initialization
|
||||
* ✅ authenticate - User authentication
|
||||
* ✅ session/new - Create new session
|
||||
* ❌ session/load - Load existing session (not implemented in CLI)
|
||||
* ✅ session/prompt - Send user message to agent
|
||||
* ✅ session/cancel - Cancel current generation
|
||||
*/
|
||||
export const AGENT_METHODS = {
|
||||
authenticate: 'authenticate',
|
||||
initialize: 'initialize',
|
||||
session_cancel: 'session/cancel',
|
||||
session_load: 'session/load',
|
||||
session_new: 'session/new',
|
||||
session_prompt: 'session/prompt',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Methods that the Client (VSCode) implements and receives from Agent (CLI)
|
||||
*
|
||||
* Status in VSCode extension:
|
||||
* ✅ fs/read_text_file - Read file content
|
||||
* ✅ fs/write_text_file - Write file content
|
||||
* ✅ session/request_permission - Request user permission for tool execution
|
||||
* ✅ session/update - Stream session updates (notification)
|
||||
*/
|
||||
export const CLIENT_METHODS = {
|
||||
fs_read_text_file: 'fs/read_text_file',
|
||||
fs_write_text_file: 'fs/write_text_file',
|
||||
session_request_permission: 'session/request_permission',
|
||||
session_update: 'session/update',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Custom methods (not in standard ACP protocol)
|
||||
* These are VSCode extension specific extensions
|
||||
*/
|
||||
export const CUSTOM_METHODS = {
|
||||
session_list: 'session/list',
|
||||
session_switch: 'session/switch',
|
||||
} as const;
|
||||
Reference in New Issue
Block a user