Compare commits
11 Commits
d415d8ee4e
...
430581598b
| Author | SHA1 | Date | |
|---|---|---|---|
| 430581598b | |||
| 2d5b297171 | |||
| 023fdb66c3 | |||
| 42481cd314 | |||
| e77194628a | |||
| 2aff54de74 | |||
| 91fadf591f | |||
| 02b56a7031 | |||
| c42ebdfe59 | |||
| 3f0cc8ae29 | |||
| 0f458f6299 |
@ -9,7 +9,8 @@
|
||||
},
|
||||
"icon": "media/icon.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
"Chat",
|
||||
"Programming Languages"
|
||||
],
|
||||
"keywords": [
|
||||
"IC",
|
||||
|
||||
@ -16,7 +16,7 @@ export interface IccoderConfig {
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG: IccoderConfig = {
|
||||
backendUrl: "http://localhost:8080",
|
||||
backendUrl: "http://192.168.1.108:2233",
|
||||
timeout: 60000,
|
||||
userId: "default-user",
|
||||
};
|
||||
|
||||
@ -9,6 +9,9 @@ import {
|
||||
handleReplaceInFile,
|
||||
handleUserAnswer,
|
||||
abortCurrentDialog,
|
||||
handlePlanAction,
|
||||
setPendingPlanExecution,
|
||||
getCurrentTaskId,
|
||||
} from "../utils/messageHandler";
|
||||
import { VCDViewerPanel } from "./VCDViewerPanel";
|
||||
import { ChatHistoryManager } from "../utils/chatHistoryManager";
|
||||
@ -23,21 +26,27 @@ export async function showICHelperPanel(
|
||||
) {
|
||||
// 检查用户是否已登录
|
||||
try {
|
||||
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
|
||||
const session = await vscode.authentication.getSession("iccoder", [], {
|
||||
createIfNone: false,
|
||||
});
|
||||
if (!session) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window
|
||||
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
|
||||
.then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
|
||||
if (selection === "立即登录") {
|
||||
vscode.commands.executeCommand("ic-coder.login");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -107,7 +116,12 @@ export async function showICHelperPanel(
|
||||
// 切换到当前面板的任务上下文
|
||||
historyManager.switchToPanelTask(panelId);
|
||||
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
handleUserMessage(
|
||||
panel,
|
||||
message.text,
|
||||
context.extensionPath,
|
||||
message.mode
|
||||
);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
@ -181,6 +195,56 @@ export async function showICHelperPanel(
|
||||
case "abortDialog":
|
||||
abortCurrentDialog();
|
||||
break;
|
||||
// 处理计划操作(只做模式切换,响应已通过 submitAnswer 发送)
|
||||
case "planAction":
|
||||
if (message.action === "confirm") {
|
||||
// 确认执行:切换到 Agent 模式
|
||||
panel.webview.postMessage({
|
||||
command: "switchMode",
|
||||
mode: "agent",
|
||||
});
|
||||
// 获取当前会话的 taskId,用于复用知识图谱数据
|
||||
const taskId = getCurrentTaskId();
|
||||
if (taskId) {
|
||||
// 设置待执行的计划,对话结束后自动执行(复用 taskId)
|
||||
setPendingPlanExecution(
|
||||
panel,
|
||||
message.planTitle || "计划",
|
||||
context.extensionPath,
|
||||
taskId
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
"[ICHelperPanel] 无法获取当前 taskId,知识图谱数据可能丢失"
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// 新增:检查工作区状态
|
||||
case "checkWorkspace":
|
||||
const hasWorkspace = !!(
|
||||
vscode.workspace.workspaceFolders &&
|
||||
vscode.workspace.workspaceFolders.length > 0
|
||||
);
|
||||
if (!hasWorkspace) {
|
||||
// 弹窗提示用户需要打开工作区
|
||||
vscode.window
|
||||
.showWarningMessage(
|
||||
"请先打开一个文件夹作为工作区,这样我就能更好地为您服务了 😊",
|
||||
"打开文件夹"
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "打开文件夹") {
|
||||
vscode.commands.executeCommand("vscode.openFolder");
|
||||
}
|
||||
});
|
||||
}
|
||||
// 返回工作区状态给前端
|
||||
panel.webview.postMessage({
|
||||
command: "workspaceStatus",
|
||||
hasWorkspace: hasWorkspace,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
|
||||
@ -6,7 +6,7 @@ import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import { getApiUrl, getConfig } from '../config/settings';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
|
||||
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* HTTP 请求选项
|
||||
@ -103,6 +103,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交工具确认响应(Ask 模式)
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
|
||||
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
|
||||
return request<ToolResultResponse>('/api/tool/confirm', {
|
||||
method: 'POST',
|
||||
body: response
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* GET /api/dialog/health
|
||||
|
||||
@ -7,13 +7,14 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from '
|
||||
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
|
||||
import { userInteractionManager } from './userInteraction';
|
||||
import { getConfig } from '../config/settings';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
|
||||
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
|
||||
import { submitToolConfirm, submitAnswer } from './apiClient';
|
||||
|
||||
/**
|
||||
* 消息段落类型
|
||||
*/
|
||||
export interface MessageSegment {
|
||||
type: 'text' | 'tool' | 'question' | 'agent';
|
||||
type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
|
||||
content?: string;
|
||||
toolName?: string;
|
||||
toolStatus?: 'running' | 'success' | 'error';
|
||||
@ -26,6 +27,10 @@ export interface MessageSegment {
|
||||
agentName?: string;
|
||||
agentStatus?: 'running' | 'completed' | 'error';
|
||||
agentSteps?: AgentStep[];
|
||||
// 计划相关字段
|
||||
planTitle?: string;
|
||||
planSteps?: string[];
|
||||
planSummary?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,6 +56,10 @@ export interface DialogCallbacks {
|
||||
onToolComplete?: (toolName: string, result: string) => void;
|
||||
/** 工具执行错误 */
|
||||
onToolError?: (toolName: string, error: string) => void;
|
||||
/** 工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
|
||||
/** 计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (confirmId: number, title: string, steps: string[], summary: string) => void;
|
||||
/** 显示问题(ask_user) */
|
||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
||||
/** 实时更新段落(流式过程中) */
|
||||
@ -75,8 +84,9 @@ export class DialogSession {
|
||||
private segments: MessageSegment[] = [];
|
||||
private currentTextSegment: MessageSegment | null = null;
|
||||
|
||||
constructor(extensionPath: string) {
|
||||
this.taskId = generateTaskId();
|
||||
constructor(extensionPath: string, existingTaskId?: string) {
|
||||
// 支持复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
this.taskId = existingTaskId || generateTaskId();
|
||||
this.toolContext = createToolExecutorContext(extensionPath);
|
||||
}
|
||||
|
||||
@ -142,12 +152,52 @@ export class DialogSession {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具操作描述(用于确认对话框)
|
||||
*/
|
||||
private getToolDescription(toolName: string, toolInput: Record<string, unknown>): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
switch (toolName) {
|
||||
case 'file_write':
|
||||
lines.push(`文件路径: ${toolInput.path || '未知'}`);
|
||||
if (toolInput.content) {
|
||||
const content = String(toolInput.content);
|
||||
lines.push(`内容长度: ${content.length} 字符`);
|
||||
lines.push(`内容预览: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
break;
|
||||
case 'file_delete':
|
||||
lines.push(`删除文件: ${toolInput.path || '未知'}`);
|
||||
break;
|
||||
case 'syntax_check':
|
||||
lines.push('执行语法检查');
|
||||
if (toolInput.code) {
|
||||
const code = String(toolInput.code);
|
||||
lines.push(`代码长度: ${code.length} 字符`);
|
||||
}
|
||||
break;
|
||||
case 'simulation':
|
||||
lines.push(`RTL文件: ${toolInput.rtlPath || '未知'}`);
|
||||
lines.push(`TB文件: ${toolInput.tbPath || '未知'}`);
|
||||
if (toolInput.duration) {
|
||||
lines.push(`仿真时长: ${toolInput.duration}`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
lines.push(`参数: ${JSON.stringify(toolInput, null, 2)}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息并开始流式对话
|
||||
*/
|
||||
async sendMessage(
|
||||
message: string,
|
||||
callbacks: DialogCallbacks
|
||||
callbacks: DialogCallbacks,
|
||||
mode?: RunMode
|
||||
): Promise<void> {
|
||||
if (this.isActive) {
|
||||
callbacks.onError?.('当前有对话正在进行中');
|
||||
@ -164,7 +214,7 @@ export class DialogSession {
|
||||
taskId: this.taskId,
|
||||
message,
|
||||
userId: config.userId,
|
||||
toolMode: 'AGENT'
|
||||
mode: mode || 'agent'
|
||||
};
|
||||
|
||||
const sseCallbacks: SSECallbacks = {
|
||||
@ -248,6 +298,72 @@ export class DialogSession {
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
},
|
||||
|
||||
onToolConfirm: async (data: ToolConfirmEvent) => {
|
||||
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
|
||||
|
||||
// 调用回调通知 UI 显示确认对话框
|
||||
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
|
||||
|
||||
// 使用 VSCode 快速选择框显示确认对话框
|
||||
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
`确认执行操作: ${data.toolName}`,
|
||||
{ modal: true, detail: toolDescription },
|
||||
'确认执行',
|
||||
'取消'
|
||||
);
|
||||
|
||||
const approved = result === '确认执行';
|
||||
console.log('[DialogSession] 用户确认结果:', approved);
|
||||
|
||||
// 发送确认响应到后端
|
||||
try {
|
||||
await submitToolConfirm({
|
||||
confirmId: data.confirmId,
|
||||
taskId: this.taskId,
|
||||
approved
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[DialogSession] 发送确认响应失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
onPlanConfirm: async (data: PlanConfirmEvent) => {
|
||||
console.log('[DialogSession] onPlanConfirm:', data.title);
|
||||
|
||||
// 结束当前文本段落
|
||||
this.finalizeTextSegment();
|
||||
|
||||
const askId = `ask_${data.confirmId}`;
|
||||
|
||||
// 添加计划段落到聊天界面(包含 askId 用于响应)
|
||||
this.segments.push({
|
||||
type: 'plan',
|
||||
askId: askId,
|
||||
planTitle: data.title,
|
||||
planSteps: data.steps,
|
||||
planSummary: data.summary
|
||||
});
|
||||
|
||||
// 实时发送段落更新
|
||||
callbacks.onSegmentUpdate?.(this.segments);
|
||||
|
||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||
const planEvent = {
|
||||
askId: askId,
|
||||
question: `请确认执行计划:${data.title}`,
|
||||
options: ['确认执行', '修改计划', '取消']
|
||||
};
|
||||
try {
|
||||
await userInteractionManager.handleAskUser(planEvent as AskUserEvent, this.taskId);
|
||||
} catch (error) {
|
||||
console.error('[DialogSession] 处理计划确认失败:', error);
|
||||
}
|
||||
|
||||
// 调用回调通知 UI
|
||||
callbacks.onPlanConfirm?.(data.confirmId, data.title, data.steps, data.summary);
|
||||
},
|
||||
|
||||
onAskUser: async (data: AskUserEvent) => {
|
||||
this.finalizeTextSegment();
|
||||
this.segments.push({
|
||||
@ -402,13 +518,15 @@ class DialogManager {
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
* @param extensionPath 扩展路径
|
||||
* @param existingTaskId 可选,复用现有的 taskId(用于 Plan 模式确认后继续执行)
|
||||
*/
|
||||
createSession(extensionPath: string): DialogSession {
|
||||
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
|
||||
// 如果有活跃会话,先中止
|
||||
if (this.currentSession?.active) {
|
||||
this.currentSession.abort();
|
||||
}
|
||||
this.currentSession = new DialogSession(extensionPath);
|
||||
this.currentSession = new DialogSession(extensionPath, existingTaskId);
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,8 @@ import type {
|
||||
SSEEventType,
|
||||
TextDeltaEvent,
|
||||
ToolCallRequest,
|
||||
ToolConfirmEvent,
|
||||
PlanConfirmEvent,
|
||||
AskUserEvent,
|
||||
CompleteEvent,
|
||||
ErrorEvent,
|
||||
@ -36,6 +38,10 @@ export interface SSECallbacks {
|
||||
onTextDelta?: (data: TextDeltaEvent) => void;
|
||||
/** 收到工具调用请求 */
|
||||
onToolCall?: (data: ToolCallRequest) => void;
|
||||
/** 收到工具确认请求(Ask 模式) */
|
||||
onToolConfirm?: (data: ToolConfirmEvent) => void;
|
||||
/** 收到计划确认请求(Plan 模式) */
|
||||
onPlanConfirm?: (data: PlanConfirmEvent) => void;
|
||||
/** 工具开始执行 */
|
||||
onToolStart?: (data: ToolStartEvent) => void;
|
||||
/** 工具执行完成 */
|
||||
@ -136,7 +142,7 @@ export async function startStreamDialog(
|
||||
|
||||
const body = JSON.stringify(request);
|
||||
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
|
||||
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const options: http.RequestOptions = {
|
||||
@ -268,6 +274,12 @@ function dispatchEvent(
|
||||
case 'tool_call':
|
||||
callbacks.onToolCall?.(data as ToolCallRequest);
|
||||
break;
|
||||
case 'tool_confirm':
|
||||
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
|
||||
break;
|
||||
case 'plan_confirm':
|
||||
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
|
||||
break;
|
||||
case 'tool_start':
|
||||
callbacks.onToolStart?.(data as ToolStartEvent);
|
||||
break;
|
||||
|
||||
@ -5,6 +5,15 @@
|
||||
|
||||
// ============== 对话请求/响应 ==============
|
||||
|
||||
/**
|
||||
* 运行模式类型
|
||||
* - plan: 只读模式,只能查询分析
|
||||
* - ask: 逐个确认,每个写操作需确认
|
||||
* - agent: 智能体自主(默认)
|
||||
* - auto: 完全自动
|
||||
*/
|
||||
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
|
||||
|
||||
/**
|
||||
* 对话请求
|
||||
* POST /api/dialog/stream
|
||||
@ -16,8 +25,8 @@ export interface DialogRequest {
|
||||
message: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 工具模式 */
|
||||
toolMode: 'ASK' | 'AGENT';
|
||||
/** 运行模式 */
|
||||
mode: RunMode;
|
||||
}
|
||||
|
||||
// ============== SSE 事件类型 ==============
|
||||
@ -26,6 +35,8 @@ export interface DialogRequest {
|
||||
export type SSEEventType =
|
||||
| 'text_delta' // 文本增量
|
||||
| 'tool_call' // 客户端工具调用请求
|
||||
| 'tool_confirm' // 工具确认请求(Ask 模式)
|
||||
| 'plan_confirm' // 计划确认请求(Plan 模式)
|
||||
| 'tool_start' // 工具开始执行
|
||||
| 'tool_complete' // 工具执行完成
|
||||
| 'tool_error' // 工具执行错误
|
||||
@ -63,6 +74,32 @@ export interface ToolErrorEvent {
|
||||
error: string;
|
||||
}
|
||||
|
||||
/** tool_confirm 事件数据(Ask 模式确认请求) */
|
||||
export interface ToolConfirmEvent {
|
||||
/** 确认ID,用于匹配响应 */
|
||||
confirmId: number;
|
||||
/** 工具名称 */
|
||||
toolName: string;
|
||||
/** 工具输入参数 */
|
||||
toolInput: Record<string, unknown>;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** plan_confirm 事件数据(Plan 模式计划确认) */
|
||||
export interface PlanConfirmEvent {
|
||||
/** 确认ID */
|
||||
confirmId: number;
|
||||
/** 计划标题 */
|
||||
title: string;
|
||||
/** 执行步骤列表 */
|
||||
steps: string[];
|
||||
/** 计划摘要 */
|
||||
summary: string;
|
||||
/** 时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** ask_user 事件数据 */
|
||||
export interface AskUserEvent {
|
||||
askId: string;
|
||||
@ -229,6 +266,21 @@ export interface ToolResultResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============== 工具确认响应 ==============
|
||||
|
||||
/**
|
||||
* 工具确认响应请求
|
||||
* POST /api/tool/confirm
|
||||
*/
|
||||
export interface ToolConfirmResponse {
|
||||
/** 确认ID,与 ToolConfirmEvent.confirmId 对应 */
|
||||
confirmId: number;
|
||||
/** 任务ID */
|
||||
taskId: string;
|
||||
/** 是否批准执行 */
|
||||
approved: boolean;
|
||||
}
|
||||
|
||||
// ============== 辅助类型 ==============
|
||||
|
||||
/** 后端工具名称 */
|
||||
|
||||
@ -16,7 +16,9 @@ export async function createFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +30,7 @@ export async function createFile(
|
||||
throw new Error(`文件已存在: ${absolutePath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法创建相对路径的目录");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 如果目录不存在,继续创建
|
||||
if (error.code !== 'FileNotFound') {
|
||||
if (error.code !== "FileNotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法删除相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +205,9 @@ export async function updateFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +246,9 @@ export async function appendToFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法追加相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -274,7 +286,9 @@ export async function replaceFile(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,14 +305,17 @@ export async function replaceFile(
|
||||
|
||||
// 转义特殊字符,将字符串作为字面量处理
|
||||
const escapeRegExp = (str: string) => {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
};
|
||||
|
||||
// 替换内容 - 如果是字符串,先转义特殊字符
|
||||
let newContent: string;
|
||||
if (typeof searchValue === 'string') {
|
||||
if (typeof searchValue === "string") {
|
||||
const escapedSearch = escapeRegExp(searchValue);
|
||||
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue);
|
||||
newContent = fileContent.replace(
|
||||
new RegExp(escapedSearch, "g"),
|
||||
replaceValue
|
||||
);
|
||||
} else {
|
||||
newContent = fileContent.replace(searchValue, replaceValue);
|
||||
}
|
||||
@ -330,7 +347,9 @@ export async function insertAtLine(
|
||||
if (workspaceFolders && workspaceFolders.length > 0) {
|
||||
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法修改相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -382,7 +401,9 @@ export async function renameFile(
|
||||
absoluteNewPath = path.join(workspaceRoot, newPath);
|
||||
}
|
||||
} else {
|
||||
throw new Error("没有打开的工作区,无法重命名相对路径的文件");
|
||||
throw new Error(
|
||||
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
|
||||
);
|
||||
}
|
||||
|
||||
const oldUri = vscode.Uri.file(absoluteOldPath);
|
||||
@ -401,10 +422,13 @@ export async function renameFile(
|
||||
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
|
||||
} catch (error: any) {
|
||||
// 如果文件不存在,继续重命名
|
||||
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
|
||||
if (
|
||||
error.code !== "FileNotFound" &&
|
||||
!error.message.includes("目标文件已存在")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes('目标文件已存在')) {
|
||||
if (error.message.includes("目标文件已存在")) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,19 +19,43 @@ import { dialogManager, DialogSession } from "../services/dialogService";
|
||||
import { userInteractionManager } from "../services/userInteraction";
|
||||
import { healthCheck } from "../services/apiClient";
|
||||
|
||||
import type { RunMode } from '../types/api';
|
||||
|
||||
/** 是否使用后端服务(可通过配置控制) */
|
||||
let useBackendService = true;
|
||||
|
||||
/** 当前对话会话 */
|
||||
let currentSession: DialogSession | null = null;
|
||||
|
||||
/** 待执行的计划(Plan 模式确认后自动执行) */
|
||||
let pendingPlanExecution: {
|
||||
panel: vscode.WebviewPanel;
|
||||
planTitle: string;
|
||||
extensionPath: string;
|
||||
taskId: string; // 保存 taskId 以便复用
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* 设置待执行的计划(由 ICHelperPanel 调用)
|
||||
*/
|
||||
export function setPendingPlanExecution(
|
||||
panel: vscode.WebviewPanel,
|
||||
planTitle: string,
|
||||
extensionPath: string,
|
||||
taskId: string
|
||||
): void {
|
||||
pendingPlanExecution = { panel, planTitle, extensionPath, taskId };
|
||||
console.log('[MessageHandler] 设置待执行计划:', planTitle, 'taskId:', taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户消息
|
||||
*/
|
||||
export async function handleUserMessage(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath?: string
|
||||
extensionPath?: string,
|
||||
mode?: RunMode
|
||||
) {
|
||||
console.log("收到用户消息:", text);
|
||||
|
||||
@ -63,7 +87,7 @@ export async function handleUserMessage(
|
||||
// 尝试使用后端服务
|
||||
if (useBackendService && extensionPath) {
|
||||
try {
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath);
|
||||
await handleUserMessageWithBackend(panel, text, extensionPath, mode);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("后端服务不可用,回退到本地模式:", error);
|
||||
@ -97,11 +121,16 @@ export async function handleUserMessage(
|
||||
async function handleUserMessageWithBackend(
|
||||
panel: vscode.WebviewPanel,
|
||||
text: string,
|
||||
extensionPath: string
|
||||
extensionPath: string,
|
||||
mode?: RunMode,
|
||||
reuseTaskId?: string // 可选,复用现有 taskId(用于 Plan 模式确认后继续执行)
|
||||
): Promise<void> {
|
||||
// 创建或复用会话
|
||||
if (!currentSession || !currentSession.active) {
|
||||
currentSession = dialogManager.createSession(extensionPath);
|
||||
currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
|
||||
if (reuseTaskId) {
|
||||
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
const historyManager = ChatHistoryManager.getInstance();
|
||||
@ -184,6 +213,29 @@ async function handleUserMessageWithBackend(
|
||||
console.warn("保存AI响应历史失败:", error);
|
||||
}
|
||||
|
||||
// 检查是否有待执行的计划(Plan 模式确认后自动执行)
|
||||
if (pendingPlanExecution) {
|
||||
const { panel: execPanel, planTitle, extensionPath: execPath, taskId: reuseTaskId } = pendingPlanExecution;
|
||||
pendingPlanExecution = null;
|
||||
console.log('[MessageHandler] 自动执行计划:', planTitle, '复用 taskId:', reuseTaskId);
|
||||
|
||||
// 延迟一小段时间确保当前对话完全结束
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// 复用 taskId 创建新会话,确保知识图谱数据不丢失
|
||||
await handleUserMessageWithBackend(
|
||||
execPanel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
execPath,
|
||||
'agent',
|
||||
reuseTaskId // 复用 Plan 模式的 taskId
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[MessageHandler] 自动执行计划失败:', err);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
resolve();
|
||||
},
|
||||
|
||||
@ -201,7 +253,7 @@ async function handleUserMessageWithBackend(
|
||||
onNotification: (message) => {
|
||||
vscode.window.showInformationMessage(message);
|
||||
},
|
||||
});
|
||||
}, mode);
|
||||
});
|
||||
}
|
||||
|
||||
@ -226,6 +278,75 @@ export function abortCurrentDialog(): void {
|
||||
currentSession = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话的 taskId
|
||||
*/
|
||||
export function getCurrentTaskId(): string | null {
|
||||
return currentSession?.getTaskId() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理计划操作(Plan 模式)
|
||||
* @param panel WebView 面板
|
||||
* @param action 操作类型:confirm/modify/cancel
|
||||
* @param planTitle 计划标题
|
||||
* @param extensionPath 扩展路径
|
||||
*/
|
||||
export async function handlePlanAction(
|
||||
panel: vscode.WebviewPanel,
|
||||
action: string,
|
||||
planTitle: string,
|
||||
extensionPath: string
|
||||
): Promise<void> {
|
||||
console.log('[handlePlanAction] action:', action, 'planTitle:', planTitle);
|
||||
|
||||
switch (action) {
|
||||
case 'confirm':
|
||||
// 确认执行:切换到 Agent 模式并发送执行消息
|
||||
panel.webview.postMessage({
|
||||
command: 'switchMode',
|
||||
mode: 'agent'
|
||||
});
|
||||
// 发送执行消息
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请按照刚才的计划执行:${planTitle}`,
|
||||
extensionPath,
|
||||
'agent'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'modify':
|
||||
// 修改计划:提示用户输入修改建议
|
||||
const modification = await vscode.window.showInputBox({
|
||||
prompt: '请输入您对计划的修改建议',
|
||||
placeHolder: '例如:第2步需要先检查文件是否存在...',
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (modification) {
|
||||
await handleUserMessage(
|
||||
panel,
|
||||
`请根据以下建议修改计划:${modification}`,
|
||||
extensionPath,
|
||||
'plan'
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
// 取消计划:通知用户
|
||||
panel.webview.postMessage({
|
||||
command: 'addMessage',
|
||||
text: '计划已取消。',
|
||||
sender: 'bot'
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('[handlePlanAction] 未知操作:', action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件操作命令
|
||||
*/
|
||||
|
||||
@ -47,7 +47,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
||||
(message) => {
|
||||
switch (message.command) {
|
||||
case "sendMessage":
|
||||
handleUserMessage(panel, message.text, context.extensionPath);
|
||||
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
|
||||
break;
|
||||
case "readFile":
|
||||
handleReadFile(panel, message.filePath);
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
/**
|
||||
* 模式选择器组件
|
||||
* 提供 Agent/Ask/Auto 三种模式的选择功能
|
||||
* 提供 Plan/Ask/Agent/Auto 四种模式的选择功能
|
||||
*
|
||||
* 模式说明:
|
||||
* - Plan: 只读模式,只能查询分析,不能写文件
|
||||
* - Ask: 逐个确认,每个写操作需用户确认
|
||||
* - Agent: 智能体自主,自动执行大部分操作
|
||||
* - Auto: 完全自动,所有操作自动执行
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -17,12 +23,25 @@ export function getModeSelectorContent(): string {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mode-dropdown" id="modeDropdown">
|
||||
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
|
||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
|
||||
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
|
||||
<span class="mode-option-label">Plan</span>
|
||||
<span class="mode-option-desc">只读模式</span>
|
||||
</div>
|
||||
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
|
||||
<span class="mode-option-label">Ask</span>
|
||||
<span class="mode-option-desc">逐个确认</span>
|
||||
</div>
|
||||
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
|
||||
<span class="mode-option-label">Agent</span>
|
||||
<span class="mode-option-desc">智能体自主</span>
|
||||
</div>
|
||||
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">
|
||||
<span class="mode-option-label">Auto</span>
|
||||
<span class="mode-option-desc">完全自动</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tooltiptext">切换模式</span>
|
||||
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -69,7 +88,7 @@ export function getModeSelectorStyles(): string {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 2px);
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
min-width: 140px;
|
||||
background: var(--vscode-dropdown-background);
|
||||
border: 1px solid var(--vscode-dropdown-border);
|
||||
border-radius: 4px;
|
||||
@ -83,7 +102,10 @@ export function getModeSelectorStyles(): string {
|
||||
}
|
||||
/* 模式选择器的选项样式 */
|
||||
.mode-option {
|
||||
padding: 6px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
@ -93,8 +115,15 @@ export function getModeSelectorStyles(): string {
|
||||
background: rgba(128, 128, 128, 0.3);
|
||||
}
|
||||
.mode-option.selected {
|
||||
background: rgba(128, 128, 128, 0.5);
|
||||
color: var(--vscode-foreground);
|
||||
background: rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
.mode-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.mode-option-desc {
|
||||
font-size: 10px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-left: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@ -124,10 +153,23 @@ export function getModeSelectorScript(): string {
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
|
||||
// 更新 tooltip
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式',
|
||||
'auto': '完全自动 - 所有操作自动执行'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
|
||||
@ -261,7 +261,7 @@ export function getInputAreaStyles(): string {
|
||||
*/
|
||||
export function getInputAreaScript(): string {
|
||||
return `
|
||||
${getModeSelectorScript()}
|
||||
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
|
||||
${getModelSelectorScript()}
|
||||
${getContextButtonScript()}
|
||||
${getContextCompressScript()}
|
||||
@ -271,6 +271,10 @@ export function getInputAreaScript(): string {
|
||||
// 对话状态管理
|
||||
let isConversationActive = false;
|
||||
|
||||
// 工作区检测状态
|
||||
let hasCheckedWorkspace = false; // 是否已经检测过工作区
|
||||
let hasWorkspace = true; // 工作区状态
|
||||
|
||||
// 自动调整 textarea 高度
|
||||
function autoResizeTextarea() {
|
||||
if (messageInput) {
|
||||
@ -283,11 +287,16 @@ export function getInputAreaScript(): string {
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('input', autoResizeTextarea);
|
||||
|
||||
// 监听点击事件,检测工作区状态
|
||||
messageInput.addEventListener('focus', () => {
|
||||
if (!hasCheckedWorkspace) {
|
||||
hasCheckedWorkspace = true;
|
||||
vscode.postMessage({ command: 'checkWorkspace' });
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时调整一次高度
|
||||
autoResizeTextarea();
|
||||
|
||||
// 聚焦到输入框
|
||||
messageInput.focus();
|
||||
}
|
||||
|
||||
// 切换发送按钮状态
|
||||
@ -326,15 +335,24 @@ export function getInputAreaScript(): string {
|
||||
const text = messageInput.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// 检查工作区状态
|
||||
if (!hasWorkspace) {
|
||||
// 如果没有工作区,阻止发送并清空输入框
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea();
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
|
||||
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
|
||||
const planMode = document.getElementById('planToggle')?.checked || false;
|
||||
|
||||
addMessage(text, 'user');
|
||||
|
||||
// 切换按钮为暂停状态
|
||||
setSendButtonState(true);
|
||||
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
|
||||
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model, planMode: planMode });
|
||||
messageInput.value = '';
|
||||
autoResizeTextarea(); // 重置输入框高度
|
||||
messageInput.focus();
|
||||
|
||||
@ -611,6 +611,88 @@ export function getMessageAreaStyles(): string {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 计划卡片样式 */
|
||||
.segment-plan {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.plan-card {
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.plan-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--vscode-sideBar-background);
|
||||
border-bottom: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.plan-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.plan-body {
|
||||
padding: 12px;
|
||||
}
|
||||
.plan-summary {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-steps {
|
||||
font-size: 13px;
|
||||
}
|
||||
.plan-step {
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.plan-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.step-num {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.plan-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-sideBar-background);
|
||||
}
|
||||
.plan-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.plan-btn-confirm {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
}
|
||||
.plan-btn-confirm:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.plan-btn-modify {
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
}
|
||||
.plan-btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
${getWaveformPreviewContent()}
|
||||
`;
|
||||
}
|
||||
@ -1034,6 +1116,93 @@ export function getMessageAreaScript(): string {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染(类似 askUser)
|
||||
segmentDiv.className += ' segment-plan';
|
||||
|
||||
// 检查是否已回答
|
||||
const isAnswered = answeredQuestions.has(segment.askId);
|
||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
||||
|
||||
if (isAnswered) {
|
||||
segmentDiv.classList.add('answered');
|
||||
}
|
||||
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
// 选项按钮
|
||||
const options = ['确认执行', '修改计划', '取消'];
|
||||
const optionsHtml = options.map(opt => {
|
||||
const isSelected = isAnswered && opt === selectedAnswer;
|
||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
||||
}).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">📋</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>
|
||||
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
|
||||
<input type="text" class="custom-input" placeholder="输入修改建议..." />
|
||||
<button class="custom-submit">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 只在未回答时添加事件监听
|
||||
if (!isAnswered) {
|
||||
setTimeout(() => {
|
||||
const optionButtons = segmentDiv.querySelectorAll('.question-option');
|
||||
optionButtons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const option = this.getAttribute('data-option');
|
||||
// 发送答案到后端
|
||||
handleQuestionAnswerInSegment(segment.askId, option, segmentDiv);
|
||||
// 同时发送 planAction 用于模式切换
|
||||
const actionMap = {
|
||||
'确认执行': 'confirm',
|
||||
'修改计划': 'modify',
|
||||
'取消': 'cancel'
|
||||
};
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: actionMap[option] || option,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||
const customInput = segmentDiv.querySelector('.custom-input');
|
||||
if (submitBtn && customInput) {
|
||||
submitBtn.addEventListener('click', function() {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
customInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
const customValue = customInput.value.trim();
|
||||
if (customValue) {
|
||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
currentSegmentedMessage.appendChild(segmentDiv);
|
||||
@ -1203,6 +1372,47 @@ export function getMessageAreaScript(): string {
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
} else if (segment.type === 'plan') {
|
||||
// 计划卡片渲染
|
||||
segmentDiv.className += ' segment-plan';
|
||||
const stepsHtml = (segment.planSteps || []).map((step, i) =>
|
||||
\`<div class="plan-step"><span class="step-num">\${i + 1}.</span> \${step}</div>\`
|
||||
).join('');
|
||||
|
||||
segmentDiv.innerHTML = \`
|
||||
<div class="plan-card">
|
||||
<div class="plan-header">
|
||||
<span class="plan-icon">📋</span>
|
||||
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
|
||||
</div>
|
||||
<div class="plan-body">
|
||||
<div class="plan-summary">\${segment.planSummary || ''}</div>
|
||||
<div class="plan-steps">\${stepsHtml}</div>
|
||||
</div>
|
||||
<div class="plan-actions">
|
||||
<button class="plan-btn plan-btn-confirm" data-action="confirm">确认执行</button>
|
||||
<button class="plan-btn plan-btn-modify" data-action="modify">修改计划</button>
|
||||
<button class="plan-btn plan-btn-cancel" data-action="cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
\`;
|
||||
|
||||
// 绑定按钮事件
|
||||
setTimeout(() => {
|
||||
const planCard = segmentDiv.querySelector('.plan-card');
|
||||
if (planCard) {
|
||||
planCard.querySelectorAll('.plan-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const action = e.currentTarget?.dataset?.action;
|
||||
vscode.postMessage({
|
||||
command: 'planAction',
|
||||
action: action,
|
||||
planTitle: segment.planTitle
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
container.appendChild(segmentDiv);
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
/**
|
||||
* Plan 开关组件
|
||||
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取 Plan 开关的 HTML 内容
|
||||
* 已禁用,仅保留样式展示
|
||||
*/
|
||||
export function getPlanToggleContent(): string {
|
||||
return `
|
||||
<div class="tooltip">
|
||||
<label class="plan-toggle">
|
||||
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
|
||||
<label class="plan-toggle plan-toggle-disabled">
|
||||
<input type="checkbox" id="planToggle" disabled>
|
||||
<span class="plan-toggle-slider"></span>
|
||||
<span class="plan-toggle-label">Plan</span>
|
||||
</label>
|
||||
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
|
||||
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
|
||||
font-weight: 500;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
/* 禁用状态样式 */
|
||||
.plan-toggle-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.plan-toggle-disabled .plan-toggle-slider {
|
||||
background: var(--vscode-input-background);
|
||||
border-color: var(--vscode-input-border);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@ -263,61 +263,63 @@ export function getWaveformPreviewScript(): string {
|
||||
const timeRange = maxTime - minTime || 1;
|
||||
|
||||
// 绘制波形
|
||||
let pathData = '';
|
||||
let lastX = leftMargin;
|
||||
let lastValue = signal.values[0].value;
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
let pathData = '';
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const value = point.value;
|
||||
|
||||
if (signal.width === 1) {
|
||||
// 单比特信号 - 绘制数字波形
|
||||
const yHigh = y;
|
||||
const yLow = y + signalHeight;
|
||||
const currentY = (value === '1') ? yHigh : yLow;
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
const currentY = (point.value === '1') ? yHigh : yLow;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${currentY}\`;
|
||||
} else {
|
||||
// 绘制垂直跳变
|
||||
const prevY = (lastValue === '1') ? yHigh : yLow;
|
||||
const prevValue = signal.values[i - 1].value;
|
||||
const prevY = (prevValue === '1') ? yHigh : yLow;
|
||||
if (prevY !== currentY) {
|
||||
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
|
||||
} else {
|
||||
pathData += \` L \${x} \${currentY}\`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastValue = value;
|
||||
lastX = x;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(梯形)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 5;
|
||||
|
||||
if (i === 0) {
|
||||
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
} else {
|
||||
// 绘制梯形过渡
|
||||
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
|
||||
}
|
||||
|
||||
lastX = x;
|
||||
}
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
if (signal.width === 1) {
|
||||
const lastY = (lastValue === '1') ? y : (y + signalHeight);
|
||||
// 延伸到右边界
|
||||
const lastValue = signal.values[signal.values.length - 1].value;
|
||||
const lastY = (lastValue === '1') ? yHigh : yLow;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
|
||||
} else {
|
||||
const yMid = y + signalHeight / 2;
|
||||
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
|
||||
}
|
||||
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
} else {
|
||||
// 多比特信号 - 绘制总线波形(上下双线)
|
||||
const yTop = y + 5;
|
||||
const yBottom = y + signalHeight - 5;
|
||||
const transitionWidth = 4;
|
||||
|
||||
let topPath = \`M \${leftMargin} \${yTop}\`;
|
||||
let bottomPath = \`M \${leftMargin} \${yBottom}\`;
|
||||
|
||||
signal.values.forEach((point, i) => {
|
||||
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
|
||||
|
||||
// 上线和下线都延伸到变化点
|
||||
topPath += \` L \${x} \${yTop}\`;
|
||||
bottomPath += \` L \${x} \${yBottom}\`;
|
||||
|
||||
// 绘制梯形过渡
|
||||
topPath += \` L \${x + transitionWidth} \${yBottom} L \${x + transitionWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${x + transitionWidth} \${yTop} L \${x + transitionWidth} \${yBottom}\`;
|
||||
});
|
||||
|
||||
// 延伸到右边界
|
||||
topPath += \` L \${leftMargin + waveformWidth} \${yTop}\`;
|
||||
bottomPath += \` L \${leftMargin + waveformWidth} \${yBottom}\`;
|
||||
|
||||
svgContent += \`<path d="\${topPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
svgContent += \`<path d="\${bottomPath}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
|
||||
}
|
||||
});
|
||||
|
||||
// 绘制时间轴
|
||||
|
||||
@ -255,7 +255,7 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
padding: 0;
|
||||
}
|
||||
.message-segment {
|
||||
padding: 10px 22 px;
|
||||
padding: 10px 22px;
|
||||
}
|
||||
.segment-text {
|
||||
line-height: 1.6;
|
||||
@ -417,6 +417,62 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
let loadingIndicator = null;
|
||||
let currentSegmentedMessage = null; // 当前分段消息容器
|
||||
|
||||
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
|
||||
let currentMode = 'agent';
|
||||
|
||||
function toggleModeDropdown() {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
const modelSelectEl = document.getElementById('modelSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.toggle('active');
|
||||
if (modelSelectEl) {
|
||||
modelSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectMode(value, label) {
|
||||
currentMode = value;
|
||||
const modeValue = document.getElementById('modeValue');
|
||||
const modeTooltip = document.getElementById('modeTooltip');
|
||||
if (modeValue) {
|
||||
modeValue.textContent = label;
|
||||
}
|
||||
if (modeTooltip) {
|
||||
const tooltipMap = {
|
||||
'plan': '只读模式 - 只能查询分析',
|
||||
'ask': '逐个确认 - 每个写操作需确认',
|
||||
'agent': '智能体自主模式',
|
||||
'auto': '完全自动 - 所有操作自动执行'
|
||||
};
|
||||
modeTooltip.textContent = tooltipMap[value] || '切换模式';
|
||||
}
|
||||
const options = document.querySelectorAll('.mode-option');
|
||||
options.forEach(option => {
|
||||
if (option.getAttribute('data-value') === value) {
|
||||
option.classList.add('selected');
|
||||
} else {
|
||||
option.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const modeSelectEl = document.getElementById('modeSelect');
|
||||
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
|
||||
modeSelectEl.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// ========== 模式选择器脚本结束 ==========
|
||||
|
||||
function quickAction(type) {
|
||||
const questions = {
|
||||
counter: '生成一个4位同步计数器',
|
||||
@ -443,8 +499,6 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
messageInput.focus();
|
||||
|
||||
// 监听来自插件的消息
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
@ -516,6 +570,14 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
hideLoadingIndicator();
|
||||
break;
|
||||
|
||||
case 'workspaceStatus':
|
||||
// 更新工作区状态
|
||||
if (typeof hasWorkspace !== 'undefined') {
|
||||
hasWorkspace = message.hasWorkspace;
|
||||
console.log('[WebView] 工作区状态:', hasWorkspace);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vcdInfo':
|
||||
// 渲染迷你波形预览信息
|
||||
try {
|
||||
@ -588,6 +650,27 @@ export function getWebviewContent(iconUri?: string): string {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'switchMode':
|
||||
// 切换运行模式(Plan 确认后自动切换到 Agent)
|
||||
if (message.mode && typeof selectMode === 'function') {
|
||||
const labelMap = {
|
||||
'plan': 'Plan',
|
||||
'ask': 'Ask',
|
||||
'agent': 'Agent',
|
||||
'auto': 'Auto'
|
||||
};
|
||||
selectMode(message.mode, labelMap[message.mode] || message.mode);
|
||||
console.log('[WebView] 模式已切换到:', message.mode);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'addMessage':
|
||||
// 添加消息(通用)
|
||||
if (message.text && message.sender) {
|
||||
addMessage(message.text, message.sender);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WebView] 未处理的消息类型:', message.command);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user