46 Commits

Author SHA1 Message Date
acf3f9ff37 feat: 添加模型图标支持并更新相关组件以显示图标 2026-01-04 10:56:57 +08:00
c27b08cccf feat: 将当前环境修改为测试环境并调整模型选择器的选项顺序 2026-01-04 10:39:15 +08:00
9fc3c9f056 feat: 将当前环境从测试环境切换为生产环境 2025-12-31 19:10:00 +08:00
60d8eaf0eb feat: 修改当前环境为测试环境并调整后端服务地址注释格式 2025-12-31 19:08:37 +08:00
df6f983e83 Merge branch 'feat/back-to-front' into feat/plugin-front-end 2025-12-31 19:00:23 +08:00
acf60f2a17 feat: 添加消息区域操作按钮,包括复制、点赞和点踩功能 2025-12-31 18:51:17 +08:00
f933d84cd1 feat: 新增会话压缩命令和上下文显示功能
- ICHelperPanel: 新增 compressConversation 命令处理,支持手动触发会话压缩
- ICHelperPanel: 在加载历史会话时设置 lastTaskId,确保压缩操作可用
- webviewContent: 新增 contextUsage 消息处理,更新上下文使用量显示
- userInteraction: 将用户回答超时时间从 5 分钟延长至 2 小时
2025-12-31 18:50:27 +08:00
b794d1ceb0 feat: 实现上下文使用量监控和会话压缩功能
- sseHandler: 新增 onContextUsage 回调处理上下文使用量事件
- dialogService: 集成上下文使用量回调,追踪 AI 消息用于后端重启恢复
- apiClient: 新增 compactDialog API 支持手动压缩对话历史
- messageHandler: 新增 lastTaskId 管理机制,支持会话恢复后的压缩操作,转发上下文使用量到 WebView
2025-12-31 18:50:20 +08:00
259310a29d feat: 新增上下文使用量事件类型定义
- 新增 context_usage 事件类型
- 新增 ContextUsageEvent 接口,包含当前 token 数、最大 token 数和使用百分比
- 用于实时监控对话上下文的使用情况
2025-12-31 18:50:11 +08:00
715eac5949 feat: 新增多环境配置支持
- 新增 dev/test/prod 三种环境配置
- 支持通过 CURRENT_ENV 常量快速切换环境
- 重构配置获取逻辑,使用环境映射表
- 新增 getCurrentEnv() 方法获取当前环境
2025-12-31 18:50:05 +08:00
c2936395d9 refactor: 优化代码结构,简化导入语句并注释掉快速操作部分 2025-12-31 18:16:04 +08:00
8762eacb3e feat: 增强输入框状态管理,添加禁用状态和恢复输入状态的逻辑 2025-12-31 18:13:21 +08:00
3d535fd3e1 fix: 优化后端服务不可用时的错误处理,移除本地模拟回复逻辑 2025-12-31 18:02:38 +08:00
ecdbe0bdc0 feat: 更新输入框占位符提示,增加使用说明
- 按 Enter 发送,Shift + Enter 换行
2025-12-31 16:42:23 +08:00
c49aaf753c Merge remote-tracking branch 'origin/feat/plugin-front-end' into feat/back-to-front 2025-12-31 11:58:32 +08:00
0f8674e1c7 fix: 修复对话停止和会话记忆保存问题
- apiClient 添加 stopDialog 接口
- dialogService 添加 getSegments/getAccumulatedText 方法
- dialogService.abort 调用后端停止接口
- messageHandler.abortCurrentDialog 保存中止前的对话内容
- userInteraction 添加 getWebviewPanel 方法
- webviewContent 添加 resetSegmentedMessage 命令处理
- 修复停止后新消息覆盖旧消息的问题
2025-12-31 11:55:31 +08:00
ef2159f1bd refactor: 移除 VSCode 设置中的配置项,使用预配置的默认值 2025-12-31 10:42:53 +08:00
b662d25c9c refactor: 将 Ask 模式的工具确认从弹窗改为内嵌聊天卡片
## 主要修改

   ### dialogService.ts
   - 移除 `vscode.window.showWarningMessage` 弹窗
   - 将工具确认改为添加 question 类型的 segment
   - 使用 `userInteractionManager.handleAskUser` 等待用户回答
   - 生成唯一的 askId: `tool_confirm_{confirmId}`

   ### userInteraction.ts
   - 导入 `submitToolConfirm` 方法
   - 在 `submitUserAnswer` 中识别工具确认类型的 askId
   - 根据用户选择("确认执行" / "取消")调用对应的 API

   ## 用户体验改进
   - 工具确认问题自然融入对话流程
   - 用户可以看到历史确认记录
   - 非阻塞式交互,体验更流畅
2025-12-31 10:18:35 +08:00
1ce1ed715c refactor: 移除模式选择器中的 Auto 模式选项 2025-12-31 10:01:41 +08:00
2587018405 fix: 修复工具下拉框展开/折叠功能
- 修复工具下拉框无法打开的问题,添加正确的图标元素结构
   - 实现状态持久化机制,解决新内容产出时下拉框自动关闭的问题
   - 优化图标显示逻辑,使用 CSS transform 实现平滑旋转动画
   - 折叠状态图标向左旋转 90 度,展开状态图标向下
2025-12-31 09:57:48 +08:00
28b75e8475 Merge remote-tracking branch 'origin/feat/plugin-front-end' into feat/back-to-front 2025-12-31 09:44:03 +08:00
5287d483d8 feat: 为所有工具添加图标显示
- 新增多个工具图标定义(文件读取、删除、仿真、波形分析等)
   - 添加 getToolIcon 函数统一管理工具图标映射
   - 优化工具显示逻辑,所有工具现在都显示对应图标
   - 新增 addStateTransition 工具的图标和名称映射
   - 添加所有新图标的 CSS 样式
2025-12-31 09:43:30 +08:00
16e91bd2c0 feat: 实现会话记忆同步和知识图谱恢复机制
- 添加 memory_compacted SSE 事件处理
- 添加 CompactedMemory/CompactedMessage 类型定义
- 添加 COMPACTION_SUMMARY 消息类型
- 实现压缩数据存储到 conversation.json
- 实现从 conversation.json 构建恢复数据
- 发送请求时附带 knowledgeData 用于恢复知识图谱
2025-12-31 09:35:20 +08:00
e6b6bc3698 feat: 抽取 plan-card 为独立组件并优化样式
主要改动:
1. 创建独立的 planCard.ts 组件
   - 导出 getPlanCardStyles() 和 getPlanCardScript()
   - 包含 renderPlanCardInSegment 和 renderPlanCardStatic 两个渲染函数

2. 优化 messageArea.ts
   - 移除原有的 plan-card 内联样式和脚本(约 80 行)
   - 引入 planCard 组件并调用其函数
   - 添加 addRule 和 updateNode 工具映射

3. 优化计划卡片样式
   - 调整外边距:segment-plan 从 8px 增加到 12px
   - 优化内边距:header 12px 16px,body 16px,actions 14px 16px
   - 改进按钮布局:使用垂直布局,按钮和输入框分行显示
   - 增加元素间距:步骤间距 6px,按钮间距 10px
   - 添加行高 1.5 提升可读性

4. 添加 plannerIconSvg 图标
2025-12-31 09:25:08 +08:00
d43cd610a0 feat: 优化智能体卡片和工具显示
- 添加智能体卡片智能滚动功能:自动滚动到底部,用户向上滚动时停止,滚动到底部恢复
   - 过滤 spawnExplorer 工具,不在界面显示
   - 添加所有工具的中文名称映射(file_read、file_write、queryRules、setModule 等)
   - 优化代码结构,移除未使用的导入
2025-12-30 22:51:15 +08:00
842e5fb49b feat: 移除计划切换相关内容和样式 2025-12-30 20:48:32 +08:00
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
d415d8ee4e feat: 更新后端服务地址为192.168.1.108 2025-12-30 15:02:28 +08:00
bd7a85b705 Merge remote-tracking branch 'origin/feat/Plugin-front-end' into feat/back-to-front 2025-12-30 09:42:23 +08:00
44bbcde5cf feat: 知识图谱工具支持 + 智能体事件处理
- dialogService: 添加智能体 SSE 事件处理
- toolExecutor: 添加 knowledge_save/knowledge_load 工具
- messageArea: 添加智能体消息渲染支持
- 添加 CLAUDE.md 项目配置
2025-12-30 09:40:04 +08:00
d4c726ea9c feat: 添加智能体卡片组件
- 新建 agentCard.ts 智能体卡片UI组件
- webviewContent.ts 集成样式和脚本
2025-12-29 09:22:34 +08:00
082ef923b2 feat: 添加智能体事件类型和SSE处理
- api.ts 添加4个智能体事件类型定义
- sseHandler.ts 添加智能体事件回调和分发
2025-12-29 09:22:26 +08:00
5cb68652f9 fix: file_list 工具现在同时返回文件和目录 2025-12-26 15:40:40 +08:00
9bfa774336 feat: 添加 knowledge_save/load 工具
- api.ts: 添加 knowledge_save/load 类型定义
- toolExecutor.ts: 实现知识图谱保存和加载功能
2025-12-26 11:41:38 +08:00
009da59d38 Merge remote-tracking branch 'origin/feat/Plugin-front-end' into feat/back-to-front 2025-12-25 16:18:05 +08:00
5ea5ddba6e feat: 添加 file_delete 工具支持 2025-12-25 16:17:31 +08:00
39 changed files with 2933 additions and 392 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/IC-Coder-Plugin.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

16
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,16 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
</JSCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
</JavaCodeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" filepath="$PROJECT_DIR$/.idea/IC-Coder-Plugin.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

90
CLAUDE.md Normal file
View File

@ -0,0 +1,90 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
IC Coder Plugin 是一个 VS Code 扩展,为 Verilog/FPGA 开发提供智能辅助功能包括代码生成、文件操作、iverilog 仿真和 VCD 波形查看。
## Build Commands
```bash
# 安装依赖
pnpm install
# 编译 (开发模式)
pnpm run compile
# 监听模式编译
pnpm run watch
# 生产环境打包
pnpm run package
# 代码检查
pnpm run lint
# 运行测试
pnpm run test
# 编译测试文件
pnpm run compile-tests
```
## Development
- 按 F5 在 VS Code 中启动调试模式
- 使用 webpack 打包,入口文件为 `src/extension.ts`
- 输出目录为 `dist/`
## Architecture
```
src/
├── extension.ts # 插件入口,注册命令和视图
├── panels/
│ ├── ICHelperPanel.ts # 主聊天面板 (WebviewPanel)
│ └── VCDViewerPanel.ts # VCD 波形查看器面板
├── views/
│ ├── ICViewProvider.ts # 侧边栏视图提供者
│ └── webviewContent.ts # Webview HTML 内容 (大文件,使用搜索)
├── utils/
│ ├── messageHandler.ts # 消息处理核心逻辑 (大文件,使用搜索)
│ ├── iverilogRunner.ts # iverilog 编译和仿真执行
│ ├── chatHistoryManager.ts # 会话历史管理
│ ├── createFiles.ts # 文件创建工具
│ └── readFiles.ts # 文件读取工具
├── types/
│ └── chatHistory.ts # 消息类型定义 (LangChain4j 格式)
└── test/ # 测试文件
```
### Key Components
**消息流程**: Webview -> `onDidReceiveMessage` -> `messageHandler.ts` -> 后端处理 -> `panel.webview.postMessage` -> Webview
**消息类型** (`src/types/chatHistory.ts`):
- `MessageType.SYSTEM` / `USER` / `AI` / `TOOL_EXECUTION_RESULT`
- 兼容 LangChain4j 格式
**iverilog 集成** (`tools/iverilog/`):
- 内置 Windows x64 版本的 iverilog/vvp
- 通过 `IVERILOG_ROOT` 环境变量配置库路径
- 支持命令: "生成 VCD"、"运行仿真"、"生成波形"
## VS Code Extension Points
- 命令: `ic-coder.openPanel`, `ic-coder.openChat`, `ic-coder.openVCDViewer`
- 侧边栏视图: `ic-coder.mainView`
- 激活事件: `onLanguage:verilog`, `onLanguage:vhdl`, `onStartupFinished`
## Dependencies
- `vcdrom`, `vcd-stream`, `waveql` - VCD 波形处理
- `@wavedrom/doppler`, `onml` - 波形渲染
- `iconv-lite` - 编码转换
## Notes
- `webviewContent.ts``messageHandler.ts` 文件较大,建议使用搜索而非完整读取
- 当前仅支持 Windows 平台的 iverilog其他平台需用户自行安装

View File

@ -9,7 +9,8 @@
}, },
"icon": "media/icon.png", "icon": "media/icon.png",
"categories": [ "categories": [
"Other" "Chat",
"Programming Languages"
], ],
"keywords": [ "keywords": [
"IC", "IC",
@ -69,27 +70,7 @@
"id": "iccoder", "id": "iccoder",
"label": "IC Coder" "label": "IC Coder"
} }
], ]
"configuration": {
"title": "IC Coder",
"properties": {
"icCoder.backendUrl": {
"type": "string",
"default": "http://192.168.1.108:2233",
"description": "后端服务地址"
},
"icCoder.timeout": {
"type": "number",
"default": 60000,
"description": "请求超时时间(毫秒)"
},
"icCoder.userId": {
"type": "string",
"default": "default-user",
"description": "用户ID临时配置"
}
}
}
}, },
"scripts": { "scripts": {
"vscode:prepublish": "pnpm run package", "vscode:prepublish": "pnpm run package",

BIN
src/assets/model/Auto.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/model/Max.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/model/Sy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/model/lite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -1,9 +1,15 @@
/** /**
* 配置管理 * 配置管理
* 从 VSCode 设置读取配置项 * 支持 dev本地开发和 test测试服务器两种环境
*/ */
import * as vscode from "vscode"; import * as vscode from "vscode";
/** 环境类型 */
type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "test";
/** 配置项接口 */ /** 配置项接口 */
export interface IccoderConfig { export interface IccoderConfig {
/** 后端服务地址 */ /** 后端服务地址 */
@ -14,24 +20,40 @@ export interface IccoderConfig {
userId: string; userId: string;
} }
/** 默认配置 */ /** 环境配置 */
const DEFAULT_CONFIG: IccoderConfig = { const ENV_CONFIG: Record<Environment, IccoderConfig> = {
backendUrl: "http://localhost:8080", /** 本地开发环境 */
dev: {
backendUrl: "http://localhost:2233",
timeout: 60000, timeout: 60000,
userId: "default-user", userId: "default-user",
},
/** 测试服务器环境 */
test: {
backendUrl: "http://192.168.1.108:2233",
timeout: 60000,
userId: "default-user",
},
/** 生产环境 */
prod: {
backendUrl: "https://api.iccoder.com", // TODO: 替换为实际生产地址
timeout: 60000,
userId: "default-user",
},
}; };
/**
* 获取当前环境
*/
export function getCurrentEnv(): Environment {
return CURRENT_ENV;
}
/** /**
* 获取配置项 * 获取配置项
*/ */
export function getConfig(): IccoderConfig { export function getConfig(): IccoderConfig {
const config = vscode.workspace.getConfiguration("icCoder"); return { ...ENV_CONFIG[CURRENT_ENV] };
return {
backendUrl: config.get<string>("backendUrl", DEFAULT_CONFIG.backendUrl),
timeout: config.get<number>("timeout", DEFAULT_CONFIG.timeout),
userId: config.get<string>("userId", DEFAULT_CONFIG.userId),
};
} }
/** /**
@ -39,7 +61,6 @@ export function getConfig(): IccoderConfig {
*/ */
export function getApiUrl(path: string): string { export function getApiUrl(path: string): string {
const { backendUrl } = getConfig(); const { backendUrl } = getConfig();
// 确保 URL 格式正确
const baseUrl = backendUrl.endsWith("/") const baseUrl = backendUrl.endsWith("/")
? backendUrl.slice(0, -1) ? backendUrl.slice(0, -1)
: backendUrl; : backendUrl;

View File

@ -70,3 +70,98 @@ export const stopIconSvg = `
<path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path> <path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path>
</svg> </svg>
`; `;
/**
* 探索智能体图标 SVG
*/
export const agentIconSvg = `
<svg t="1767101071638" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7779" width="16" height="16" style="display: inline-block; vertical-align: middle;"><path d="M173.474909 410.414545c-20.293818 0-33.838545-13.498182-33.838545-33.792v-135.377454C139.636364 187.066182 187.019636 139.636364 241.198545 139.636364h135.447273c20.270545 0 33.815273 13.591273 33.815273 33.885091s-13.591273 33.838545-33.885091 33.838545h-135.447273c-20.317091 0-33.815273 13.591273-33.815272 33.885091v135.377454c0.046545 20.293818-13.498182 33.792-33.838546 33.792z m203.101091 473.902546h-135.447273C187.042909 884.317091 139.636364 836.933818 139.636364 782.754909V647.447273c0-20.386909 13.544727-33.838545 33.838545-33.838546s33.885091 13.451636 33.885091 33.838546v135.330909c0 20.293818 13.544727 33.931636 33.838545 33.931636h135.447273c20.270545 0 33.815273 13.451636 33.815273 33.745455-0.046545 20.340364-13.591273 33.885091-33.885091 33.885091z m406.178909 0H647.447273c-20.386909 0-33.931636-13.544727-33.931637-33.931636 0-20.293818 13.544727-33.745455 33.931637-33.745455h135.330909c20.386909 0 33.838545-13.637818 33.838545-33.931636V647.447273c0-20.386909 13.544727-33.838545 33.931637-33.838546 20.293818 0 33.838545 13.451636 33.838545 33.838546v135.330909c-0.046545 54.178909-47.522909 101.562182-101.608727 101.562182z m67.723636-473.902546c-20.386909 0-33.931636-13.498182-33.931636-33.792v-135.377454c0-20.340364-13.451636-33.885091-33.838545-33.885091H647.447273c-20.386909 0-33.931636-13.498182-33.931637-33.838545S627.083636 139.636364 647.424 139.636364h135.330909c54.085818 0 101.562182 47.429818 101.562182 101.608727v135.377454c0 20.293818-13.544727 33.792-33.838546 33.792z m0 135.493819H173.474909c-20.293818 0-33.838545-13.591273-33.838545-33.931637s13.544727-33.885091 33.838545-33.885091h677.003636c20.293818 0 33.838545 13.591273 33.838546 33.885091s-13.544727 33.931636-33.838546 33.931637z" fill="#8a8a8a" p-id="7780"></path></svg>`;
/**
* planner 图标 SVG
*/
export const plannerIconSvg = `<svg t="1767143425474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10954" width="16" height="16"><path d="M860.544 633.856c-82.368 0-152.128 69.632-158.464 152h-354.88c-31.616 0-63.296-31.68-63.296-63.296V437.376c12.608 0 25.344 6.4 44.288 6.4h380.16c12.672 69.696 76.032 126.656 152.128 126.656 88.704 0 158.336-69.696 158.336-158.4s-69.632-158.4-158.336-158.4c-76.096 0-139.456 57.024-152.128 126.656h-361.216c-31.616 0-63.296-31.68-63.296-63.296v-133.12h164.736c31.68 0 63.296-22.848 63.296-54.528a55.04 55.04 0 0 0-56-56h-380.16c-31.68 0-70.72 17.984-70.72 56s31.68 54.528 63.36 54.528h133.056v538.624c0 69.696 57.088 126.656 126.72 126.656h386.56c25.344 57.088 82.368 101.376 145.728 101.376a156.8 156.8 0 0 0 158.336-158.4 156.608 156.608 0 0 0-158.208-158.272z m0-316.8c50.624 0 94.912 44.288 94.912 94.976s-44.288 94.976-94.912 94.976c-50.752 0-95.104-44.288-95.104-94.976s44.352-94.976 95.104-94.976z m0 570.24c-50.752 0-95.104-44.352-95.104-95.04s44.352-95.04 95.104-95.04c50.624 0 94.912 44.352 94.912 95.04s-44.288 95.04-94.912 95.04z" p-id="10955" fill="#8a8a8a"></path></svg>`;
/**
* 保存知识库图标 SVG
*/
export const saveKnowledgeIconSvg = `
<span class="tool-save-knowledge-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M827.733333 315.733333l-123.733333-123.733333c-12.8-12.8-34.133333-21.333333-59.733333-21.333334H256c-46.96 0-85.44 40.96-85.44 85.44v512c0 46.96 40.96 85.44 85.44 85.44h512c46.96 0 85.44-40.96 85.44-85.44V375.466667c0-25.6-8.533333-46.933333-21.333333-59.733334z m-140.8 469.333334H337.066667v-85.333334h349.866666v85.333334z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z m0-170.666667H337.066667v-85.333333h349.866666v85.333333z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 文件读取图标 SVG
*/
export const fileReadIconSvg = `
<span class="tool-file-read-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326z m1.8 562H232V136h302v216c0 23.2 18.8 42 42 42h216v494z" fill="#8a8a8a"/>
<path d="M342 472h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 616h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM342 760h340c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H342c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 文件删除图标 SVG
*/
export const fileDeleteIconSvg = `
<span class="tool-file-delete-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M360 184h-8c4.4 0 8-3.6 8-8v8h304v-8c0 4.4 3.6 8 8 8h-8v72h72v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80h72v-72z" fill="#8a8a8a"/>
<path d="M832 256H192c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM731.3 840H292.7l-24.2-512h487l-24.2 512z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 仿真图标 SVG
*/
export const simulationIconSvg = `
<span class="tool-simulation-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32z m-40 632H136V232h752v560z" fill="#8a8a8a"/>
<path d="M210 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM210 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H210c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM462 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM462 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H462c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8zM714 304h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V312c0-4.4 3.6-8 8-8zM714 544h100c4.4 0 8 3.6 8 8v152c0 4.4-3.6 8-8 8H714c-4.4 0-8-3.6-8-8V552c0-4.4 3.6-8 8-8z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 波形分析图标 SVG
*/
export const waveformIconSvg = `
<span class="tool-waveform-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M952 474H829.8C812.5 327.6 696.4 211.5 550 194.2V72c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v122.2C327.6 211.5 211.5 327.6 194.2 474H72c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h122.2C211.5 696.4 327.6 812.5 474 829.8V952c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V829.8C696.4 812.5 812.5 696.4 829.8 550H952c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8zM512 756c-134.8 0-244-109.2-244-244s109.2-244 244-244 244 109.2 244 244-109.2 244-244 244z" fill="#8a8a8a"/>
<path d="M512 392c-32.1 0-62.1 12.4-84.8 35.2-22.7 22.7-35.2 52.7-35.2 84.8s12.5 62.1 35.2 84.8c22.7 22.7 52.7 35.2 84.8 35.2s62.1-12.5 84.8-35.2c22.7-22.7 35.2-52.7 35.2-84.8s-12.5-62.1-35.2-84.8C574.1 404.4 544.1 392 512 392z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 知识库加载图标 SVG
*/
export const knowledgeLoadIconSvg = `
<span class="tool-knowledge-load-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" fill="#8a8a8a"/>
<path d="M492 400h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 544h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM492 688h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H492c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8zM340 368a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 512a40 40 0 1 0 80 0 40 40 0 1 0-80 0zM340 656a40 40 0 1 0 80 0 40 40 0 1 0-80 0z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 状态转换图标 SVG
*/
export const stateTransitionIconSvg = `
<span class="tool-state-transition-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#8a8a8a"/>
<path d="M686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.9-11.2z" fill="#8a8a8a"/>
<path d="M512 320c-4.4 0-8 3.6-8 8v184c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H560V328c0-4.4-3.6-8-8-8h-40z" fill="#8a8a8a"/>
</svg>
</span>
`;

View File

@ -9,7 +9,12 @@ import {
handleReplaceInFile, handleReplaceInFile,
handleUserAnswer, handleUserAnswer,
abortCurrentDialog, abortCurrentDialog,
handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId,
setLastTaskId,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel"; import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager"; import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory"; import { MessageType } from "../types/chatHistory";
@ -23,9 +28,13 @@ 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 === "立即登录") { if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login");
} }
@ -33,7 +42,9 @@ export async function showICHelperPanel(
return; return;
} }
} catch (error) { } catch (error) {
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => { vscode.window
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => {
if (selection === "立即登录") { if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login"); vscode.commands.executeCommand("ic-coder.login");
} }
@ -49,7 +60,10 @@ export async function showICHelperPanel(
{ {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")], localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
],
} }
); );
@ -71,8 +85,28 @@ export async function showICHelperPanel(
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png") vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
); );
// 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
);
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
);
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
);
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
);
// 设置HTML内容 // 设置HTML内容
panel.webview.html = getWebviewContent(iconUri.toString()); panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息 // 处理消息
panel.webview.onDidReceiveMessage( panel.webview.onDidReceiveMessage(
@ -107,7 +141,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);
@ -179,7 +218,90 @@ export async function showICHelperPanel(
break; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": case "abortDialog":
abortCurrentDialog(); void abortCurrentDialog();
break;
// 新增:压缩会话
case "compressConversation":
{
const taskId = getCurrentTaskId();
if (taskId) {
compactDialog(taskId)
.then((result) => {
if (result.success) {
panel.webview.postMessage({
command: "receiveMessage",
text: "✅ 会话压缩完成",
});
} else {
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 压缩失败: ${result.error || "未知错误"}`,
});
}
})
.catch((err) => {
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 压缩失败: ${err.message || "网络错误"}`,
});
});
} else {
panel.webview.postMessage({
command: "receiveMessage",
text: "❌ 没有活跃的会话",
});
}
}
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; break;
} }
}, },
@ -464,6 +586,9 @@ async function selectConversation(
return; return;
} }
// 设置 lastTaskId用于压缩等操作
setLastTaskId(taskId);
// 更新面板的任务映射,确保后续对话保存到正确的任务中 // 更新面板的任务映射,确保后续对话保存到正确的任务中
const panelId = (panel as any).__uniqueId; const panelId = (panel as any).__uniqueId;
historyManager.setPanelTask(panelId, taskId, workspacePath); historyManager.setPanelTask(panelId, taskId, workspacePath);

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
@ -114,6 +126,55 @@ export async function healthCheck(): Promise<{ status: string }> {
}); });
} }
/**
* 停止对话请求
*/
export interface StopDialogRequest {
taskId: string;
}
/**
* 停止对话响应
*/
export interface StopDialogResponse {
success: boolean;
taskId: string;
message?: string;
error?: string;
}
/**
* 停止对话
* POST /api/dialog/stop
*/
export async function stopDialog(taskId: string): Promise<StopDialogResponse> {
console.log(`[API] 停止对话: taskId=${taskId}`);
return request<StopDialogResponse>('/api/dialog/stop', {
method: 'POST',
body: { taskId }
});
}
/** 压缩对话响应 */
export interface CompactDialogResponse {
success: boolean;
taskId: string;
message?: string;
error?: string;
}
/**
* 手动压缩对话历史
* POST /api/dialog/compact
*/
export async function compactDialog(taskId: string): Promise<CompactDialogResponse> {
console.log(`[API] 压缩对话: taskId=${taskId}`);
return request<CompactDialogResponse>('/api/dialog/compact', {
method: 'POST',
body: { taskId }
});
}
/** /**
* 创建成功的工具结果 * 创建成功的工具结果
*/ */

View File

@ -3,17 +3,21 @@
* 整合 SSE 通信、工具执行、用户交互 * 整合 SSE 通信、工具执行、用户交互
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler'; import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
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, stopDialog } from './apiClient';
import { ChatHistoryManager } from '../utils/chatHistoryManager';
/** /**
* 消息段落类型 * 消息段落类型
*/ */
export interface MessageSegment { export interface MessageSegment {
type: 'text' | 'tool' | 'question'; type: 'text' | 'tool' | 'question' | 'agent' | 'plan';
content?: string; content?: string;
toolName?: string; toolName?: string;
toolStatus?: 'running' | 'success' | 'error'; toolStatus?: 'running' | 'success' | 'error';
@ -21,6 +25,26 @@ export interface MessageSegment {
askId?: string; askId?: string;
question?: string; question?: string;
options?: string[]; options?: string[];
// 智能体相关字段
agentId?: string;
agentName?: string;
agentStatus?: 'running' | 'completed' | 'error';
agentSteps?: AgentStep[];
// 计划相关字段
planTitle?: string;
planSteps?: string[];
planSummary?: string;
}
/**
* 智能体执行步骤
*/
export interface AgentStep {
step: number;
toolName: string;
toolInput?: unknown;
toolResult?: string;
status: 'running' | 'completed' | 'error';
} }
/** /**
@ -35,6 +59,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;
/** 实时更新段落(流式过程中) */ /** 实时更新段落(流式过程中) */
@ -45,6 +73,8 @@ export interface DialogCallbacks {
onError?: (message: string) => void; onError?: (message: string) => void;
/** 通知消息 */ /** 通知消息 */
onNotification?: (message: string) => void; onNotification?: (message: string) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
} }
/** /**
@ -59,8 +89,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);
} }
@ -126,12 +157,166 @@ export class DialogSession {
return this.isActive; return this.isActive;
} }
/**
* 加载知识图谱数据
* 从 .iccoder/knowledge.json 读取
*/
private async loadKnowledgeData(): Promise<string | null> {
console.log('[DialogSession] loadKnowledgeData 开始执行');
// 等待 workspaceFolders 就绪(首次打开窗口/首次触发命令时可能为空)
const workspaceFolders = await this.waitForWorkspaceFolders();
if (!workspaceFolders || workspaceFolders.length === 0) {
console.log('[DialogSession] 没有工作区文件夹');
return null;
}
// 多根工作区场景:优先读取实际存在 knowledge.json 的根目录
for (const folder of this.getWorkspaceFolderCandidates(workspaceFolders)) {
const knowledgeUri = vscode.Uri.joinPath(folder.uri, '.iccoder', 'knowledge.json');
console.log('[DialogSession] 知识图谱 URI:', knowledgeUri.toString());
try {
const content = await this.readTextFileWithRetry(knowledgeUri, 5);
if (!content) {
continue;
}
// 基础校验 + 清洗:避免偶发读取到半截内容导致后端反序列化失败
try {
const parsed = JSON.parse(content) as any;
// 兼容:后端 KnowledgeGraph.isEmpty() 可能被序列化为 "empty",老后端反序列化会失败
if (parsed && typeof parsed === 'object' && 'empty' in parsed) {
delete parsed.empty;
}
const sanitized = JSON.stringify(parsed);
console.log('[DialogSession] 知识图谱已清洗, sanitizedLen:', sanitized.length);
return sanitized;
} catch (e) {
console.warn('[DialogSession] 知识图谱 JSON 解析失败,跳过本次读取:', e);
continue;
}
} catch (error) {
console.warn('[DialogSession] 加载知识图谱失败:', error);
}
}
return null;
}
private async waitForWorkspaceFolders(): Promise<readonly vscode.WorkspaceFolder[] | undefined> {
for (let i = 0; i < 10; i++) {
const folders = vscode.workspace.workspaceFolders;
if (folders && folders.length > 0) {
return folders;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
return vscode.workspace.workspaceFolders;
}
private getWorkspaceFolderCandidates(
workspaceFolders: readonly vscode.WorkspaceFolder[]
): vscode.WorkspaceFolder[] {
const result: vscode.WorkspaceFolder[] = [];
// 1) 当前激活文件所在的 workspace folder如果有
const activeUri = vscode.window.activeTextEditor?.document?.uri;
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
if (activeFolder) {
result.push(activeFolder);
}
// 2) 其它 workspace folders去重
for (const folder of workspaceFolders) {
if (!result.some(f => f.uri.toString() === folder.uri.toString())) {
result.push(folder);
}
}
return result;
}
private async readTextFileWithRetry(uri: vscode.Uri, maxAttempts: number): Promise<string | null> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const bytes = await vscode.workspace.fs.readFile(uri);
const text = Buffer.from(bytes).toString('utf-8');
if (!text || !text.trim()) {
return null;
}
return text;
} catch (error) {
// 文件不存在:不是错误,直接返回 null
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
return null;
}
const retryable =
(error instanceof vscode.FileSystemError && error.code === 'Unavailable') ||
(typeof (error as any)?.code === 'string' && ['EBUSY', 'EPERM', 'EACCES'].includes((error as any).code));
if (!retryable || attempt >= maxAttempts) {
throw error;
}
const delayMs = 50 * attempt;
console.log(`[DialogSession] 读取知识图谱失败(可重试): attempt=${attempt}/${maxAttempts}, delay=${delayMs}ms`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
return null;
}
/**
* 获取工具操作描述(用于确认对话框)
*/
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?.('当前有对话正在进行中');
@ -144,13 +329,29 @@ export class DialogSession {
this.currentTextSegment = null; this.currentTextSegment = null;
const config = getConfig(); const config = getConfig();
// 获取压缩数据和新消息(用于后端重启后恢复)
const historyManager = ChatHistoryManager.getInstance();
const compactedData = await historyManager.loadCompactedData(this.taskId);
const newMessages = historyManager.getNewMessagesSinceCompaction();
// 加载知识图谱数据
const knowledgeData = await this.loadKnowledgeData();
console.log('[DialogSession] knowledgeData 加载结果:', knowledgeData ? `${knowledgeData.length} 字符` : 'null');
const request: DialogRequest = { const request: DialogRequest = {
taskId: this.taskId, taskId: this.taskId,
message, message,
userId: config.userId, userId: config.userId,
toolMode: 'AGENT' mode: mode || 'agent',
compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined
}; };
// 追踪用户消息
historyManager.trackUserMessage(message);
const sseCallbacks: SSECallbacks = { const sseCallbacks: SSECallbacks = {
onTextDelta: (data) => { onTextDelta: (data) => {
this.accumulatedText += data.text; this.accumulatedText += data.text;
@ -164,6 +365,15 @@ export class DialogSession {
onToolCall: async (data: ToolCallRequest) => { onToolCall: async (data: ToolCallRequest) => {
const toolName = data.params.name; const toolName = data.params.name;
console.log('[DialogSession] onToolCall:', toolName); console.log('[DialogSession] onToolCall:', toolName);
// 检查是否有活跃的智能体(如果有,工具执行会显示在智能体卡片内,不需要单独显示)
const hasActiveAgent = this.segments.some(
s => s.type === 'agent' && s.agentStatus === 'running'
);
if (hasActiveAgent) {
console.log('[DialogSession] onToolCall: 智能体执行中,跳过工具段落:', toolName);
} else {
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加) // 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop(); const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') { if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
@ -173,20 +383,25 @@ export class DialogSession {
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
} }
}
// 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复 // 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复
try { try {
await executeToolCall(data, this.toolContext); await executeToolCall(data, this.toolContext);
if (!hasActiveAgent) {
this.updateToolSegment(toolName, 'success', '执行完成'); this.updateToolSegment(toolName, 'success', '执行完成');
// 实时发送段落更新 // 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
}
// 也不调用 callbacks.onToolComplete避免重复 // 也不调用 callbacks.onToolComplete避免重复
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误'; const errorMsg = error instanceof Error ? error.message : '未知错误';
if (!hasActiveAgent) {
this.updateToolSegment(toolName, 'error', errorMsg); this.updateToolSegment(toolName, 'error', errorMsg);
callbacks.onToolError?.(toolName, errorMsg);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
} }
callbacks.onToolError?.(toolName, errorMsg);
}
}, },
onToolStart: (data) => { onToolStart: (data) => {
@ -218,6 +433,106 @@ export class DialogSession {
callbacks.onSegmentUpdate?.(this.segments); callbacks.onSegmentUpdate?.(this.segments);
}, },
onToolConfirm: async (data: ToolConfirmEvent) => {
console.log('[DialogSession] onToolConfirm:', data.toolName, data.confirmId);
// 结束当前文本段落
this.finalizeTextSegment();
// 生成工具描述
const toolDescription = this.getToolDescription(data.toolName, data.toolInput);
// 构建问题文本
const toolNameMap: Record<string, string> = {
'file_write': '写入文件',
'file_delete': '删除文件',
'syntax_check': '语法检查',
'simulation': '运行仿真'
};
const toolDisplayName = toolNameMap[data.toolName] || data.toolName;
const question = `确认执行操作:${toolDisplayName}\n\n${toolDescription}`;
// 生成唯一的 askId
const askId = `tool_confirm_${data.confirmId}`;
// 添加问题段落到聊天界面
this.segments.push({
type: 'question',
askId: askId,
question: question,
options: ['确认执行', '取消']
});
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
// 调用回调通知 UI
callbacks.onToolConfirm?.(data.confirmId, data.toolName, data.toolInput);
// 使用 userInteractionManager 等待用户回答
try {
await userInteractionManager.handleAskUser(
{
askId: askId,
question: question,
options: ['确认执行', '取消']
} as AskUserEvent,
this.taskId
);
// 注意:用户回答后,需要在 receiveAnswer 中处理 tool_confirm 类型的 askId
// 这里不直接调用 submitToolConfirm而是在 userInteractionManager 中统一处理
} catch (error) {
console.error('[DialogSession] 处理工具确认失败:', error);
// 如果出错,默认取消执行
try {
await submitToolConfirm({
confirmId: data.confirmId,
taskId: this.taskId,
approved: false
});
} catch (submitError) {
console.error('[DialogSession] 发送取消响应失败:', submitError);
}
}
},
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({
@ -240,6 +555,12 @@ export class DialogSession {
onComplete: (data) => { onComplete: (data) => {
this.isActive = false; this.isActive = false;
this.finalizeTextSegment(); this.finalizeTextSegment();
// 追踪 AI 消息(用于后端重启后恢复)
if (this.accumulatedText) {
historyManager.trackAiMessage(this.accumulatedText);
}
// 发送所有段落 // 发送所有段落
callbacks.onComplete?.(this.segments); callbacks.onComplete?.(this.segments);
}, },
@ -257,6 +578,80 @@ export class DialogSession {
callbacks.onNotification?.(data.message); callbacks.onNotification?.(data.message);
}, },
// 智能体事件处理
onAgentStart: (data) => {
console.log('[DialogSession] onAgentStart:', data.agentId);
this.finalizeTextSegment();
this.segments.push({
type: 'agent',
agentId: data.agentId,
agentName: data.agentName,
content: data.instruction,
agentStatus: 'running',
agentSteps: []
});
callbacks.onSegmentUpdate?.(this.segments);
},
onAgentProgress: (data) => {
console.log('[DialogSession] onAgentProgress:', data.agentId, data.step, data.status);
const agentSegment = this.segments.find(
s => s.type === 'agent' && s.agentId === data.agentId
);
if (agentSegment && agentSegment.agentSteps) {
if (data.status === 'running') {
agentSegment.agentSteps.push({
step: data.step,
toolName: data.toolName,
toolInput: data.toolInput,
status: 'running'
});
} else {
const step = agentSegment.agentSteps.find(s => s.step === data.step);
if (step) {
step.status = data.status;
step.toolResult = data.toolResult;
}
}
callbacks.onSegmentUpdate?.(this.segments);
}
},
onAgentComplete: (data) => {
console.log('[DialogSession] onAgentComplete:', data.agentId);
const agentSegment = this.segments.find(
s => s.type === 'agent' && s.agentId === data.agentId
);
if (agentSegment) {
agentSegment.agentStatus = 'completed';
agentSegment.content = data.summary;
callbacks.onSegmentUpdate?.(this.segments);
}
},
onAgentError: (data) => {
console.log('[DialogSession] onAgentError:', data.agentId, data.error);
const agentSegment = this.segments.find(
s => s.type === 'agent' && s.agentId === data.agentId
);
if (agentSegment) {
agentSegment.agentStatus = 'error';
agentSegment.content = data.error;
callbacks.onSegmentUpdate?.(this.segments);
}
},
onMemoryCompacted: async (data) => {
console.log('[DialogSession] onMemoryCompacted:', data.taskId);
// 保存压缩数据到本地
await historyManager.saveCompactedData(data.compactedData);
},
onContextUsage: (data) => {
console.log('[DialogSession] onContextUsage:', data.currentTokens, '/', data.maxTokens);
callbacks.onContextUsage?.(data);
},
onOpen: () => { onOpen: () => {
console.log('[DialogSession] SSE 连接已建立'); console.log('[DialogSession] SSE 连接已建立');
}, },
@ -287,6 +682,25 @@ export class DialogSession {
} }
this.isActive = false; this.isActive = false;
userInteractionManager.cancelAll(); userInteractionManager.cancelAll();
// 通知后端停止处理
stopDialog(this.taskId).catch(err => {
console.warn('[DialogSession] 停止对话请求失败:', err);
});
}
/**
* 获取当前的消息段落(用于中止时保存)
*/
getSegments(): MessageSegment[] {
return this.segments;
}
/**
* 获取累积的文本内容
*/
getAccumulatedText(): string {
return this.accumulatedText;
} }
/** /**
@ -309,13 +723,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,
@ -21,8 +23,14 @@ import type {
ToolErrorEvent, ToolErrorEvent,
WarningEvent, WarningEvent,
NotificationEvent, NotificationEvent,
DepthUpdateEvent DepthUpdateEvent,
AgentStartEvent,
AgentProgressEvent,
AgentCompleteEvent,
AgentErrorEvent,
ContextUsageEvent
} from '../types/api'; } from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory';
/** /**
* SSE 事件回调接口 * SSE 事件回调接口
@ -32,6 +40,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;
/** 工具执行完成 */ /** 工具执行完成 */
@ -50,6 +62,18 @@ export interface SSECallbacks {
onNotification?: (data: NotificationEvent) => void; onNotification?: (data: NotificationEvent) => void;
/** 深度更新 */ /** 深度更新 */
onDepthUpdate?: (data: DepthUpdateEvent) => void; onDepthUpdate?: (data: DepthUpdateEvent) => void;
/** 子智能体启动 */
onAgentStart?: (data: AgentStartEvent) => void;
/** 子智能体进度 */
onAgentProgress?: (data: AgentProgressEvent) => void;
/** 子智能体完成 */
onAgentComplete?: (data: AgentCompleteEvent) => void;
/** 子智能体错误 */
onAgentError?: (data: AgentErrorEvent) => void;
/** 记忆压缩完成 */
onMemoryCompacted?: (data: MemoryCompactedEvent) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: ContextUsageEvent) => void;
/** 连接打开 */ /** 连接打开 */
onOpen?: () => void; onOpen?: () => void;
/** 连接关闭 */ /** 连接关闭 */
@ -124,7 +148,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 = {
@ -256,6 +280,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;
@ -283,6 +313,24 @@ function dispatchEvent(
case 'depth_update': case 'depth_update':
callbacks.onDepthUpdate?.(data as DepthUpdateEvent); callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
break; break;
case 'agent_start':
callbacks.onAgentStart?.(data as AgentStartEvent);
break;
case 'agent_progress':
callbacks.onAgentProgress?.(data as AgentProgressEvent);
break;
case 'agent_complete':
callbacks.onAgentComplete?.(data as AgentCompleteEvent);
break;
case 'agent_error':
callbacks.onAgentError?.(data as AgentErrorEvent);
break;
case 'memory_compacted':
callbacks.onMemoryCompacted?.(data as MemoryCompactedEvent);
break;
case 'context_usage':
callbacks.onContextUsage?.(data as ContextUsageEvent);
break;
default: default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data); console.log(`[SSE] 未知事件类型: ${eventType}`, data);
} }

View File

@ -20,10 +20,13 @@ import type {
ToolName, ToolName,
FileReadArgs, FileReadArgs,
FileWriteArgs, FileWriteArgs,
FileDeleteArgs,
FileListArgs, FileListArgs,
SyntaxCheckArgs, SyntaxCheckArgs,
SimulationArgs, SimulationArgs,
WaveformSummaryArgs WaveformSummaryArgs,
KnowledgeSaveArgs,
KnowledgeLoadArgs
} from '../types/api'; } from '../types/api';
/** /**
@ -61,6 +64,9 @@ export async function executeToolCall(
case 'file_write': case 'file_write':
resultText = await executeFileWrite(args as unknown as FileWriteArgs); resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break; break;
case 'file_delete':
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
break;
case 'file_list': case 'file_list':
resultText = await executeFileList(args as unknown as FileListArgs); resultText = await executeFileList(args as unknown as FileListArgs);
break; break;
@ -73,6 +79,12 @@ export async function executeToolCall(
case 'waveform_summary': case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs); resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
break; break;
case 'knowledge_save':
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
break;
case 'knowledge_load':
resultText = await executeKnowledgeLoad();
break;
default: default:
throw new Error(`未知工具: ${toolName}`); throw new Error(`未知工具: ${toolName}`);
} }
@ -105,9 +117,59 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
*/ */
async function executeFileWrite(args: FileWriteArgs): Promise<string> { async function executeFileWrite(args: FileWriteArgs): Promise<string> {
await createOrOverwriteFile(args.path, args.content); await createOrOverwriteFile(args.path, args.content);
// Verilog 文件添加知识图谱提示
const isVerilogFile = args.path.endsWith('.v') || args.path.endsWith('.sv');
if (isVerilogFile) {
return `文件已写入: ${args.path}\n\n[提示] 如有新信号或规则,请更新知识图谱`;
}
return `文件已写入: ${args.path}`; return `文件已写入: ${args.path}`;
} }
/**
* 执行 file_delete 工具
* 删除指定路径的文件
*/
async function executeFileDelete(args: FileDeleteArgs): Promise<string> {
const filePath = args.path;
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const workspacePath = workspaceFolders[0].uri.fsPath;
// 解析文件路径(支持相对路径和绝对路径)
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(workspacePath, filePath);
// 检查文件是否存在
if (!fs.existsSync(absolutePath)) {
throw new Error(`文件不存在: ${filePath}`);
}
// 检查是否为文件(不允许删除目录)
const stat = fs.statSync(absolutePath);
if (stat.isDirectory()) {
throw new Error(`不能删除目录,请指定文件路径: ${filePath}`);
}
// 删除文件
fs.unlinkSync(absolutePath);
// Verilog 文件添加知识图谱提示
const isVerilogFile = filePath.endsWith('.v') || filePath.endsWith('.sv');
if (isVerilogFile) {
return `文件已删除: ${filePath}\n\n[提示] 请删除知识图谱中相关节点`;
}
return `文件已删除: ${filePath}`;
}
/** /**
* 执行 file_list 工具 * 执行 file_list 工具
*/ */
@ -246,6 +308,65 @@ async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`; return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
} }
/**
* 执行 knowledge_save 工具
* 保存知识图谱到 .iccoder/knowledge.json
*/
async function executeKnowledgeSave(args: KnowledgeSaveArgs): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) {
throw new Error('请先打开一个工作区');
}
const iccoderDirUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder');
const knowledgeUri = vscode.Uri.joinPath(iccoderDirUri, 'knowledge.json');
// 确保 .iccoder 目录存在(兼容远程/虚拟工作区)
await vscode.workspace.fs.createDirectory(iccoderDirUri);
// 写入知识图谱UTF-8
await vscode.workspace.fs.writeFile(knowledgeUri, Buffer.from(args.data || '', 'utf-8'));
return `知识图谱已保存: .iccoder/knowledge.json`;
}
/**
* 执行 knowledge_load 工具
* 从 .iccoder/knowledge.json 加载知识图谱
*/
async function executeKnowledgeLoad(): Promise<string> {
const workspaceFolder = getWorkspaceFolder();
if (!workspaceFolder) {
throw new Error('请先打开一个工作区');
}
const knowledgeUri = vscode.Uri.joinPath(workspaceFolder.uri, '.iccoder', 'knowledge.json');
try {
const bytes = await vscode.workspace.fs.readFile(knowledgeUri);
const content = Buffer.from(bytes).toString('utf-8');
return content;
} catch (error) {
// 文件不存在:返回空图谱
if (error instanceof vscode.FileSystemError && error.code === 'FileNotFound') {
// 与后端 KnowledgeGraph 结构保持一致nodes/edges + nodeClass 多态字段)
return JSON.stringify({ taskId: '', version: 1, module: null, nodes: [], edges: [] });
}
throw error;
}
}
function getWorkspaceFolder(): vscode.WorkspaceFolder | undefined {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return undefined;
}
const activeUri = vscode.window.activeTextEditor?.document?.uri;
const activeFolder = activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : undefined;
return activeFolder ?? folders[0];
}
/** /**
* 获取 iverilog 路径 * 获取 iverilog 路径
*/ */

View File

@ -3,7 +3,7 @@
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答 * 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
*/ */
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { submitAnswer } from './apiClient'; import { submitAnswer, submitToolConfirm } from './apiClient';
import type { AskUserEvent, AnswerRequest } from '../types/api'; import type { AskUserEvent, AnswerRequest } from '../types/api';
/** /**
@ -32,6 +32,13 @@ export class UserInteractionManager {
this.webviewPanel = panel; this.webviewPanel = panel;
} }
/**
* 获取 WebView 面板
*/
getWebviewPanel(): vscode.WebviewPanel | null {
return this.webviewPanel;
}
/** /**
* 处理 ask_user 事件 * 处理 ask_user 事件
* @param event ask_user 事件数据 * @param event ask_user 事件数据
@ -60,13 +67,13 @@ export class UserInteractionManager {
reject reject
}); });
// 设置超时(5分钟 // 设置超时(2小时
setTimeout(() => { setTimeout(() => {
if (this.pendingQuestions.has(askId)) { if (this.pendingQuestions.has(askId)) {
this.pendingQuestions.delete(askId); this.pendingQuestions.delete(askId);
reject(new Error('用户回答超时')); reject(new Error('用户回答超时'));
} }
}, 300000); }, 7200000);
}); });
} }
@ -107,6 +114,30 @@ export class UserInteractionManager {
taskId: string, taskId: string,
answer: string answer: string
): Promise<void> { ): Promise<void> {
// 检查是否是工具确认类型的问题
if (askId.startsWith('tool_confirm_')) {
// 提取 confirmId
const confirmId = parseInt(askId.replace('tool_confirm_', ''));
const approved = answer === '确认执行';
console.log(`[UserInteraction] 提交工具确认: confirmId=${confirmId}, approved=${approved}`);
try {
const response = await submitToolConfirm({
confirmId,
taskId,
approved
});
if (!response.success) {
throw new Error(response.error || '提交工具确认失败');
}
console.log(`[UserInteraction] 工具确认已提交: confirmId=${confirmId}`);
} catch (error) {
console.error(`[UserInteraction] 提交工具确认失败: confirmId=${confirmId}`, error);
throw error;
}
} else {
// 普通问题回答
const request: AnswerRequest = { const request: AnswerRequest = {
askId, askId,
taskId, taskId,
@ -124,6 +155,7 @@ export class UserInteractionManager {
throw error; throw error;
} }
} }
}
/** /**
* 取消所有待处理的问题 * 取消所有待处理的问题

View File

@ -3,8 +3,19 @@
* 对应后端 IC Coder Backend 的接口格式 * 对应后端 IC Coder Backend 的接口格式
*/ */
import { CompactedMemory, CompactedMessage } from './memory';
// ============== 对话请求/响应 ============== // ============== 对话请求/响应 ==============
/**
* 运行模式类型
* - plan: 只读模式,只能查询分析
* - ask: 逐个确认,每个写操作需确认
* - agent: 智能体自主(默认)
* - auto: 完全自动
*/
export type RunMode = 'plan' | 'ask' | 'agent' | 'auto';
/** /**
* 对话请求 * 对话请求
* POST /api/dialog/stream * POST /api/dialog/stream
@ -16,8 +27,14 @@ export interface DialogRequest {
message: string; message: string;
/** 用户ID */ /** 用户ID */
userId: string; userId: string;
/** 工具模式 */ /** 运行模式 */
toolMode: 'ASK' | 'AGENT'; mode: RunMode;
/** 压缩后的记忆数据(用于后端重启后恢复) */
compactedData?: CompactedMemory;
/** 压缩后产生的新消息 */
newMessages?: CompactedMessage[];
/** 知识图谱数据JSON 字符串,用于恢复知识图谱) */
knowledgeData?: string;
} }
// ============== SSE 事件类型 ============== // ============== SSE 事件类型 ==============
@ -26,10 +43,18 @@ 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' // 工具执行错误
| 'ask_user' // 向用户提问 | 'ask_user' // 向用户提问
| 'agent_start' // 子智能体启动
| 'agent_progress' // 子智能体进度
| 'agent_complete' // 子智能体完成
| 'agent_error' // 子智能体错误
| 'memory_compacted' // 记忆压缩完成
| 'context_usage' // 上下文使用量
| 'complete' // 对话完成 | 'complete' // 对话完成
| 'error' // 错误 | 'error' // 错误
| 'warning' // 警告 | 'warning' // 警告
@ -59,6 +84,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;
@ -92,6 +143,52 @@ export interface DepthUpdateEvent {
depth: number; depth: number;
} }
// ============== 智能体事件类型 ==============
/** agent_start 事件数据 */
export interface AgentStartEvent {
agentId: string;
agentType: string;
agentName: string;
instruction: string;
timestamp: number;
}
/** agent_progress 事件数据 */
export interface AgentProgressEvent {
agentId: string;
step: number;
toolName: string;
toolInput?: unknown;
toolResult?: string;
status: 'running' | 'completed' | 'error';
timestamp: number;
}
/** agent_complete 事件数据 */
export interface AgentCompleteEvent {
agentId: string;
agentType: string;
summary: string;
stats: Record<string, unknown>;
timestamp: number;
}
/** agent_error 事件数据 */
export interface AgentErrorEvent {
agentId: string;
agentType: string;
error: string;
timestamp: number;
}
/** context_usage 事件数据 */
export interface ContextUsageEvent {
currentTokens: number;
maxTokens: number;
percentage: number;
}
// ============== 工具调用协议 (MCP 格式) ============== // ============== 工具调用协议 (MCP 格式) ==============
/** /**
@ -186,16 +283,34 @@ export interface ToolResultResponse {
error?: string; error?: string;
} }
// ============== 工具确认响应 ==============
/**
* 工具确认响应请求
* POST /api/tool/confirm
*/
export interface ToolConfirmResponse {
/** 确认ID与 ToolConfirmEvent.confirmId 对应 */
confirmId: number;
/** 任务ID */
taskId: string;
/** 是否批准执行 */
approved: boolean;
}
// ============== 辅助类型 ============== // ============== 辅助类型 ==============
/** 后端工具名称 */ /** 后端工具名称 */
export type ToolName = export type ToolName =
| 'file_read' | 'file_read'
| 'file_write' | 'file_write'
| 'file_delete'
| 'file_list' | 'file_list'
| 'syntax_check' | 'syntax_check'
| 'simulation' | 'simulation'
| 'waveform_summary'; | 'waveform_summary'
| 'knowledge_save'
| 'knowledge_load';
/** file_read 工具参数 */ /** file_read 工具参数 */
export interface FileReadArgs { export interface FileReadArgs {
@ -208,6 +323,12 @@ export interface FileWriteArgs {
content: string; content: string;
} }
/** file_delete 工具参数 */
export interface FileDeleteArgs {
/** 要删除的文件路径 */
path: string;
}
/** file_list 工具参数 */ /** file_list 工具参数 */
export interface FileListArgs { export interface FileListArgs {
path?: string; path?: string;
@ -233,11 +354,25 @@ export interface WaveformSummaryArgs {
checkpoints?: string; checkpoints?: string;
} }
/** knowledge_save 工具参数 */
export interface KnowledgeSaveArgs {
/** 知识图谱 JSON 数据 */
data: string;
}
/** knowledge_load 工具参数 */
export interface KnowledgeLoadArgs {
// 无参数,直接读取 .iccoder/knowledge.json
}
/** 工具参数联合类型 */ /** 工具参数联合类型 */
export type ToolArgs = export type ToolArgs =
| FileReadArgs | FileReadArgs
| FileWriteArgs | FileWriteArgs
| FileDeleteArgs
| FileListArgs | FileListArgs
| SyntaxCheckArgs | SyntaxCheckArgs
| SimulationArgs | SimulationArgs
| WaveformSummaryArgs; | WaveformSummaryArgs
| KnowledgeSaveArgs
| KnowledgeLoadArgs;

View File

@ -5,7 +5,8 @@ export enum MessageType {
SYSTEM = "SYSTEM", SYSTEM = "SYSTEM",
USER = "USER", USER = "USER",
AI = "AI", AI = "AI",
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT" TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT",
COMPACTION_SUMMARY = "COMPACTION_SUMMARY" // 压缩摘要
} }
/** /**
@ -69,10 +70,22 @@ export interface ToolExecutionResultMessage extends BaseMessage {
text: string; // JSON字符串 text: string; // JSON字符串
} }
/**
* 压缩摘要消息
*/
export interface CompactionSummaryMessage extends BaseMessage {
type: MessageType.COMPACTION_SUMMARY;
summary: string;
version: number;
compactedAt: string;
originalMessageCount: number;
compactedMessageCount: number;
}
/** /**
* 联合消息类型 * 联合消息类型
*/ */
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage; export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage | CompactionSummaryMessage;
/** /**
* 对话轮次元数据 * 对话轮次元数据

42
src/types/memory.ts Normal file
View File

@ -0,0 +1,42 @@
/**
* 压缩记忆相关类型定义
*/
/**
* 压缩后的记忆数据
*/
export interface CompactedMemory {
taskId: string;
version: number;
compactedAt: string;
summary: string;
recentMessages: CompactedMessage[];
originalMessageCount: number;
compactedMessageCount: number;
}
/**
* 压缩消息格式
*/
export interface CompactedMessage {
type: 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT';
content: string;
toolCall?: ToolCallInfo;
}
/**
* 工具调用信息
*/
export interface ToolCallInfo {
toolName: string;
toolInput: string;
toolOutput?: string;
}
/**
* 记忆压缩 SSE 事件
*/
export interface MemoryCompactedEvent {
taskId: string;
compactedData: CompactedMemory;
}

View File

@ -9,8 +9,10 @@ import {
UserMessage, UserMessage,
AiMessage, AiMessage,
SystemMessage, SystemMessage,
ToolExecutionResultMessage ToolExecutionResultMessage,
CompactionSummaryMessage
} from '../types/chatHistory'; } from '../types/chatHistory';
import { CompactedMemory, CompactedMessage } from '../types/memory';
/** /**
* 会话历史管理器 * 会话历史管理器
@ -23,6 +25,8 @@ export class ChatHistoryManager {
private currentProjectPath: string | null = null; private currentProjectPath: string | null = null;
// 存储每个面板的任务信息taskId 和 projectPath // 存储每个面板的任务信息taskId 和 projectPath
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map(); private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
// 追踪压缩后产生的新消息
private newMessagesSinceCompaction: CompactedMessage[] = [];
private constructor() { private constructor() {
// 设置存储路径: ~/.iccoder // 设置存储路径: ~/.iccoder
@ -690,4 +694,203 @@ export class ChatHistoryManager {
hasMore: end < total hasMore: end < total
}; };
} }
// ========== 压缩数据相关方法 ==========
/**
* 保存压缩数据(存入 conversation.json 作为压缩摘要消息)
*/
public async saveCompactedData(compacted: CompactedMemory): Promise<void> {
// 尝试从多个来源获取 projectPath
let projectPath = this.currentProjectPath;
if (!projectPath) {
for (const [, taskInfo] of this.panelTaskMap) {
if (taskInfo.taskId === compacted.taskId) {
projectPath = taskInfo.projectPath;
break;
}
}
}
if (!projectPath) {
console.error('[ChatHistoryManager] 无法保存压缩数据projectPath 为空');
return;
}
// 读取现有对话历史
const taskDir = this.getTaskDir(projectPath, compacted.taskId);
const conversationPath = path.join(taskDir, 'conversation.json');
let messages: ChatMessage[] = [];
try {
const uri = vscode.Uri.file(conversationPath);
const content = await vscode.workspace.fs.readFile(uri);
messages = JSON.parse(Buffer.from(content).toString('utf-8'));
} catch {
// 文件不存在,使用空数组
}
// 创建压缩摘要消息
const summaryMessage: CompactionSummaryMessage = {
type: MessageType.COMPACTION_SUMMARY,
summary: compacted.summary,
version: compacted.version,
compactedAt: compacted.compactedAt,
originalMessageCount: compacted.originalMessageCount,
compactedMessageCount: compacted.compactedMessageCount
};
// 添加到对话历史
messages.push(summaryMessage);
// 保存
const uri = vscode.Uri.file(conversationPath);
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
await vscode.workspace.fs.writeFile(uri, content);
// 重置新消息追踪
this.newMessagesSinceCompaction = [];
console.log(`[ChatHistoryManager] 压缩摘要已保存到 conversation.json: taskId=${compacted.taskId}`);
}
/**
* 加载压缩数据(从 conversation.json 构建)
*/
public async loadCompactedData(taskId: string): Promise<CompactedMemory | null> {
// 尝试从多个来源获取 projectPath
let projectPath = this.currentProjectPath;
if (!projectPath) {
for (const [, taskInfo] of this.panelTaskMap) {
if (taskInfo.taskId === taskId) {
projectPath = taskInfo.projectPath;
break;
}
}
}
if (!projectPath) {
console.log('[ChatHistoryManager] loadCompactedData: projectPath 为空');
return null;
}
// 读取 conversation.json
const taskDir = this.getTaskDir(projectPath, taskId);
const conversationPath = path.join(taskDir, 'conversation.json');
try {
const uri = vscode.Uri.file(conversationPath);
const content = await vscode.workspace.fs.readFile(uri);
const messages: ChatMessage[] = JSON.parse(Buffer.from(content).toString('utf-8'));
if (messages.length === 0) {
console.log('[ChatHistoryManager] conversation.json 为空');
return null;
}
// 从 conversation.json 构建 CompactedMemory
return this.buildCompactedMemoryFromConversation(taskId, messages);
} catch {
console.log('[ChatHistoryManager] conversation.json 不存在:', conversationPath);
return null;
}
}
/**
* 从 conversation.json 构建 CompactedMemory
*/
private buildCompactedMemoryFromConversation(taskId: string, messages: ChatMessage[]): CompactedMemory {
// 查找最后一个压缩摘要消息
let lastSummary: CompactionSummaryMessage | null = null;
let summaryIndex = -1;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].type === MessageType.COMPACTION_SUMMARY) {
lastSummary = messages[i] as CompactionSummaryMessage;
summaryIndex = i;
break;
}
}
// 获取摘要后的消息(或全部消息)
const recentMessages = summaryIndex >= 0
? messages.slice(summaryIndex + 1)
: messages;
// 转换为 CompactedMessage 格式
const compactedMessages: CompactedMessage[] = recentMessages.map(msg => ({
type: this.mapMessageType(msg.type),
content: this.extractMessageContent(msg)
}));
return {
taskId,
version: lastSummary?.version || Date.now(),
compactedAt: lastSummary?.compactedAt || new Date().toISOString(),
summary: lastSummary?.summary || '',
recentMessages: compactedMessages,
originalMessageCount: messages.length,
compactedMessageCount: compactedMessages.length
};
}
/**
* 映射消息类型
*/
private mapMessageType(type: MessageType): 'USER' | 'AI' | 'SYSTEM' | 'TOOL_RESULT' {
switch (type) {
case MessageType.USER: return 'USER';
case MessageType.AI: return 'AI';
case MessageType.SYSTEM: return 'SYSTEM';
case MessageType.TOOL_EXECUTION_RESULT: return 'TOOL_RESULT';
default: return 'USER';
}
}
/**
* 提取消息内容
*/
private extractMessageContent(msg: ChatMessage): string {
switch (msg.type) {
case MessageType.USER:
return (msg as UserMessage).contents?.[0]?.text || '';
case MessageType.AI:
return (msg as AiMessage).text || '';
case MessageType.SYSTEM:
return (msg as SystemMessage).text || '';
case MessageType.TOOL_EXECUTION_RESULT:
return (msg as ToolExecutionResultMessage).text || '';
default:
return '';
}
}
/**
* 获取压缩后产生的新消息
*/
public getNewMessagesSinceCompaction(): CompactedMessage[] {
return this.newMessagesSinceCompaction;
}
/**
* 追踪新消息(用户消息)
*/
public trackUserMessage(text: string): void {
this.newMessagesSinceCompaction.push({
type: 'USER',
content: text
});
}
/**
* 追踪新消息AI消息
*/
public trackAiMessage(text: string): void {
this.newMessagesSinceCompaction.push({
type: 'AI',
content: text
});
}
} }

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,46 @@ 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;
/** 最后一个活跃的 taskId用于压缩等操作 */
let lastTaskId: string | 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,32 +90,31 @@ 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);
// 后端不可用时,使用本地模拟回复
}
}
// 本地模拟回复(后端不可用时的 fallback
console.log("使用本地模拟回复");
const reply = getMockReply(text);
// 记录AI回复到历史允许失败
try {
const historyManager = ChatHistoryManager.getInstance();
await historyManager.addAiMessage(reply);
} catch (error) {
console.warn("记录AI回复历史失败:", error);
}
setTimeout(() => {
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "updateStatus",
text: reply, text: "后端服务不可用",
type: "error",
});
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
throw error;
}
}
// 如果没有 extensionPath显示错误
panel.webview.postMessage({
command: "updateStatus",
text: "无法处理消息:缺少必要参数",
type: "error",
}); });
}, 500);
} }
/** /**
@ -97,11 +123,18 @@ 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);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
if (reuseTaskId) {
console.log("[MessageHandler] 复用 taskId 创建会话:", reuseTaskId);
}
} }
const historyManager = ChatHistoryManager.getInstance(); const historyManager = ChatHistoryManager.getInstance();
@ -114,7 +147,9 @@ async function handleUserMessageWithBackend(
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
currentSession!.sendMessage(text, { currentSession!.sendMessage(
text,
{
onText: (fullText, isStreaming) => { onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理 // 不再单独处理文本,统一通过 onSegmentUpdate 处理
}, },
@ -160,30 +195,66 @@ async function handleUserMessageWithBackend(
}); });
// 最后一次发送完整的段落 // 最后一次发送完整的段落
console.log('[MessageHandler] 对话完成, 段落数:', segments.length); console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments)); console.log(
"[MessageHandler] segments 内容:",
JSON.stringify(segments)
);
const result = await panel.webview.postMessage({ const result = await panel.webview.postMessage({
command: "updateSegments", command: "updateSegments",
segments: segments, segments: segments,
isComplete: true, isComplete: true,
}); });
console.log('[MessageHandler] postMessage 返回值:', result); console.log("[MessageHandler] postMessage 返回值:", result);
// 保存完整的 segments 到历史记录 // 保存完整的 segments 到历史记录
try { try {
// 将完整的 segments 保存到一条 AI 消息中 // 将完整的 segments 保存到一条 AI 消息中
// 这样加载时可以完整还原对话样式 // 这样加载时可以完整还原对话样式
const textContent = segments const textContent = segments
.filter(s => s.type === 'text' && s.content) .filter((s) => s.type === "text" && s.content)
.map(s => s.content) .map((s) => s.content)
.join('\n'); .join("\n");
await historyManager.addAiMessage(textContent, undefined, segments); await historyManager.addAiMessage(textContent, undefined, segments);
} catch (error) { } catch (error) {
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();
}, },
@ -195,13 +266,31 @@ async function handleUserMessageWithBackend(
command: "receiveMessage", command: "receiveMessage",
text: `❌ 错误: ${message}`, text: `❌ 错误: ${message}`,
}); });
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
reject(new Error(message)); reject(new Error(message));
}, },
onNotification: (message) => { onNotification: (message) => {
vscode.window.showInformationMessage(message); vscode.window.showInformationMessage(message);
}, },
onContextUsage: (data) => {
// 发送上下文使用量到 WebView
panel.webview.postMessage({
command: "contextUsage",
currentTokens: data.currentTokens,
maxTokens: data.maxTokens,
percentage: data.percentage,
}); });
},
},
mode
);
}); });
} }
@ -221,11 +310,116 @@ export async function handleUserAnswer(
/** /**
* 中止当前对话 * 中止当前对话
*/ */
export function abortCurrentDialog(): void { export async function abortCurrentDialog(): Promise<void> {
if (currentSession) {
// 保存当前已有的对话内容
const segments = currentSession.getSegments();
if (segments && segments.length > 0) {
try {
const historyManager = ChatHistoryManager.getInstance();
const textContent = segments
.filter((s) => s.type === "text" && s.content)
.map((s) => s.content)
.join("\n");
// 添加中止标记
const abortedContent = textContent + "\n\n[对话已被用户中止]";
await historyManager.addAiMessage(abortedContent, undefined, segments);
console.log("[MessageHandler] 已保存中止前的对话内容");
} catch (error) {
console.warn("[MessageHandler] 保存中止对话失败:", error);
}
}
}
// 通知 WebView 重置分段消息容器
const panel = userInteractionManager.getWebviewPanel();
if (panel) {
panel.webview.postMessage({ command: "resetSegmentedMessage" });
console.log("[MessageHandler] 已发送重置分段消息命令");
}
dialogManager.abortCurrentSession(); dialogManager.abortCurrentSession();
currentSession = null; currentSession = null;
} }
/**
* 获取当前会话的 taskId
*/
export function getCurrentTaskId(): string | null {
return currentSession?.getTaskId() || lastTaskId;
}
/**
* 设置最后的 taskId加载历史会话时调用
*/
export function setLastTaskId(taskId: string): void {
lastTaskId = taskId;
console.log("[MessageHandler] 设置 lastTaskId:", taskId);
}
/**
* 处理计划操作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);
}
}
/** /**
* 解析文件操作命令 * 解析文件操作命令
*/ */
@ -261,7 +455,9 @@ function parseFileOperation(text: string): {
} }
// 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts优先匹配避免被修改匹配 // 匹配重命名文件:将 xxx.ts 重命名为 yyy.ts 或 把 xxx.ts 改名为 yyy.ts优先匹配避免被修改匹配
const renameMatch = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/); const renameMatch = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:重命名|改名)\s*(?:为|成)\s*(.+?\.\w+)/
);
if (renameMatch) { if (renameMatch) {
const oldPath = renameMatch[1].trim(); const oldPath = renameMatch[1].trim();
const newPath = renameMatch[2].trim(); const newPath = renameMatch[2].trim();
@ -276,7 +472,9 @@ function parseFileOperation(text: string): {
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb" // 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb" // 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
// 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb' // 格式3: 将 xxx.ts 文件 'aaa' 替换为 'bbb'
const replaceMatch1 = lowerText.match(/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/); const replaceMatch1 = lowerText.match(
/在\s*(.+?\.\w+)\s*(?:文件)?中?\s*(?:将|把)\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
);
if (replaceMatch1) { if (replaceMatch1) {
const filePath = replaceMatch1[1].trim(); const filePath = replaceMatch1[1].trim();
const searchText = replaceMatch1[2].trim(); const searchText = replaceMatch1[2].trim();
@ -290,7 +488,9 @@ function parseFileOperation(text: string): {
} }
// 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb" // 格式2: 将 xxx.ts 文件 "aaa" 替换为 "bbb"
const replaceMatch2 = lowerText.match(/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/); const replaceMatch2 = lowerText.match(
/(?:将|把)\s*(.+?\.\w+)\s*(?:文件)?\s*["'](.+?)["']\s*替换\s*(?:为|成)\s*["'](.+?)["']/
);
if (replaceMatch2) { if (replaceMatch2) {
const filePath = replaceMatch2[1].trim(); const filePath = replaceMatch2[1].trim();
const searchText = replaceMatch2[2].trim(); const searchText = replaceMatch2[2].trim();
@ -618,41 +818,6 @@ export async function handleReplaceInFile(
} }
} }
/**
* 获取模拟回复
*/
function getMockReply(question: string): string {
const replies = [
`已收到您的问题:"${question}"
这是一个演示版本实际需要连接AI服务。
示例回复:这是一个计数器模板:
\`\`\`verilog
module counter (
input clk,
input rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) count <= 0;
else count <= count + 1;
end
endmodule
\`\`\``,
`感谢提问!关于"${question}",在真实版本中我会:
1. 分析您的代码上下文
2. 提供优化建议
3. 生成完整代码
4. 解释设计原理
当前是演示版,请点击侧边栏按钮快速生成代码。`,
];
return replies[Math.floor(Math.random() * replies.length)];
}
/** /**
* 将代码插入到编辑器 * 将代码插入到编辑器
*/ */
@ -745,7 +910,8 @@ async function handleVCDGeneration(
if (!projectCheck.hasTestbench) { if (!projectCheck.hasTestbench) {
errorMsg += "• ❌ 缺少 testbench 文件\n"; errorMsg += "• ❌ 缺少 testbench 文件\n";
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n"; errorMsg +=
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
} else { } else {
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`; errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
} }
@ -789,9 +955,7 @@ async function handleVCDGeneration(
fileName: fileName, fileName: fileName,
}); });
vscode.window.showInformationMessage( vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
`VCD 文件生成成功: ${fileName}`
);
} else { } else {
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",

View File

@ -94,6 +94,13 @@ export async function readDirectory(
const results = []; const results = [];
for (const [fileName, fileType] of entries) { for (const [fileName, fileType] of entries) {
// 处理目录
if (fileType === vscode.FileType.Directory) {
results.push({ path: fileName + '/', content: '[目录]', isDirectory: true });
continue;
}
// 处理文件
if (fileType === vscode.FileType.File) { if (fileType === vscode.FileType.File) {
// 如果指定了扩展名过滤 // 如果指定了扩展名过滤
if (extensions && extensions.length > 0) { if (extensions && extensions.length > 0) {
@ -108,7 +115,7 @@ export async function readDirectory(
const fileUri = vscode.Uri.file(filePath); const fileUri = vscode.Uri.file(filePath);
const contentBytes = await vscode.workspace.fs.readFile(fileUri); const contentBytes = await vscode.workspace.fs.readFile(fileUri);
const content = Buffer.from(contentBytes).toString("utf-8"); const content = Buffer.from(contentBytes).toString("utf-8");
results.push({ path: fileName, content }); results.push({ path: fileName, content, isDirectory: false });
} catch (error) { } catch (error) {
// 跳过无法读取的文件 // 跳过无法读取的文件
continue; continue;

View File

@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
{ {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")], localResourceRoots: [
vscode.Uri.joinPath(context.extensionUri, "media"),
vscode.Uri.joinPath(context.extensionUri, "src", "assets")
],
} }
); );
@ -39,15 +42,36 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
const iconUri = panel.webview.asWebviewUri( const iconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "media", "icon.png") vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
); );
// 获取模型图标URI
const autoIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Auto.png")
);
const liteIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "lite.png")
);
const syIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Sy.png")
);
const maxIconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "src", "assets", "model", "Max.png")
);
// 设置HTML内容 // 设置HTML内容
panel.webview.html = getWebviewContent(iconUri.toString()); panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息 // 处理消息
panel.webview.onDidReceiveMessage( panel.webview.onDidReceiveMessage(
(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);
@ -90,7 +114,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
break; break;
// 新增:中止对话 // 新增:中止对话
case "abortDialog": case "abortDialog":
abortCurrentDialog(); void abortCurrentDialog();
break; break;
} }
}, },

177
src/views/agentCard.ts Normal file
View File

@ -0,0 +1,177 @@
/**
* 智能体卡片组件
*
* 功能说明:
* - 提供智能体执行状态的可视化展示
* - 显示智能体名称、状态和执行步骤
* - 支持实时更新步骤信息
*/
import { agentIconSvg } from "../constants/toolIcons";
/**
* 获取智能体卡片的样式
*/
export function getAgentCardStyles(): string {
return `
/* 智能体卡片样式 */
.segment-agent {
margin: 8px 0;
}
.agent-card {
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
overflow: hidden;
background: var(--vscode-editor-background);
}
.agent-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-input-border);
}
.agent-icon {
font-size: 16px;
}
.agent-name {
font-weight: 500;
flex: 1;
}
.agent-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
}
.agent-status.running {
background: var(--vscode-inputValidation-infoBackground);
color: var(--vscode-inputValidation-infoForeground);
}
.agent-status.completed {
background: #28a745;
color: white;
}
.agent-status.error {
background: #dc3545;
color: white;
}
.agent-body {
padding: 8px;
}
.agent-steps-container {
max-height: 150px;
overflow-y: auto;
font-size: 12px;
}
.agent-step {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 4px;
background: var(--vscode-list-hoverBackground);
}
.agent-step:last-child {
margin-bottom: 0;
}
.step-icon {
flex-shrink: 0;
}
.step-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.step-result {
color: var(--vscode-descriptionForeground);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-step-placeholder {
color: var(--vscode-descriptionForeground);
font-style: italic;
padding: 8px;
text-align: center;
}
`;
}
/**
* 获取智能体卡片的脚本
*/
export function getAgentCardScript(): string {
return `
// 工具名称中文映射
function getAgentToolDisplayName(toolName) {
const toolNameMap = {
'file_read': '文件读取',
'file_write': '文件写入',
'file_delete': '文件删除',
'file_list': '检索文件',
'syntax_check': '语法检查',
'simulation': '仿真',
'waveform_summary': '波形分析',
'knowledge_save': '保存知识库',
'knowledge_load': '加载知识库',
'queryKnowledgeSummary': '查询知识摘要',
'queryRules': '查询规则',
'setModule': '设置模块',
'addSignal': '添加信号',
'addSignalExample': '添加信号示例',
'validateKnowledgeGraph': '验证知识图谱',
'querySignals': '查询信号',
'addPlan': '添加计划',
'addEdge': '添加边',
'showPlan': '显示计划',
'spawnExplorer': '代码探索'
};
return toolNameMap[toolName] || toolName;
}
/**
* 渲染智能体卡片
* @param {Object} segment - 智能体段落数据
* @param {HTMLElement} segmentDiv - 段落容器元素
*/
function renderAgentCard(segment, segmentDiv) {
segmentDiv.className += ' segment-agent';
const statusText = segment.agentStatus === 'completed' ? '完成'
: segment.agentStatus === 'error' ? '错误' : '执行中';
const statusClass = segment.agentStatus || 'running';
const stepsHtml = (segment.agentSteps || []).map(step => {
const icon = step.status === 'completed' ? '✅' : step.status === 'error' ? '❌' : '🔄';
const displayName = getAgentToolDisplayName(step.toolName);
const result = step.toolResult ? \`: \${step.toolResult.substring(0, 50)}\${step.toolResult.length > 50 ? '...' : ''}\` : '';
return \`<div class="agent-step"><span class="step-icon">\${icon}</span><span class="step-name">\${displayName}</span><span class="step-result">\${result}</span></div>\`;
}).join('');
segmentDiv.innerHTML = \`
<div class="agent-card">
<div class="agent-header">
<span class="agent-icon">${agentIconSvg}</span>
<span class="agent-name">\${segment.agentName || '智能体'}</span>
<span class="agent-status \${statusClass}">\${statusText}</span>
</div>
<div class="agent-body">
<div class="agent-steps-container">
\${stepsHtml || '<div class="agent-step-placeholder">等待执行...</div>'}
</div>
</div>
</div>
\`;
// 自动滚动到最新步骤
setTimeout(() => {
const container = segmentDiv.querySelector('.agent-steps-container');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
}
`;
}

View File

@ -1,6 +1,11 @@
/** /**
* 模式选择器组件 * 模式选择器组件
* 提供 Agent/Ask/Auto 三种模式的选择功能 * 提供 Plan/Ask/Agent 四种模式的选择功能
*
* 模式说明:
* - Plan: 只读模式,只能查询分析,不能写文件
* - Ask: 逐个确认,每个写操作需用户确认
* - Agent: 智能体自主,自动执行大部分操作
*/ */
/** /**
@ -17,12 +22,21 @@ 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>
</div> </div>
<span class="tooltiptext">切换模式</span> </div>
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
</div> </div>
`; `;
} }
@ -69,7 +83,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 +97,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 +110,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 +148,22 @@ 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': '智能体自主模式','
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
// 更新选中状态 // 更新选中状态
const options = document.querySelectorAll('.mode-option'); const options = document.querySelectorAll('.mode-option');
options.forEach(option => { options.forEach(option => {

View File

@ -2,42 +2,39 @@ import { getWaveformPreviewContent } from "./waveformPreviewContent";
import { import {
getModelSelectorContent, getModelSelectorContent,
getModelSelectorStyles, getModelSelectorStyles,
getModelSelectorScript getModelSelectorScript,
} from "./modelSelector"; } from "./modelSelector";
import { import {
getModeSelectorContent, getModeSelectorContent,
getModeSelectorStyles, getModeSelectorStyles,
getModeSelectorScript getModeSelectorScript,
} from "./agentModeSelector"; } from "./agentModeSelector";
import { import {
getContextButtonContent, getContextButtonContent,
getContextButtonStyles, getContextButtonStyles,
getContextButtonScript getContextButtonScript,
} from "./contextButton"; } from "./contextButton";
import { import {
getContextCompressContent, getContextCompressContent,
getContextCompressStyles, getContextCompressStyles,
getContextCompressScript getContextCompressScript,
} from "./contextCompress"; } from "./contextCompress";
import {
getPlanToggleContent,
getPlanToggleStyles,
getPlanToggleScript
} from "./planToggle";
import { import {
getOptimizeButtonContent, getOptimizeButtonContent,
getOptimizeButtonStyles, getOptimizeButtonStyles,
getOptimizeButtonScript getOptimizeButtonScript,
} from "./optimizeButton"; } from "./optimizeButton";
import { import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
sendIconSvg,
stopIconSvg
} from "../constants/toolIcons";
/** /**
* 获取输入区域的 HTML 内容 * 获取输入区域的 HTML 内容
*/ */
export function getInputAreaContent(): string { export function getInputAreaContent(
autoIcon: string = '',
liteIcon: string = '',
syIcon: string = '',
maxIcon: string = ''
): string {
return ` return `
<div class="input-area"> <div class="input-area">
<div class="input-group"> <div class="input-group">
@ -45,17 +42,16 @@ export function getInputAreaContent(): string {
<!-- 顶部工具栏 --> <!-- 顶部工具栏 -->
<div class="input-top-toolbar"> <div class="input-top-toolbar">
${getContextButtonContent()} ${getContextButtonContent()}
${getPlanToggleContent()}
</div> </div>
<textarea <textarea
id="messageInput" id="messageInput"
placeholder="输入您的问题..." placeholder="输入您的问题,按 Enter 发送Shift + Enter 换行..."
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }" onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea> ></textarea>
<div class="input-bottom-row"> <div class="input-bottom-row">
<div class="mode-selector"> <div class="mode-selector">
${getModeSelectorContent()} ${getModeSelectorContent()}
${getModelSelectorContent()} ${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
</div> </div>
<div class="input-actions"> <div class="input-actions">
${getContextCompressContent()} ${getContextCompressContent()}
@ -81,7 +77,6 @@ export function getInputAreaStyles(): string {
${getModelSelectorStyles()} ${getModelSelectorStyles()}
${getContextButtonStyles()} ${getContextButtonStyles()}
${getContextCompressStyles()} ${getContextCompressStyles()}
${getPlanToggleStyles()}
${getOptimizeButtonStyles()} ${getOptimizeButtonStyles()}
.input-area { .input-area {
border-top: 1px solid var(--vscode-panel-border); border-top: 1px solid var(--vscode-panel-border);
@ -207,6 +202,11 @@ export function getInputAreaStyles(): string {
overflow-y: auto; overflow-y: auto;
line-height: 1.5; line-height: 1.5;
} }
textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(128, 128, 128, 0.1);
}
/* 简洁的滚动条样式 */ /* 简洁的滚动条样式 */
textarea::-webkit-scrollbar { textarea::-webkit-scrollbar {
width: 8px; width: 8px;
@ -261,16 +261,19 @@ export function getInputAreaStyles(): string {
*/ */
export function getInputAreaScript(): string { export function getInputAreaScript(): string {
return ` return `
${getModeSelectorScript()} // 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()} ${getModelSelectorScript()}
${getContextButtonScript()} ${getContextButtonScript()}
${getContextCompressScript()} ${getContextCompressScript()}
${getPlanToggleScript()}
${getOptimizeButtonScript()} ${getOptimizeButtonScript()}
// 对话状态管理 // 对话状态管理
let isConversationActive = false; let isConversationActive = false;
// 工作区检测状态
let hasCheckedWorkspace = false; // 是否已经检测过工作区
let hasWorkspace = true; // 工作区状态
// 自动调整 textarea 高度 // 自动调整 textarea 高度
function autoResizeTextarea() { function autoResizeTextarea() {
if (messageInput) { if (messageInput) {
@ -283,11 +286,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();
} }
// 切换发送按钮状态 // 切换发送按钮状态
@ -302,11 +310,17 @@ export function getInputAreaScript(): string {
sendIconContainer.style.display = 'none'; sendIconContainer.style.display = 'none';
stopIconContainer.style.display = 'block'; stopIconContainer.style.display = 'block';
isConversationActive = true; isConversationActive = true;
// 禁用输入框
messageInput.disabled = true;
messageInput.placeholder = '正在处理中,请稍候...';
} else { } else {
sendButton.classList.remove('sending'); sendButton.classList.remove('sending');
sendIconContainer.style.display = 'block'; sendIconContainer.style.display = 'block';
stopIconContainer.style.display = 'none'; stopIconContainer.style.display = 'none';
isConversationActive = false; isConversationActive = false;
// 启用输入框
messageInput.disabled = false;
messageInput.placeholder = '输入您的问题,按 Enter 发送Shift + Enter 换行...';
} }
} }
@ -326,15 +340,29 @@ export function getInputAreaScript(): string {
const text = messageInput.value.trim(); const text = messageInput.value.trim();
if (!text) return; if (!text) return;
// 如果正在对话中,阻止发送新消息
if (isConversationActive) {
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

@ -13,13 +13,23 @@
import { import {
collapseIconSvg, collapseIconSvg,
fileWriteIconSvg, fileWriteIconSvg,
fileReadIconSvg,
fileDeleteIconSvg,
syntaxCheckIconSvg, syntaxCheckIconSvg,
SearchCode, SearchCode,
agentIconSvg,
saveKnowledgeIconSvg,
simulationIconSvg,
waveformIconSvg,
knowledgeLoadIconSvg,
stateTransitionIconSvg,
} from "../constants/toolIcons"; } from "../constants/toolIcons";
import { import {
getWaveformPreviewContent, getWaveformPreviewContent,
getWaveformPreviewScript, getWaveformPreviewScript,
} from "./waveformPreviewContent"; } from "./waveformPreviewContent";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
/** /**
* 获取消息区域的 HTML 内容 * 获取消息区域的 HTML 内容
@ -400,7 +410,7 @@ export function getMessageAreaStyles(): string {
display: block; display: block;
} }
.tool-segment-header.collapsed .tool-collapse-icon { .tool-segment-header.collapsed .tool-collapse-icon {
transform: rotate(0deg); transform: rotate(-90deg);
} }
.tool-segment-header:not(.collapsed) .tool-collapse-icon { .tool-segment-header:not(.collapsed) .tool-collapse-icon {
transform: rotate(0deg); transform: rotate(0deg);
@ -416,6 +426,28 @@ export function getMessageAreaStyles(): string {
height: 100%; height: 100%;
display: block; display: block;
} }
.tool-file-read-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-file-read-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-file-delete-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-file-delete-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-syntax-check-icon { .tool-syntax-check-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -438,6 +470,61 @@ export function getMessageAreaStyles(): string {
height: 100%; height: 100%;
display: block; display: block;
} }
.tool-save-knowledge-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-save-knowledge-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-simulation-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-simulation-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-waveform-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-waveform-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-knowledge-load-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-knowledge-load-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-state-transition-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
}
.tool-state-transition-icon svg {
width: 100%;
height: 100%;
display: block;
}
.tool-segment-content { .tool-segment-content {
overflow: hidden; overflow: hidden;
transition: max-height 0.3s ease; transition: max-height 0.3s ease;
@ -528,6 +615,10 @@ export function getMessageAreaStyles(): string {
border-radius: 4px; border-radius: 4px;
font-size: 12px;} font-size: 12px;}
${getAgentCardStyles()}
${getPlanCardStyles()}
${getWaveformPreviewContent()} ${getWaveformPreviewContent()}
`; `;
} }
@ -540,18 +631,75 @@ export function getMessageAreaScript(): string {
// 工具图标定义 // 工具图标定义
const collapseIconSvg = \`${collapseIconSvg}\`; const collapseIconSvg = \`${collapseIconSvg}\`;
const fileWriteIconSvg = \`${fileWriteIconSvg}\`; const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
const fileReadIconSvg = \`${fileReadIconSvg}\`;
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`; const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
const searchCodeIconSvg = \`${SearchCode}\`; const searchCodeIconSvg = \`${SearchCode}\`;
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
const simulationIconSvg = \`${simulationIconSvg}\`;
const waveformIconSvg = \`${waveformIconSvg}\`;
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
${getAgentCardScript()}
${getPlanCardScript()}
// 获取工具图标
function getToolIcon(toolName) {
const iconMap = {
'file_read': fileReadIconSvg,
'file_write': fileWriteIconSvg,
'file_delete': fileDeleteIconSvg,
'file_list': searchCodeIconSvg,
'syntax_check': syntaxCheckIconSvg,
'simulation': simulationIconSvg,
'waveform_summary': waveformIconSvg,
'knowledge_save': saveKnowledgeIconSvg,
'knowledge_load': knowledgeLoadIconSvg,
'queryKnowledgeSummary': knowledgeLoadIconSvg,
'queryRules': knowledgeLoadIconSvg,
'setModule': fileWriteIconSvg,
'addSignal': fileWriteIconSvg,
'addSignalExample': fileWriteIconSvg,
'validateKnowledgeGraph': syntaxCheckIconSvg,
'querySignals': searchCodeIconSvg,
'addPlan': fileWriteIconSvg,
'addEdge': fileWriteIconSvg,
'showPlan': searchCodeIconSvg,
'addRule': fileWriteIconSvg,
'updateNode': fileWriteIconSvg,
'addStateTransition': stateTransitionIconSvg
};
return iconMap[toolName] || '';
}
// 工具名称映射 // 工具名称映射
function getToolDisplayName(toolName) { function getToolDisplayName(toolName) {
const toolNameMap = { const toolNameMap = {
'file_read': '已完成文件读取', 'file_read': '已完成文件读取',
'file_write': '已完成文件写入', 'file_write': '已完成文件写入',
'file_delete': '已完成文件删除',
'file_list': '已检索代码文件', 'file_list': '已检索代码文件',
'syntax_check': '已完成语法检查', 'syntax_check': '已完成语法检查',
'simulation': '已完成仿真', 'simulation': '已完成仿真',
'waveform_summary': '已完成波形分析' 'waveform_summary': '已完成波形分析',
'knowledge_save': '已保存知识库',
'knowledge_load': '已加载知识库',
'queryKnowledgeSummary': '已查询知识摘要',
'queryRules': '已查询规则',
'setModule': '已设置模块',
'addSignal': '已添加信号',
'addSignalExample': '已添加信号示例',
'validateKnowledgeGraph': '已验证知识图谱',
'querySignals': '已查询信号',
'addPlan': '已添加计划',
'addEdge': '已添加边',
'showPlan': '已显示计划',
'addRule': '已添加规则',
'updateNode': '已更新节点',
'addStateTransition': '已添加状态转换',
'spawnExplorer': '代码探索'
}; };
return toolNameMap[toolName] || toolName; return toolNameMap[toolName] || toolName;
} }
@ -742,6 +890,9 @@ export function getMessageAreaScript(): string {
// 存储已回答问题的状态 // 存储已回答问题的状态
const answeredQuestions = new Map(); // askId -> answer const answeredQuestions = new Map(); // askId -> answer
// 存储工具展开/折叠状态
const toolCollapseStates = new Map(); // index -> isCollapsed
// 实时更新分段消息(按后端返回顺序) // 实时更新分段消息(按后端返回顺序)
function updateSegmentsRealtime(segments, isComplete) { function updateSegmentsRealtime(segments, isComplete) {
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete); console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
@ -773,9 +924,19 @@ export function getMessageAreaScript(): string {
messagesEl.appendChild(currentSegmentedMessage); messagesEl.appendChild(currentSegmentedMessage);
} }
// 保存当前所有工具的展开/折叠状态
if (currentSegmentedMessage) {
const toolHeaders = currentSegmentedMessage.querySelectorAll('.tool-segment-header[data-collapsible="true"]');
toolHeaders.forEach((header, idx) => {
const isCollapsed = header.classList.contains('collapsed');
toolCollapseStates.set(idx, isCollapsed);
});
}
// 清空容器并重新渲染所有段落 // 清空容器并重新渲染所有段落
currentSegmentedMessage.innerHTML = ''; currentSegmentedMessage.innerHTML = '';
let toolIndex = 0; // 用于跟踪工具段落的索引
segments.forEach((segment, index) => { segments.forEach((segment, index) => {
const segmentDiv = document.createElement('div'); const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type; segmentDiv.className = 'message-segment segment-' + segment.type;
@ -784,22 +945,29 @@ export function getMessageAreaScript(): string {
segmentDiv.className += ' segment-text'; segmentDiv.className += ' segment-text';
segmentDiv.innerHTML = formatText(segment.content); segmentDiv.innerHTML = formatText(segment.content);
} else if (segment.type === 'tool') { } else if (segment.type === 'tool') {
// 过滤掉不需要显示的工具
if (segment.toolName === 'spawnExplorer') {
return;
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
// 检查工具结果是否过长(超过一行显示不下) // 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60; const shouldCollapse = toolResult && toolResult.length > 60;
// 恢复之前保存的展开/折叠状态
const savedState = toolCollapseStates.get(toolIndex);
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
const currentToolIndex = toolIndex;
toolIndex++; // 递增工具索引
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}"> <div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
\${shouldCollapse ? collapseIconSvg : ''} \${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span> <span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''} \${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div> </div>
\${shouldCollapse ? \`<div class="tool-segment-content collapsed"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''} \${shouldCollapse ? \`<div class="tool-segment-content\${isCollapsed ? ' collapsed' : ''}" style="max-height:\${isCollapsed ? '0' : 'none'}"><span class="tool-segment-result" style="display:block;white-space:pre-wrap;max-width:100%;margin-top:8px;margin-left:18px;">\${toolResult}</span></div>\` : ''}
\`; \`;
// 如果是仿真工具且成功完成,尝试添加波形预览 // 如果是仿真工具且成功完成,尝试添加波形预览
@ -825,27 +993,24 @@ export function getMessageAreaScript(): string {
setTimeout(() => { setTimeout(() => {
const header = segmentDiv.querySelector('.tool-segment-header'); const header = segmentDiv.querySelector('.tool-segment-header');
const content = segmentDiv.querySelector('.tool-segment-content'); const content = segmentDiv.querySelector('.tool-segment-content');
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
if (header && content) { if (header && content) {
header.addEventListener('click', function() { header.addEventListener('click', function() {
const isCollapsed = header.classList.contains('collapsed'); const isCollapsed = header.classList.contains('collapsed');
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
if (isCollapsed) { if (isCollapsed) {
// 展开 // 展开
header.classList.remove('collapsed'); header.classList.remove('collapsed');
content.classList.remove('collapsed'); content.classList.remove('collapsed');
content.style.maxHeight = content.scrollHeight + 'px'; content.style.maxHeight = content.scrollHeight + 'px';
if (iconCollapsed) iconCollapsed.style.display = 'none'; toolCollapseStates.set(toolIdx, false);
if (iconExpanded) iconExpanded.style.display = 'block';
} else { } else {
// 折叠 // 折叠
header.classList.add('collapsed'); header.classList.add('collapsed');
content.classList.add('collapsed'); content.classList.add('collapsed');
content.style.maxHeight = '0'; content.style.maxHeight = '0';
if (iconCollapsed) iconCollapsed.style.display = 'block'; toolCollapseStates.set(toolIdx, true);
if (iconExpanded) iconExpanded.style.display = 'none';
} }
}); });
} }
@ -916,6 +1081,12 @@ export function getMessageAreaScript(): string {
} }
}, 0); }, 0);
} }
} else if (segment.type === 'agent') {
// 智能体卡片渲染
renderAgentCard(segment, segmentDiv);
} else if (segment.type === 'plan') {
// 计划卡片渲染(使用独立组件)
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
} }
currentSegmentedMessage.appendChild(segmentDiv); currentSegmentedMessage.appendChild(segmentDiv);
@ -926,9 +1097,11 @@ export function getMessageAreaScript(): string {
console.log('[WebView] 对话完成,添加操作按钮'); console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div'); const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions'; actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button'); const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn'; copyBtn.className = 'action-btn';
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'; copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
copyBtn.onclick = () => { copyBtn.onclick = () => {
const textContent = segments const textContent = segments
.filter(s => s.type === 'text' && s.content) .filter(s => s.type === 'text' && s.content)
@ -936,7 +1109,22 @@ export function getMessageAreaScript(): string {
.join('\\n'); .join('\\n');
copyMessage(textContent, copyBtn); copyMessage(textContent, copyBtn);
}; };
// 点赞按钮
const likeBtn = document.createElement('button');
likeBtn.className = 'action-btn';
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
likeBtn.onclick = () => toggleLike(likeBtn);
// 点踩按钮
const dislikeBtn = document.createElement('button');
dislikeBtn.className = 'action-btn';
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
actionsDiv.appendChild(copyBtn); actionsDiv.appendChild(copyBtn);
actionsDiv.appendChild(likeBtn);
actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv); currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器 // 重置当前分段消息容器
@ -982,6 +1170,10 @@ export function getMessageAreaScript(): string {
segmentDiv.className += ' segment-text'; segmentDiv.className += ' segment-text';
segmentDiv.innerHTML = formatText(segment.content); segmentDiv.innerHTML = formatText(segment.content);
} else if (segment.type === 'tool') { } else if (segment.type === 'tool') {
// 过滤掉不需要显示的工具
if (segment.toolName === 'spawnExplorer') {
return;
}
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧'; const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || ''; const toolResult = segment.toolResult || '';
@ -990,10 +1182,7 @@ export function getMessageAreaScript(): string {
segmentDiv.innerHTML = \` segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}"> <div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? collapseIconSvg : ''} \${shouldCollapse ? \`<span class="icon-collapsed" style="display:block;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span><span class="icon-expanded" style="display:none;width:16px;height:16px;flex-shrink:0;">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_write' ? fileWriteIconSvg : ''}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'syntax_check' ? syntaxCheckIconSvg : ''}
\${!shouldCollapse && segment.toolStatus === 'success' && segment.toolName === 'file_list' ? searchCodeIconSvg : ''}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span> <span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''} \${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</div> </div>
@ -1023,27 +1212,24 @@ export function getMessageAreaScript(): string {
setTimeout(() => { setTimeout(() => {
const header = segmentDiv.querySelector('.tool-segment-header'); const header = segmentDiv.querySelector('.tool-segment-header');
const content = segmentDiv.querySelector('.tool-segment-content'); const content = segmentDiv.querySelector('.tool-segment-content');
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
if (header && content) { if (header && content) {
header.addEventListener('click', function() { header.addEventListener('click', function() {
const isCollapsed = header.classList.contains('collapsed'); const isCollapsed = header.classList.contains('collapsed');
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
if (isCollapsed) { if (isCollapsed) {
// 展开 // 展开
header.classList.remove('collapsed'); header.classList.remove('collapsed');
content.classList.remove('collapsed'); content.classList.remove('collapsed');
content.style.maxHeight = content.scrollHeight + 'px'; content.style.maxHeight = content.scrollHeight + 'px';
if (iconCollapsed) iconCollapsed.style.display = 'none'; toolCollapseStates.set(toolIdx, false);
if (iconExpanded) iconExpanded.style.display = 'block';
} else { } else {
// 折叠 // 折叠
header.classList.add('collapsed'); header.classList.add('collapsed');
content.classList.add('collapsed'); content.classList.add('collapsed');
content.style.maxHeight = '0'; content.style.maxHeight = '0';
if (iconCollapsed) iconCollapsed.style.display = 'block'; toolCollapseStates.set(toolIdx, true);
if (iconExpanded) iconExpanded.style.display = 'none';
} }
}); });
} }
@ -1058,6 +1244,12 @@ export function getMessageAreaScript(): string {
</div> </div>
</div> </div>
\`; \`;
} else if (segment.type === 'agent') {
// 智能体卡片渲染
renderAgentCard(segment, segmentDiv);
} else if (segment.type === 'plan') {
// 计划卡片渲染(使用独立组件)
renderPlanCardStatic(segment, segmentDiv);
} }
container.appendChild(segmentDiv); container.appendChild(segmentDiv);
@ -1066,9 +1258,11 @@ export function getMessageAreaScript(): string {
// 添加操作按钮 // 添加操作按钮
const actionsDiv = document.createElement('div'); const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions'; actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button'); const copyBtn = document.createElement('button');
copyBtn.className = 'action-btn'; copyBtn.className = 'action-btn';
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'; copyBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M761.088 715.3152a38.7072 38.7072 0 0 1 0-77.4144 37.4272 37.4272 0 0 0 37.4272-37.4272V265.0112a37.4272 37.4272 0 0 0-37.4272-37.4272H425.6256a37.4272 37.4272 0 0 0-37.4272 37.4272 38.7072 38.7072 0 1 1-77.4144 0 115.0976 115.0976 0 0 1 114.8416-114.8416h335.4624a115.0976 115.0976 0 0 1 114.8416 114.8416v335.4624a115.0976 115.0976 0 0 1-114.8416 114.8416z" fill="currentColor"/><path d="M589.4656 883.0976H268.1856a121.1392 121.1392 0 0 1-121.2928-121.2928v-322.56a121.1392 121.1392 0 0 1 121.2928-121.344h321.28a121.1392 121.1392 0 0 1 121.2928 121.2928v322.56c1.28 67.1232-54.1696 121.344-121.2928 121.344zM268.1856 395.3152a43.52 43.52 0 0 0-43.8784 43.8784v322.56a43.52 43.52 0 0 0 43.8784 43.8784h321.28a43.52 43.52 0 0 0 43.8784-43.8784v-322.56a43.52 43.52 0 0 0-43.8784-43.8784z" fill="currentColor"/></svg><span class="action-tooltip">复制</span>\`;
copyBtn.onclick = () => { copyBtn.onclick = () => {
const textContent = segments const textContent = segments
.filter(s => s.type === 'text' && s.content) .filter(s => s.type === 'text' && s.content)
@ -1076,7 +1270,22 @@ export function getMessageAreaScript(): string {
.join('\\n'); .join('\\n');
copyMessage(textContent, copyBtn); copyMessage(textContent, copyBtn);
}; };
// 点赞按钮
const likeBtn = document.createElement('button');
likeBtn.className = 'action-btn';
likeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M923.5 411.2c-28.6-33.9-72.1-53-116.4-51.1h-68c6.4-31.6 10.1-63.9 11.2-96v-0.8c-0.5-60.9-18.7-112-51.2-144-22.6-22.2-50.8-33.7-81.5-33.3-38.3 0-69.1 11.5-91.7 34.2-26.5 26.5-39.9 66.8-39.8 119.6 0.1 40.1-19.4 83.4-52.1 115.9-32 31.8-71.7 49.3-111.8 49.3H295.6c-3 0-6 0.3-8.9 0.8v-1.2H140.8c-39.7 0-72.2 32.5-72.2 72.2v392.9c0 39.7 32.5 72.2 72.2 72.2h146.8v-0.6c2.9 0.4 5.9 0.7 8.9 0.7h464.7c33.3-0.8 65.6-13 91.1-34.4s43.1-51.1 49.6-83.8l52.3-289.1c9.4-43.4-2.1-89.6-30.7-123.5zM147.7 843.7v-344c0-9 7.3-16.3 16.3-16.3h70.4V860H164c-9 0-16.3-7.3-16.3-16.3z m726.4-324.9l-0.2 0.6-51.7 290.3c-6.7 29.1-32.3 50.2-62.2 51.3l-4.9 0.2-0.4 0.3h-440V486h7.3c61.4 0 121-25.7 168.1-72.4 48.6-48.2 76.5-111.7 76.5-174.2-0.1-31.5 4.9-51.8 15.3-62.2 7.4-7.4 18.7-10.8 35.8-10.8h0.2c9-0.1 17.4 3.6 24.9 11 16.3 16.2 25.7 47.3 25.8 85.4-1.2 41.8-7.9 83.3-19.9 123.4l-21.6 54.3h181.5c24.5-0.6 48.2 8.9 65.1 26.1 16.9 17.2 25.3 40.8 23 64.9z" fill="currentColor"/></svg><span class="action-tooltip">点赞</span>\`;
likeBtn.onclick = () => toggleLike(likeBtn);
// 点踩按钮
const dislikeBtn = document.createElement('button');
dislikeBtn.className = 'action-btn';
dislikeBtn.innerHTML = \`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M360 640c60.992 40.107 88 87.381 88 149.333 0 5.611 0.427 12.864 1.579 21.462a174.933 174.933 0 0 0 11.2 42.666c19.584 48.192 60.864 83.008 119.018 85.12 28.843 2.56 60.886-3.584 91.414-25.045 58.709-41.237 81.706-117.248 69.973-230.87h48.15V640v42.667c17.173 0 38.4-2.475 61.823-10.667 66.304-23.125 107.627-84.117 84.928-162.816l-53.674-254.55a213.333 213.333 0 0 0-208.747-169.3H207.019a85.333 85.333 0 0 0-85.078 78.783l-29.546 384A85.333 85.333 0 0 0 177.493 640H360z m-61.333-109.333v24H177.493l29.526-384h91.648v360z m85.333-360h289.664a128 128 0 0 1 125.227 101.589l54.442 258.07c21.334 67.007-64 67.007-64 67.007H640c64 277.334-54.613 256-54.613 256-52.054 0-52.054-64-52.054-64 0-92.8-43.264-167.082-129.77-222.805A42.667 42.667 0 0 1 384 530.667v-360z" fill="currentColor"/></svg><span class="action-tooltip">点踩</span>\`;
dislikeBtn.onclick = () => toggleDislike(dislikeBtn);
actionsDiv.appendChild(copyBtn); actionsDiv.appendChild(copyBtn);
actionsDiv.appendChild(likeBtn);
actionsDiv.appendChild(dislikeBtn);
container.appendChild(actionsDiv); container.appendChild(actionsDiv);
messagesEl.appendChild(container); messagesEl.appendChild(container);

View File

@ -5,7 +5,12 @@
/** /**
* 获取模型选择器的 HTML 内容 * 获取模型选择器的 HTML 内容
*/ */
export function getModelSelectorContent(): string { export function getModelSelectorContent(
autoIcon: string = '',
liteIcon: string = '',
syIcon: string = '',
maxIcon: string = ''
): string {
return ` return `
<!-- 模型选择 --> <!-- 模型选择 -->
<div class="tooltip"> <div class="tooltip">
@ -17,10 +22,22 @@ export function getModelSelectorContent(): string {
</svg> </svg>
</div> </div>
<div class="select-dropdown" id="modelDropdown"> <div class="select-dropdown" id="modelDropdown">
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">Lite</div> <div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">
<div class="select-option selected" data-value="auto" data-tooltip="自动选择最佳模型" onclick="selectModel('auto', 'Auto')">Auto</div> ${autoIcon ? `<img src="${autoIcon}" class="model-icon" alt="Auto">` : ''}
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div> <span class="option-label">Auto</span>
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div> </div>
<div class="select-option" data-value="lite" data-tooltip="快速响应,适合简单任务" onclick="selectModel('lite', 'Lite')">
${liteIcon ? `<img src="${liteIcon}" class="model-icon" alt="Lite">` : ''}
<span class="option-label">Lite</span>
</div>
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">
${syIcon ? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">` : ''}
<span class="option-label">Syntaxic</span>
</div>
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">
${maxIcon ? `<img src="${maxIcon}" class="model-icon" alt="Max">` : ''}
<span class="option-label">Max</span>
</div>
</div> </div>
<!-- 模型选择器的 tooltip 容器 --> <!-- 模型选择器的 tooltip 容器 -->
<div id="modelTooltip" class="model-tooltip"></div> <div id="modelTooltip" class="model-tooltip"></div>
@ -88,10 +105,12 @@ export function getModelSelectorStyles(): string {
#modelDropdown .select-option { #modelDropdown .select-option {
position: relative; position: relative;
padding: 6px 12px; padding: 6px 12px;
font-size: 12px;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
white-space: nowrap; display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
} }
#modelDropdown .select-option:hover { #modelDropdown .select-option:hover {
background: rgba(128, 128, 128, 0.3); background: rgba(128, 128, 128, 0.3);
@ -100,6 +119,17 @@ export function getModelSelectorStyles(): string {
background: rgba(128, 128, 128, 0.5); background: rgba(128, 128, 128, 0.5);
color: var(--vscode-foreground); color: var(--vscode-foreground);
} }
.model-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
object-fit: contain;
}
.option-label {
font-size: 12px;
color: var(--vscode-foreground);
white-space: nowrap;
}
/* 模型选择器的 tooltip 样式 */ /* 模型选择器的 tooltip 样式 */
.model-tooltip { .model-tooltip {
position: fixed; position: fixed;

273
src/views/planCard.ts Normal file
View File

@ -0,0 +1,273 @@
/**
* 计划卡片组件
*
* 功能说明:
* - 显示执行计划的卡片界面
* - 包含计划标题、摘要和步骤列表
* - 提供确认执行、修改计划、取消等操作按钮
*/
import { plannerIconSvg } from "../constants/toolIcons";
/**
* 获取计划卡片的样式
*/
export function getPlanCardStyles(): string {
return `
/* 计划卡片样式 */
.segment-plan {
margin: 12px 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: 10px;
padding: 12px 16px;
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: 16px;
}
.plan-summary {
color: var(--vscode-descriptionForeground);
margin-bottom: 12px;
font-size: 13px;
line-height: 1.5;
}
.plan-steps {
font-size: 13px;
}
.plan-step {
padding: 8px 12px;
margin-bottom: 6px;
background: var(--vscode-list-hoverBackground);
border-radius: 4px;
line-height: 1.5;
}
.plan-step:last-child {
margin-bottom: 0;
}
.step-num {
color: var(--vscode-textLink-foreground);
font-weight: 500;
margin-right: 6px;
}
.plan-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
border-top: 1px solid var(--vscode-input-border);
background: var(--vscode-sideBar-background);
}
.plan-actions .question-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.plan-btn {
padding: 8px 18px;
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);
}
.plan-actions .custom-input-container {
display: flex;
gap: 8px;
width: 100%;
}
.plan-actions .custom-input {
flex: 1;
padding: 8px 12px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 13px;
}
.plan-actions .custom-submit {
padding: 8px 18px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.plan-actions .custom-submit:hover {
background: var(--vscode-button-hoverBackground);
}
`;
}
/**
* 获取计划卡片的脚本
*/
export function getPlanCardScript(): string {
return `
// 渲染计划卡片(在 updateSegmentsRealtime 中使用)
function renderPlanCardInSegment(segment, segmentDiv, answeredQuestions) {
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">${plannerIconSvg}</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);
}
}
// 渲染计划卡片(在 renderSegments 中使用)
function renderPlanCardStatic(segment, segmentDiv) {
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);
}
`;
}

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;
// 绘制波形 // 绘制波形
if (signal.width === 1) {
// 单比特信号 - 绘制数字波形
let pathData = ''; let pathData = '';
let lastX = leftMargin; const yHigh = y;
let lastValue = signal.values[0].value; 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;
} 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 lastValue = signal.values[signal.values.length - 1].value;
const lastY = (lastValue === '1') ? y : (y + signalHeight); const lastY = (lastValue === '1') ? yHigh : yLow;
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

@ -17,10 +17,17 @@ import {
getMessageAreaStyles, getMessageAreaStyles,
getMessageAreaScript, getMessageAreaScript,
} from "./messageArea"; } from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
*/ */
export function getWebviewContent(iconUri?: string): string { export function getWebviewContent(
iconUri?: string,
autoIconUri?: string,
liteIconUri?: string,
syIconUri?: string,
maxIconUri?: string
): string {
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@ -65,6 +72,7 @@ export function getWebviewContent(iconUri?: string): string {
padding: 0 20px 20px 20px; padding: 0 20px 20px 20px;
} }
${getMessageAreaStyles()} ${getMessageAreaStyles()}
${getAgentCardStyles()}
${getWaveformPreviewContent()} ${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()} ${getConversationHistoryBarStyles()}
${getInputAreaStyles()} ${getInputAreaStyles()}
@ -250,7 +258,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;
@ -389,13 +397,14 @@ export function getWebviewContent(iconUri?: string): string {
<span id="statusText">思考中...</span> <span id="statusText">思考中...</span>
</div> </div>
<div class="quick-actions"> <!-- <div class="quick-actions">
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button> <button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button> <button class="quick-btn" onclick="quickAction('fsm')">生成状态机</button>
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button> <button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
</div> <button class="quick-btn" onclick="quickAction('explore')">知识探索</button>
</div> -->
${getInputAreaContent()} ${getInputAreaContent(autoIconUri, liteIconUri, syIconUri, maxIconUri)}
</div> </div>
<script> <script>
@ -411,11 +420,68 @@ 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位同步计数器',
fsm: '生成一个状态机', fsm: '生成一个状态机',
testbench: '生成测试平台' testbench: '生成测试平台',
explore: '请启动知识探索智能体,分析当前项目结构'
}; };
if (questions[type]) { if (questions[type]) {
messageInput.value = questions[type]; messageInput.value = questions[type];
@ -436,8 +502,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;
@ -504,11 +568,32 @@ export function getWebviewContent(iconUri?: string): string {
} }
break; break;
case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器');
currentSegmentedMessage = null;
break;
case 'contextUsage':
// 更新上下文使用量显示
if (typeof updateContextDisplay === 'function') {
updateContextDisplay(message.currentTokens, message.maxTokens);
}
break;
case 'hideLoading': case 'hideLoading':
// 隐藏加载指示器 // 隐藏加载指示器
hideLoadingIndicator(); hideLoadingIndicator();
break; break;
case 'workspaceStatus':
// 更新工作区状态
if (typeof hasWorkspace !== 'undefined') {
hasWorkspace = message.hasWorkspace;
console.log('[WebView] 工作区状态:', hasWorkspace);
}
break;
case 'vcdInfo': case 'vcdInfo':
// 渲染迷你波形预览信息 // 渲染迷你波形预览信息
try { try {
@ -581,12 +666,34 @@ 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);
} }
}); });
${getMessageAreaScript()} ${getMessageAreaScript()}
${getAgentCardScript()}
${getWaveformPreviewScript()} ${getWaveformPreviewScript()}
${getConversationHistoryBarScript()} ${getConversationHistoryBarScript()}
${getInputAreaScript()} ${getInputAreaScript()}