11 Commits

Author SHA1 Message Date
430581598b Merge branch 'feat/back-to-front' into feat/plugin-front-end 2025-12-30 20:46:26 +08:00
2d5b297171 Merge branch 'feat/back-to-front' of https://git.pengyejiatu.com/pengyejiatu/IC-Coder-Plugin into feat/back-to-front 2025-12-30 20:43:22 +08:00
023fdb66c3 feat: WebView 集成运行模式
- webviewContent 集成模式选择器脚本和样式
- inputArea 适配模式传递
- ICViewProvider/ICHelperPanel 传递模式参数
2025-12-30 20:42:44 +08:00
42481cd314 feat: SSE 事件处理和计划确认 UI
- sseHandler 添加 onPlanConfirm、onToolConfirm 回调
- messageArea 添加计划确认对话框渲染
2025-12-30 20:42:35 +08:00
e77194628a feat: 模式传递和 API 调用
- dialogService 接收并传递 mode 参数
- apiClient 构造带 mode 的请求
- messageHandler 从 WebView 消息获取 mode
2025-12-30 20:42:28 +08:00
2aff54de74 feat: 实现模式选择器 UI
- agentModeSelector 添加下拉菜单和模式切换逻辑
- planToggle 适配新的模式系统
2025-12-30 20:42:19 +08:00
91fadf591f feat: 添加运行模式类型定义
- 添加 RunMode 类型(plan/ask/agent/auto)
- 添加 PlanConfirmEvent、ToolConfirmEvent 类型
- DialogRequest 使用 mode 字段替代 toolMode/planMode
2025-12-30 20:42:11 +08:00
02b56a7031 feat: 更新插件分类为聊天和编程语言 2025-12-30 16:49:49 +08:00
c42ebdfe59 feat: 更新默认后端服务地址为192.168.1.108 2025-12-30 16:10:49 +08:00
3f0cc8ae29 feat: 添加工作区状态检查功能,优化用户体验
- 用户鼠标聚焦到输入框中就弹窗提示用户打开 优化用户体验
2025-12-30 16:02:36 +08:00
0f458f6299 feat: 优化波形预览脚本绘制逻辑,支持单比特和多比特信号的不同绘制方式 2025-12-30 15:35:35 +08:00
16 changed files with 880 additions and 108 deletions

View File

@ -9,7 +9,8 @@
}, },
"icon": "media/icon.png", "icon": "media/icon.png",
"categories": [ "categories": [
"Other" "Chat",
"Programming Languages"
], ],
"keywords": [ "keywords": [
"IC", "IC",

View File

@ -16,7 +16,7 @@ export interface IccoderConfig {
/** 默认配置 */ /** 默认配置 */
const DEFAULT_CONFIG: IccoderConfig = { const DEFAULT_CONFIG: IccoderConfig = {
backendUrl: "http://localhost:8080", backendUrl: "http://192.168.1.108:2233",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
}; };

View File

@ -9,6 +9,9 @@ import {
handleReplaceInFile, handleReplaceInFile,
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { VCDViewerPanel } from "./VCDViewerPanel"; import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { ChatHistoryManager } from "../utils/chatHistoryManager";
@ -23,21 +26,27 @@ export async function showICHelperPanel(
) { ) {
// 检查用户是否已登录 // 检查用户是否已登录
try { try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false }); const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (!session) { 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 === "立即登录") { if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); 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; return;
} }
@ -107,7 +116,12 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文 // 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId); historyManager.switchToPanelTask(panelId);
handleUserMessage(panel, message.text, context.extensionPath); handleUserMessage(
panel,
message.text,
context.extensionPath,
message.mode
);
break; break;
case "readFile": case "readFile":
handleReadFile(panel, message.filePath); handleReadFile(panel, message.filePath);
@ -181,6 +195,56 @@ export async function showICHelperPanel(
case "abortDialog": case "abortDialog":
abortCurrentDialog(); abortCurrentDialog();
break; 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, undefined,

View File

@ -6,7 +6,7 @@ import * as https from 'https';
import * as http from 'http'; import * as http from 'http';
import { URL } from 'url'; import { URL } from 'url';
import { getApiUrl, getConfig } from '../config/settings'; 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 请求选项 * 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 * GET /api/dialog/health

View File

@ -7,13 +7,14 @@ import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from '
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor'; import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
import { userInteractionManager } from './userInteraction'; import { userInteractionManager } from './userInteraction';
import { getConfig } from '../config/settings'; 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 { export interface MessageSegment {
type: 'text' | 'tool' | 'question' | 'agent'; type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
content?: string; content?: string;
toolName?: string; toolName?: string;
toolStatus?: 'running' | 'success' | 'error'; toolStatus?: 'running' | 'success' | 'error';
@ -26,6 +27,10 @@ export interface MessageSegment {
agentName?: string; agentName?: string;
agentStatus?: 'running' | 'completed' | 'error'; agentStatus?: 'running' | 'completed' | 'error';
agentSteps?: AgentStep[]; agentSteps?: AgentStep[];
// 计划相关字段
planTitle?: string;
planSteps?: string[];
planSummary?: string;
} }
/** /**
@ -51,6 +56,10 @@ export interface DialogCallbacks {
onToolComplete?: (toolName: string, result: string) => void; onToolComplete?: (toolName: string, result: string) => void;
/** 工具执行错误 */ /** 工具执行错误 */
onToolError?: (toolName: string, error: 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 */ /** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void; onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */ /** 实时更新段落(流式过程中) */
@ -75,8 +84,9 @@ export class DialogSession {
private segments: MessageSegment[] = []; private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null; private currentTextSegment: MessageSegment | null = null;
constructor(extensionPath: string) { constructor(extensionPath: string, existingTaskId?: string) {
this.taskId = generateTaskId(); // 支持复用现有 taskId用于 Plan 模式确认后继续执行)
this.taskId = existingTaskId || generateTaskId();
this.toolContext = createToolExecutorContext(extensionPath); this.toolContext = createToolExecutorContext(extensionPath);
} }
@ -142,12 +152,52 @@ export class DialogSession {
return this.isActive; 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( async sendMessage(
message: string, message: string,
callbacks: DialogCallbacks callbacks: DialogCallbacks,
mode?: RunMode
): Promise<void> { ): Promise<void> {
if (this.isActive) { if (this.isActive) {
callbacks.onError?.('当前有对话正在进行中'); callbacks.onError?.('当前有对话正在进行中');
@ -164,7 +214,7 @@ export class DialogSession {
taskId: this.taskId, taskId: this.taskId,
message, message,
userId: config.userId, userId: config.userId,
toolMode: 'AGENT' mode: mode || 'agent'
}; };
const sseCallbacks: SSECallbacks = { const sseCallbacks: SSECallbacks = {
@ -248,6 +298,72 @@ export class DialogSession {
callbacks.onSegmentUpdate?.(this.segments); 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) => { onAskUser: async (data: AskUserEvent) => {
this.finalizeTextSegment(); this.finalizeTextSegment();
this.segments.push({ 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) { if (this.currentSession?.active) {
this.currentSession.abort(); this.currentSession.abort();
} }
this.currentSession = new DialogSession(extensionPath); this.currentSession = new DialogSession(extensionPath, existingTaskId);
return this.currentSession; return this.currentSession;
} }

View File

@ -13,6 +13,8 @@ import type {
SSEEventType, SSEEventType,
TextDeltaEvent, TextDeltaEvent,
ToolCallRequest, ToolCallRequest,
ToolConfirmEvent,
PlanConfirmEvent,
AskUserEvent, AskUserEvent,
CompleteEvent, CompleteEvent,
ErrorEvent, ErrorEvent,
@ -36,6 +38,10 @@ export interface SSECallbacks {
onTextDelta?: (data: TextDeltaEvent) => void; onTextDelta?: (data: TextDeltaEvent) => void;
/** 收到工具调用请求 */ /** 收到工具调用请求 */
onToolCall?: (data: ToolCallRequest) => void; onToolCall?: (data: ToolCallRequest) => void;
/** 收到工具确认请求Ask 模式) */
onToolConfirm?: (data: ToolConfirmEvent) => void;
/** 收到计划确认请求Plan 模式) */
onPlanConfirm?: (data: PlanConfirmEvent) => void;
/** 工具开始执行 */ /** 工具开始执行 */
onToolStart?: (data: ToolStartEvent) => void; onToolStart?: (data: ToolStartEvent) => void;
/** 工具执行完成 */ /** 工具执行完成 */
@ -136,7 +142,7 @@ export async function startStreamDialog(
const body = JSON.stringify(request); 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) => { return new Promise((resolve, reject) => {
const options: http.RequestOptions = { const options: http.RequestOptions = {
@ -268,6 +274,12 @@ function dispatchEvent(
case 'tool_call': case 'tool_call':
callbacks.onToolCall?.(data as ToolCallRequest); callbacks.onToolCall?.(data as ToolCallRequest);
break; break;
case 'tool_confirm':
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
break;
case 'plan_confirm':
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
break;
case 'tool_start': case 'tool_start':
callbacks.onToolStart?.(data as ToolStartEvent); callbacks.onToolStart?.(data as ToolStartEvent);
break; break;

View File

@ -5,6 +5,15 @@
// ============== 对话请求/响应 ============== // ============== 对话请求/响应 ==============
/**
* 运行模式类型
* - plan: 只读模式,只能查询分析
* - ask: 逐个确认,每个写操作需确认
* - agent: 智能体自主(默认)
* - auto: 完全自动
*/
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
/** /**
* 对话请求 * 对话请求
* POST /api/dialog/stream * POST /api/dialog/stream
@ -16,8 +25,8 @@ export interface DialogRequest {
message: string; message: string;
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
/** 工具模式 */ /** 运行模式 */
toolMode: 'ASK' | 'AGENT'; mode: RunMode;
} }
// ============== SSE 事件类型 ============== // ============== SSE 事件类型 ==============
@ -26,6 +35,8 @@ export interface DialogRequest {
export type SSEEventType = export type SSEEventType =
| 'text_delta' // 文本增量 | 'text_delta' // 文本增量
| 'tool_call' // 客户端工具调用请求 | 'tool_call' // 客户端工具调用请求
| 'tool_confirm' // 工具确认请求Ask 模式)
| 'plan_confirm' // 计划确认请求Plan 模式)
| 'tool_start' // 工具开始执行 | 'tool_start' // 工具开始执行
| 'tool_complete' // 工具执行完成 | 'tool_complete' // 工具执行完成
| 'tool_error' // 工具执行错误 | 'tool_error' // 工具执行错误
@ -63,6 +74,32 @@ export interface ToolErrorEvent {
error: string; 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 事件数据 */ /** ask_user 事件数据 */
export interface AskUserEvent { export interface AskUserEvent {
askId: string; askId: string;
@ -229,6 +266,21 @@ export interface ToolResultResponse {
error?: string; error?: string;
} }
// ============== 工具确认响应 ==============
/**
* 工具确认响应请求
* POST /api/tool/confirm
*/
export interface ToolConfirmResponse {
/** 确认ID与 ToolConfirmEvent.confirmId 对应 */
confirmId: number;
/** 任务ID */
taskId: string;
/** 是否批准执行 */
approved: boolean;
}
// ============== 辅助类型 ============== // ============== 辅助类型 ==============
/** 后端工具名称 */ /** 后端工具名称 */

View File

@ -16,7 +16,9 @@ export async function createFile(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法创建相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
);
} }
} }
@ -28,7 +30,7 @@ export async function createFile(
throw new Error(`文件已存在: ${absolutePath}`); throw new Error(`文件已存在: ${absolutePath}`);
} catch (error: any) { } catch (error: any) {
// 如果文件不存在,继续创建 // 如果文件不存在,继续创建
if (error.code !== 'FileNotFound') { if (error.code !== "FileNotFound") {
throw error; throw error;
} }
} }
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法创建相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
);
} }
} }
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
} else { } else {
throw new Error("没有打开的工作区,无法创建相对路径的目录"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
);
} }
} }
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
} }
} catch (error: any) { } catch (error: any) {
// 如果目录不存在,继续创建 // 如果目录不存在,继续创建
if (error.code !== 'FileNotFound') { if (error.code !== "FileNotFound") {
throw error; throw error;
} }
} }
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法删除相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
);
} }
} }
@ -197,7 +205,9 @@ export async function updateFile(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法修改相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
} }
} }
@ -236,7 +246,9 @@ export async function appendToFile(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法追加相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
);
} }
} }
@ -274,7 +286,9 @@ export async function replaceFile(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法修改相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
} }
} }
@ -291,14 +305,17 @@ export async function replaceFile(
// 转义特殊字符,将字符串作为字面量处理 // 转义特殊字符,将字符串作为字面量处理
const escapeRegExp = (str: string) => { const escapeRegExp = (str: string) => {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}; };
// 替换内容 - 如果是字符串,先转义特殊字符 // 替换内容 - 如果是字符串,先转义特殊字符
let newContent: string; let newContent: string;
if (typeof searchValue === 'string') { if (typeof searchValue === "string") {
const escapedSearch = escapeRegExp(searchValue); const escapedSearch = escapeRegExp(searchValue);
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue); newContent = fileContent.replace(
new RegExp(escapedSearch, "g"),
replaceValue
);
} else { } else {
newContent = fileContent.replace(searchValue, replaceValue); newContent = fileContent.replace(searchValue, replaceValue);
} }
@ -330,7 +347,9 @@ export async function insertAtLine(
if (workspaceFolders && workspaceFolders.length > 0) { if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath); absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else { } else {
throw new Error("没有打开的工作区,无法修改相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
} }
} }
@ -382,7 +401,9 @@ export async function renameFile(
absoluteNewPath = path.join(workspaceRoot, newPath); absoluteNewPath = path.join(workspaceRoot, newPath);
} }
} else { } else {
throw new Error("没有打开的工作区,无法重命名相对路径的文件"); throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
);
} }
const oldUri = vscode.Uri.file(absoluteOldPath); const oldUri = vscode.Uri.file(absoluteOldPath);
@ -401,10 +422,13 @@ export async function renameFile(
throw new Error(`目标文件已存在: ${absoluteNewPath}`); throw new Error(`目标文件已存在: ${absoluteNewPath}`);
} catch (error: any) { } catch (error: any) {
// 如果文件不存在,继续重命名 // 如果文件不存在,继续重命名
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) { if (
error.code !== "FileNotFound" &&
!error.message.includes("目标文件已存在")
) {
throw error; throw error;
} }
if (error.message.includes('目标文件已存在')) { if (error.message.includes("目标文件已存在")) {
throw error; throw error;
} }
} }

View File

@ -19,19 +19,43 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction"; import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient"; import { healthCheck } from "../services/apiClient";
import type { RunMode } from '../types/api';
/** 是否使用后端服务(可通过配置控制) */ /** 是否使用后端服务(可通过配置控制) */
let useBackendService = true; let useBackendService = true;
/** 当前对话会话 */ /** 当前对话会话 */
let currentSession: DialogSession | null = null; 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( export async function handleUserMessage(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
text: string, text: string,
extensionPath?: string extensionPath?: string,
mode?: RunMode
) { ) {
console.log("收到用户消息:", text); console.log("收到用户消息:", text);
@ -63,7 +87,7 @@ export async function handleUserMessage(
// 尝试使用后端服务 // 尝试使用后端服务
if (useBackendService && extensionPath) { if (useBackendService && extensionPath) {
try { try {
await handleUserMessageWithBackend(panel, text, extensionPath); await handleUserMessageWithBackend(panel, text, extensionPath, mode);
return; return;
} catch (error) { } catch (error) {
console.error("后端服务不可用,回退到本地模式:", error); console.error("后端服务不可用,回退到本地模式:", error);
@ -97,11 +121,16 @@ export async function handleUserMessage(
async function handleUserMessageWithBackend( async function handleUserMessageWithBackend(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
text: string, text: string,
extensionPath: string extensionPath: string,
mode?: RunMode,
reuseTaskId?: string // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
): Promise<void> { ): Promise<void> {
// 创建或复用会话 // 创建或复用会话
if (!currentSession || !currentSession.active) { if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath); currentSession = dialogManager.createSession(extensionPath, reuseTaskId);
if (reuseTaskId) {
console.log('[MessageHandler] 复用 taskId 创建会话:', reuseTaskId);
}
} }
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
@ -184,6 +213,29 @@ async function handleUserMessageWithBackend(
console.warn("保存AI响应历史失败:", error); 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(); resolve();
}, },
@ -201,7 +253,7 @@ async function handleUserMessageWithBackend(
onNotification: (message) => { onNotification: (message) => {
vscode.window.showInformationMessage(message); vscode.window.showInformationMessage(message);
}, },
}); }, mode);
}); });
} }
@ -226,6 +278,75 @@ export function abortCurrentDialog(): void {
currentSession = null; 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);
}
}
/** /**
* 解析文件操作命令 * 解析文件操作命令
*/ */

View File

@ -47,7 +47,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
(message) => { (message) => {
switch (message.command) { switch (message.command) {
case "sendMessage": case "sendMessage":
handleUserMessage(panel, message.text, context.extensionPath); handleUserMessage(panel, message.text, context.extensionPath, message.mode);
break; break;
case "readFile": case "readFile":
handleReadFile(panel, message.filePath); handleReadFile(panel, message.filePath);

View File

@ -1,6 +1,12 @@
/** /**
* 模式选择器组件 * 模式选择器组件
* 提供 Agent/Ask/Auto 种模式的选择功能 * 提供 Plan/Ask/Agent/Auto 种模式的选择功能
*
* 模式说明:
* - Plan: 只读模式,只能查询分析,不能写文件
* - Ask: 逐个确认,每个写操作需用户确认
* - Agent: 智能体自主,自动执行大部分操作
* - Auto: 完全自动,所有操作自动执行
*/ */
/** /**
@ -17,12 +23,25 @@ export function getModeSelectorContent(): string {
</svg> </svg>
</div> </div>
<div class="mode-dropdown" id="modeDropdown"> <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="plan" onclick="selectMode('plan', 'Plan')">
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div> <span class="mode-option-label">Plan</span>
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div> <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>
</div> </div>
<span class="tooltiptext">切换模式</span> <span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
</div> </div>
`; `;
} }
@ -69,7 +88,7 @@ export function getModeSelectorStyles(): string {
position: absolute; position: absolute;
bottom: calc(100% + 2px); bottom: calc(100% + 2px);
left: 0; left: 0;
min-width: 100%; min-width: 140px;
background: var(--vscode-dropdown-background); background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border); border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px; border-radius: 4px;
@ -83,7 +102,10 @@ export function getModeSelectorStyles(): string {
} }
/* 模式选择器的选项样式 */ /* 模式选择器的选项样式 */
.mode-option { .mode-option {
padding: 6px 12px; display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
@ -93,8 +115,15 @@ export function getModeSelectorStyles(): string {
background: rgba(128, 128, 128, 0.3); background: rgba(128, 128, 128, 0.3);
} }
.mode-option.selected { .mode-option.selected {
background: rgba(128, 128, 128, 0.5); background: rgba(64, 158, 255, 0.2);
color: var(--vscode-foreground); }
.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) { function selectMode(value, label) {
currentMode = value; currentMode = value;
const modeValue = document.getElementById('modeValue'); const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) { if (modeValue) {
modeValue.textContent = label; modeValue.textContent = label;
} }
// 更新 tooltip
if (modeTooltip) {
const tooltipMap = {
'plan': '只读模式 - 只能查询分析',
'ask': '逐个确认 - 每个写操作需确认',
'agent': '智能体自主模式',
'auto': '完全自动 - 所有操作自动执行'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
// 更新选中状态 // 更新选中状态
const options = document.querySelectorAll('.mode-option'); const options = document.querySelectorAll('.mode-option');
options.forEach(option => { options.forEach(option => {

View File

@ -261,7 +261,7 @@ export function getInputAreaStyles(): string {
*/ */
export function getInputAreaScript(): string { export function getInputAreaScript(): string {
return ` return `
${getModeSelectorScript()} // 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()} ${getModelSelectorScript()}
${getContextButtonScript()} ${getContextButtonScript()}
${getContextCompressScript()} ${getContextCompressScript()}
@ -271,6 +271,10 @@ export function getInputAreaScript(): string {
// 对话状态管理 // 对话状态管理
let isConversationActive = false; let isConversationActive = false;
// 工作区检测状态
let hasCheckedWorkspace = false; // 是否已经检测过工作区
let hasWorkspace = true; // 工作区状态
// 自动调整 textarea 高度 // 自动调整 textarea 高度
function autoResizeTextarea() { function autoResizeTextarea() {
if (messageInput) { if (messageInput) {
@ -283,11 +287,16 @@ export function getInputAreaScript(): string {
if (messageInput) { if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea); messageInput.addEventListener('input', autoResizeTextarea);
// 监听点击事件,检测工作区状态
messageInput.addEventListener('focus', () => {
if (!hasCheckedWorkspace) {
hasCheckedWorkspace = true;
vscode.postMessage({ command: 'checkWorkspace' });
}
});
// 初始化时调整一次高度 // 初始化时调整一次高度
autoResizeTextarea(); autoResizeTextarea();
// 聚焦到输入框
messageInput.focus();
} }
// 切换发送按钮状态 // 切换发送按钮状态
@ -326,15 +335,24 @@ export function getInputAreaScript(): string {
const text = messageInput.value.trim(); const text = messageInput.value.trim();
if (!text) return; if (!text) return;
// 检查工作区状态
if (!hasWorkspace) {
// 如果没有工作区,阻止发送并清空输入框
messageInput.value = '';
autoResizeTextarea();
return;
}
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式 const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
const model = getCurrentModel(); // 从模型选择器组件获取当前模型 const model = getCurrentModel(); // 从模型选择器组件获取当前模型
const planMode = document.getElementById('planToggle')?.checked || false;
addMessage(text, 'user'); addMessage(text, 'user');
// 切换按钮为暂停状态 // 切换按钮为暂停状态
setSendButtonState(true); 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 = ''; messageInput.value = '';
autoResizeTextarea(); // 重置输入框高度 autoResizeTextarea(); // 重置输入框高度
messageInput.focus(); messageInput.focus();

View File

@ -611,6 +611,88 @@ export function getMessageAreaStyles(): string {
text-align: center; 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()} ${getWaveformPreviewContent()}
`; `;
} }
@ -1034,6 +1116,93 @@ export function getMessageAreaScript(): string {
container.scrollTop = container.scrollHeight; container.scrollTop = container.scrollHeight;
} }
}, 0); }, 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); currentSegmentedMessage.appendChild(segmentDiv);
@ -1203,6 +1372,47 @@ export function getMessageAreaScript(): string {
</div> </div>
</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); container.appendChild(segmentDiv);

View File

@ -1,19 +1,21 @@
/** /**
* Plan 开关组件 * Plan 开关组件
* 注意:功能已移至模式选择器,此组件仅保留样式(已禁用)
*/ */
/** /**
* 获取 Plan 开关的 HTML 内容 * 获取 Plan 开关的 HTML 内容
* 已禁用,仅保留样式展示
*/ */
export function getPlanToggleContent(): string { export function getPlanToggleContent(): string {
return ` return `
<div class="tooltip"> <div class="tooltip">
<label class="plan-toggle"> <label class="plan-toggle plan-toggle-disabled">
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()"> <input type="checkbox" id="planToggle" disabled>
<span class="plan-toggle-slider"></span> <span class="plan-toggle-slider"></span>
<span class="plan-toggle-label">Plan</span> <span class="plan-toggle-label">Plan</span>
</label> </label>
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span> <span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
</div> </div>
`; `;
} }
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
font-weight: 500; font-weight: 500;
color: var(--vscode-foreground); 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);
}
`; `;
} }

View File

@ -263,61 +263,63 @@ export function getWaveformPreviewScript(): string {
const timeRange = maxTime - minTime || 1; const timeRange = maxTime - minTime || 1;
// 绘制波形 // 绘制波形
let pathData = ''; if (signal.width === 1) {
let lastX = leftMargin; // 单比特信号 - 绘制数字波形
let lastValue = signal.values[0].value; let pathData = '';
const yHigh = y;
const yLow = y + signalHeight;
signal.values.forEach((point, i) => { signal.values.forEach((point, i) => {
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth; const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
const value = point.value; const currentY = (point.value === '1') ? yHigh : yLow;
if (signal.width === 1) {
// 单比特信号 - 绘制数字波形
const yHigh = y;
const yLow = y + signalHeight;
const currentY = (value === '1') ? yHigh : yLow;
if (i === 0) { if (i === 0) {
pathData = \`M \${x} \${currentY}\`; pathData = \`M \${x} \${currentY}\`;
} else { } else {
// 绘制垂直跳变 const prevValue = signal.values[i - 1].value;
const prevY = (lastValue === '1') ? yHigh : yLow; const prevY = (prevValue === '1') ? yHigh : yLow;
if (prevY !== currentY) { if (prevY !== currentY) {
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`; pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
} else { } else {
pathData += \` L \${x} \${currentY}\`; pathData += \` L \${x} \${currentY}\`;
} }
} }
});
lastValue = value; // 延伸到右边界
lastX = x; const lastValue = signal.values[signal.values.length - 1].value;
} else { const lastY = (lastValue === '1') ? yHigh : yLow;
// 多比特信号 - 绘制总线波形(梯形)
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);
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`; 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"/>\`;
}
}); });
// 绘制时间轴 // 绘制时间轴

View File

@ -255,7 +255,7 @@ export function getWebviewContent(iconUri?: string): string {
padding: 0; padding: 0;
} }
.message-segment { .message-segment {
padding: 10px 22 px; padding: 10px 22px;
} }
.segment-text { .segment-text {
line-height: 1.6; line-height: 1.6;
@ -417,6 +417,62 @@ export function getWebviewContent(iconUri?: string): string {
let loadingIndicator = null; let loadingIndicator = null;
let currentSegmentedMessage = 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) { function quickAction(type) {
const questions = { const questions = {
counter: '生成一个4位同步计数器', counter: '生成一个4位同步计数器',
@ -443,8 +499,6 @@ export function getWebviewContent(iconUri?: string): string {
} }
} }
messageInput.focus();
// 监听来自插件的消息 // 监听来自插件的消息
window.addEventListener('message', event => { window.addEventListener('message', event => {
const message = event.data; const message = event.data;
@ -516,6 +570,14 @@ export function getWebviewContent(iconUri?: string): string {
hideLoadingIndicator(); hideLoadingIndicator();
break; break;
case 'workspaceStatus':
// 更新工作区状态
if (typeof hasWorkspace !== 'undefined') {
hasWorkspace = message.hasWorkspace;
console.log('[WebView] 工作区状态:', hasWorkspace);
}
break;
case 'vcdInfo': case 'vcdInfo':
// 渲染迷你波形预览信息 // 渲染迷你波形预览信息
try { try {
@ -588,6 +650,27 @@ export function getWebviewContent(iconUri?: string): string {
} }
break; 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: default:
console.log('[WebView] 未处理的消息类型:', message.command); console.log('[WebView] 未处理的消息类型:', message.command);
} }