From 91fadf591f0dad1f858ebc2dc6fa1465dd7907b7 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:11 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=A8=A1=E5=BC=8F=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 RunMode 类型(plan/ask/agent/auto) - 添加 PlanConfirmEvent、ToolConfirmEvent 类型 - DialogRequest 使用 mode 字段替代 toolMode/planMode --- src/types/api.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/types/api.ts b/src/types/api.ts index 5b8c514..7f3d227 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -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; + /** 时间戳 */ + 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; +} + // ============== 辅助类型 ============== /** 后端工具名称 */ From 2aff54de74fcc3b3ddf0d660f9b1ba4b10e05c71 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:19 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E9=80=89=E6=8B=A9=E5=99=A8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agentModeSelector 添加下拉菜单和模式切换逻辑 - planToggle 适配新的模式系统 --- src/views/agentModeSelector.ts | 60 +++++++++++++++++++++++++++++----- src/views/planToggle.ts | 19 +++++++++-- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/views/agentModeSelector.ts b/src/views/agentModeSelector.ts index 04a3841..fbb298b 100644 --- a/src/views/agentModeSelector.ts +++ b/src/views/agentModeSelector.ts @@ -1,6 +1,12 @@ /** * 模式选择器组件 - * 提供 Agent/Ask/Auto 三种模式的选择功能 + * 提供 Plan/Ask/Agent/Auto 四种模式的选择功能 + * + * 模式说明: + * - Plan: 只读模式,只能查询分析,不能写文件 + * - Ask: 逐个确认,每个写操作需用户确认 + * - Agent: 智能体自主,自动执行大部分操作 + * - Auto: 完全自动,所有操作自动执行 */ /** @@ -17,12 +23,25 @@ export function getModeSelectorContent(): string {
-
Agent
-
Ask
-
Auto
+
+ Plan + 只读模式 +
+
+ Ask + 逐个确认 +
+
+ Agent + 智能体自主 +
+
+ Auto + 完全自动 +
- 切换模式 + 智能体自主模式 `; } @@ -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 => { diff --git a/src/views/planToggle.ts b/src/views/planToggle.ts index 68fb9d1..7f87b91 100644 --- a/src/views/planToggle.ts +++ b/src/views/planToggle.ts @@ -1,19 +1,21 @@ /** * Plan 开关组件 + * 注意:功能已移至模式选择器,此组件仅保留样式(已禁用) */ /** * 获取 Plan 开关的 HTML 内容 + * 已禁用,仅保留样式展示 */ export function getPlanToggleContent(): string { return `
-
`; } @@ -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); + } `; } From e77194628acf5904e565034081be31e63a4abf58 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:28 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=A8=A1=E5=BC=8F=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E5=92=8C=20API=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dialogService 接收并传递 mode 参数 - apiClient 构造带 mode 的请求 - messageHandler 从 WebView 消息获取 mode --- src/services/apiClient.ts | 14 +++- src/services/dialogService.ts | 134 ++++++++++++++++++++++++++++++++-- src/utils/messageHandler.ts | 131 +++++++++++++++++++++++++++++++-- 3 files changed, 265 insertions(+), 14 deletions(-) diff --git a/src/services/apiClient.ts b/src/services/apiClient.ts index 585f471..b8ab7b1 100644 --- a/src/services/apiClient.ts +++ b/src/services/apiClient.ts @@ -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 { + console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`); + return request('/api/tool/confirm', { + method: 'POST', + body: response + }); +} + /** * 健康检查 * GET /api/dialog/health diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index 736730d..ec4551a 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -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) => 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 { + 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 { 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; } diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index c9fd435..368abcf 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -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 { // 创建或复用会话 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 { + 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); + } +} + /** * 解析文件操作命令 */ From 42481cd314e9c759b382e517c9d994a98c7e4344 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:35 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20SSE=20=E4=BA=8B=E4=BB=B6=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=92=8C=E8=AE=A1=E5=88=92=E7=A1=AE=E8=AE=A4=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sseHandler 添加 onPlanConfirm、onToolConfirm 回调 - messageArea 添加计划确认对话框渲染 --- src/services/sseHandler.ts | 14 ++- src/views/messageArea.ts | 210 +++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) diff --git a/src/services/sseHandler.ts b/src/services/sseHandler.ts index b6aba4b..d150c1f 100644 --- a/src/services/sseHandler.ts +++ b/src/services/sseHandler.ts @@ -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; diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index 89f2d39..635f8d2 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -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) => + \`
\${i + 1}. \${step}
\` + ).join(''); + + // 选项按钮 + const options = ['确认执行', '修改计划', '取消']; + const optionsHtml = options.map(opt => { + const isSelected = isAnswered && opt === selectedAnswer; + return \`\`; + }).join(''); + + segmentDiv.innerHTML = \` +
+
+ 📋 + \${segment.planTitle || '执行计划'} +
+
+
\${segment.planSummary || ''}
+
\${stepsHtml}
+
+
+
\${optionsHtml}
+
+ + +
+
+
+ \`; + + // 只在未回答时添加事件监听 + 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 { \`; + } else if (segment.type === 'plan') { + // 计划卡片渲染 + segmentDiv.className += ' segment-plan'; + const stepsHtml = (segment.planSteps || []).map((step, i) => + \`
\${i + 1}. \${step}
\` + ).join(''); + + segmentDiv.innerHTML = \` +
+
+ 📋 + \${segment.planTitle || '执行计划'} +
+
+
\${segment.planSummary || ''}
+
\${stepsHtml}
+
+
+ + + +
+
+ \`; + + // 绑定按钮事件 + 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); From 023fdb66c3b53b9b82e3d528bb37618a3f246112 Mon Sep 17 00:00:00 2001 From: XiaoFeng <117837368+Fzhiyu1@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:42:44 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20WebView=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - webviewContent 集成模式选择器脚本和样式 - inputArea 适配模式传递 - ICViewProvider/ICHelperPanel 传递模式参数 --- src/config/settings.ts | 2 +- src/panels/ICHelperPanel.ts | 23 ++++++++++- src/views/ICViewProvider.ts | 2 +- src/views/inputArea.ts | 5 ++- src/views/webviewContent.ts | 79 ++++++++++++++++++++++++++++++++++++- 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/config/settings.ts b/src/config/settings.ts index e6c041f..621be84 100644 --- a/src/config/settings.ts +++ b/src/config/settings.ts @@ -17,7 +17,7 @@ export interface IccoderConfig { /** 默认配置 */ const DEFAULT_CONFIG: IccoderConfig = { backendUrl: "http://localhost:8080", - timeout: 60000, + timeout: 3600000, // 1小时 userId: "default-user", }; diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index f9d227e..f5e149f 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -9,6 +9,9 @@ import { handleReplaceInFile, handleUserAnswer, abortCurrentDialog, + handlePlanAction, + setPendingPlanExecution, + getCurrentTaskId, } from "../utils/messageHandler"; import { VCDViewerPanel } from "./VCDViewerPanel"; import { ChatHistoryManager } from "../utils/chatHistoryManager"; @@ -107,7 +110,7 @@ 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 +184,24 @@ 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; } }, undefined, diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index 43a537a..286ac2b 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -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); diff --git a/src/views/inputArea.ts b/src/views/inputArea.ts index a0ec703..d1596a4 100644 --- a/src/views/inputArea.ts +++ b/src/views/inputArea.ts @@ -261,7 +261,7 @@ export function getInputAreaStyles(): string { */ export function getInputAreaScript(): string { return ` - ${getModeSelectorScript()} + // 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载 ${getModelSelectorScript()} ${getContextButtonScript()} ${getContextCompressScript()} @@ -328,13 +328,14 @@ export function getInputAreaScript(): string { 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(); diff --git a/src/views/webviewContent.ts b/src/views/webviewContent.ts index c586f31..beb7c67 100644 --- a/src/views/webviewContent.ts +++ b/src/views/webviewContent.ts @@ -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位同步计数器', @@ -588,6 +644,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); }