77 Commits

Author SHA1 Message Date
5c19be22d3 feat: 实现计划管理工具和进度条实时更新
- 添加 plan_step_add/remove/update 和 plan_summary_update 事件支持
- 添加 onPhaseProgress 回调,联动独立进度条组件
- 扩展 MessageSegment 接口支持 progress 类型
- 映射 phaseId (sim -> simulation) 适配进度条
2026-01-09 19:26:55 +08:00
5546791549 feat: Plan卡片支持Markdown渲染和智能步骤解析
- 添加renderPlanMarkdown函数,支持标题、列表、表格、代码块等
- 添加renderPlanSteps函数,智能解析JSON格式步骤对象
- 步骤显示模块名、描述、输入输出、逻辑等详细信息
- 添加plan-summary和step-details样式
2026-01-09 17:02:00 +08:00
178f3a7498 feat: 从JWT解析userId并添加资源点余额提醒
- 新增 jwtUtils.ts 解析JWT token获取user_id
- dialogService 从登录session获取真实userId
- 添加 credit_update 事件处理
- 余额低于5点时弹窗提醒用户充值
- settings.ts 登录URL改为可配置
2026-01-09 15:53:54 +08:00
4037e9e2d7 style:调整对话样式 2026-01-08 20:25:51 +08:00
4b2f6967dc style:优化了预览波形的样式 2026-01-08 18:18:51 +08:00
79ef879b97 Merge branch 'feat/back-to-front' into feature/waveform-renderer 2026-01-08 17:26:29 +08:00
1df7462778 docs: 添加数据流程详解文档 + fix: 修复消息渲染逻辑
- 新增完整的数据流程文档,详细说明从用户输入到响应显示的全流程
   - 修复 messageArea.ts 中的消息渲染逻辑:
     - 移除用户消息时重置分段容器的逻辑
     - 移除对话完成时跳过 segments 处理的逻辑
     - 确保对话完成时正确渲染最终的 segments
2026-01-08 17:24:36 +08:00
0bcdc615e3 style:对话界面的样式优化
- 代码高亮
- 间距调整
- 工具调用的样式调整
2026-01-08 16:10:41 +08:00
5577fe17bb fix:解决用户消息错位的bug + 解决内容重复展示的bug 2026-01-08 15:27:14 +08:00
820ee2f848 feat:实现预览波形点击展开会显示完整波形 2026-01-07 19:02:00 +08:00
be8365c8cb feature: 实现点击 VCD 文件时 Surfer 显示波形
- VCDViewerEditorProvider 现在接收并持有 vcdFileServer 实例
   - createFromWebviewPanel 方法传递 vcdFileServer 参数
   - 确保自定义编辑器打开 VCD 文件时能够通过 HTTP 服务器加载波形数据
2026-01-07 17:46:09 +08:00
b1dd2442b8 feat:surfer替换vcdroom 2026-01-07 17:30:34 +08:00
9281d1d724 feat: 支持服务等级动态切换
- 添加 ServiceTier 类型定义
- 修改 dialogService 接收 serviceTier 参数
- 修改 messageHandler 传递 serviceTier 参数
- 修改 ICHelperPanel 传递 UI 选择的服务等级
2026-01-07 16:13:56 +08:00
226bb46094 feat:换到测试服务器上 2026-01-05 19:31:28 +08:00
251289a340 Merge branch 'feat/plugin-front-end' into merge/250105merge 2026-01-05 19:08:27 +08:00
c22081c5e9 feat: 处理后端 heartbeat 事件,保持 SSE 连接活跃 2026-01-05 19:04:04 +08:00
cca82c7885 feat:将todo的需要改为勾选框
- 为后续的todo完成打勾做准备
2026-01-05 18:30:59 +08:00
e4ff49bade chore: 添加 vcdParser.ts (未使用,保留备用)
纯 TypeScript 实现的 VCD 解析器,当前未使用。
目前使用 waveformTracer.ts 调用 Python 打包的 exe。
2026-01-05 18:29:49 +08:00
ada4806493 feat: 集成 waveform_trace 波形调试工具
新增功能:
- waveformTracer.ts: 调用 waveform_trace.exe 的工具实现
- toolExecutor.ts: 添加 waveform_trace 工具分发
- types/api.ts: 添加 WaveformTraceArgs 类型定义

工具源码 (tools/waveform_trace/src/):
- AST 解析 + BFS 信号追踪
- VCD 波形解析
- 修复通用 testbench 支持

配置文件:
- .gitignore: 排除 exe 和打包产物
- .vscodeignore: 发布时排除源码
- build.bat/build.sh: 打包脚本
2026-01-05 18:18:57 +08:00
3831de2849 fix: 修复 ICViewProvider 中的事件监听器内存泄漏问题
将 webview.onDidReceiveMessage 监听器添加到 context.subscriptions 中,
   确保在扩展停用时能够正确清理,避免潜在的内存泄漏。
2026-01-05 16:40:32 +08:00
0df529c4fd feat:实现思考的组件 2026-01-05 16:25:47 +08:00
5c53d7f0e9 feat:修改模式内容和增加icon 2026-01-05 16:22:52 +08:00
ef2a0dc16e feat:修改模型描述的展现形式和内容 2026-01-05 16:19:53 +08:00
5ce420295b feat:解决图片没有被打包进去的bug 2026-01-05 16:12:15 +08:00
1d7f3d7626 feat:添加上下文功能实现 2026-01-05 15:59:26 +08:00
9b0d2d5e01 feat:进度条收起的功能和发起对话才展示 2026-01-05 15:27:40 +08:00
27e3351b55 feat:输入框居中展示
- 点击历史记录和发起对话之后回到底部
2026-01-05 15:18:03 +08:00
de3e84aa4e feat:顶部添加进度条 2026-01-05 11:27:06 +08:00
e48e822d07 fix: 修复 taskId 不一致导致 conversation.json 找不到的问题
- messageHandler 复用 historyManager 的 taskId 而非重新生成
- 环境切换为 dev,超时时间统一为 5 分钟
- agentCard 添加调试智能体相关工具名称映射
- 移除冗余的 segments 调试日志
2026-01-05 10:15:25 +08:00
8dc34ee435 feat:让用户看不懂的工具隐晦展示 2026-01-04 16:29:13 +08:00
d8cd86361e feat: 添加获取当前环境的功能以控制快速操作按钮的显示 2026-01-04 14:10:43 +08:00
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
235 changed files with 70085 additions and 772 deletions

11
.gitignore vendored
View File

@ -3,3 +3,14 @@ dist
node_modules
.vscode-test/
*.vsix
# waveform_trace 打包产物exe 太大,通过 Release 发布)
tools/waveform_trace/bin/
tools/waveform_trace/src/build/
tools/waveform_trace/src/dist/
tools/waveform_trace/src/*.spec
# Python 缓存
__pycache__/
*.pyc
*.pyo

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>

29
.vscodeignore Normal file
View File

@ -0,0 +1,29 @@
# 排除开发文件
.vscode/**
.git/**
.gitignore
node_modules/**
src/**
**/*.ts
**/*.map
# 排除测试文件
test/**
**/*.test.js
# 排除文档
*.md
!README.md
# 排除 waveform_trace Python 源码(只保留 exe
tools/waveform_trace/src/**
tools/waveform_trace/build/**
tools/waveform_trace/dist/**
tools/waveform_trace/build.bat
tools/waveform_trace/build.sh
# 排除打包临时文件
**/__pycache__/**
**/*.pyc
**/*.pyo
**/*.spec

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其他平台需用户自行安装

1027
docs/数据流程详解.md Normal file

File diff suppressed because it is too large Load Diff

200
media/surfer/index.html Normal file
View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>Surfer</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<script type="module">
import init from '/surfer.js';
await init({module_or_path: '/surfer_bg.wasm'});
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
/*SURFER_SETUP_HOOKS*/
</script>
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base href="/" />
<script>
function on_surfer_error(msg) {
console.log("Setting error message")
document.getElementById("error_message").innerHTML = msg
document.getElementById("error_container").style.display = "block"
}
window.on_surfer_error = on_surfer_error;
</script>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#error_container {
padding: 1em;
border-radius: 0.5em;
margin: 0px auto;
max-width: 980px;
color: #ffffff;
background-color: black;
position: relative;
height: 90%;
overflow: scroll;
}
#error_container a {
color: #ff9999;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
}
</style>
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<div id="error_container" style="display: none;">
<h1>Sorry, Surfer crashed 🔥</h1>
<p>
Something caused Surfer to crash. Please report the error on
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
gitlab
</a>
</p>
<p>
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
the crash and/or the steps to reproduce the crash.
</p>
<h3>
Backtrace:
</h3>
<div class="error_container">
<!-- This is filled in by javascript -->
<code id="error_message"></code>
</div>
</div>
<!-- Register the message listener system -->
<script src="integration.js"></script>
<script>
register_message_listener()
</script>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

View File

@ -0,0 +1,65 @@
// Web apps which integrate Surfer as an iframe can give commands to surfer via
// the .postMessage [1] function on the iframe.
//
// For example, to tell Surfer to load waveforms from a URL, use
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
//
// For more complex functionality, one can also inject any `Message` defined
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
// is not stable and may change at any time. If you add functionality via
// these, make sure to test the new functionality when changing Surfer version.
//
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
function register_message_listener() {
window.addEventListener("message", (event) => {
// JSON decode the message
const decoded = event.data
switch (decoded.command) {
// Load a waveform from a URL. The format is inferred from the data.
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
case 'LoadUrl': {
const msg = {
LoadWaveformFileFromUrl: [
decoded.url,
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
case 'ToggleMenu': {
const msg = "ToggleMenu"
inject_message(JSON.stringify(msg))
break;
}
// Load waveform data directly from string content
case 'LoadData': {
const msg = {
LoadFromData: [
decoded.content,
decoded.fileName || "waveform.vcd",
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
// Inject any other message supported by Surfer in the surfer::Message enum.
// NOTE: The API of these is unstable.
case 'InjectMessage': {
inject_message(decoded.message);
break
}
default:
console.log(`Unknown message.command ${decoded.command}`)
break;
}
});
}

View File

@ -0,0 +1,10 @@
{
"background_color": "white",
"display": "standalone",
"id": "/index.html",
"lang": "en-US",
"name": "Surfer",
"short_name": "surfer",
"start_url": "./index.html",
"theme_color": "white"
}

2227
media/surfer/surfer.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>Surfer</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<script type="module">
import init from '/surfer.js';
await init({module_or_path: '/surfer_bg.wasm'});
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '/surfer.js';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
/*SURFER_SETUP_HOOKS*/
</script>
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base href="/" />
<script>
function on_surfer_error(msg) {
console.log("Setting error message")
document.getElementById("error_message").innerHTML = msg
document.getElementById("error_container").style.display = "block"
}
window.on_surfer_error = on_surfer_error;
</script>
<link rel="manifest" href="manifest.json">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#error_container {
padding: 1em;
border-radius: 0.5em;
margin: 0px auto;
max-width: 980px;
color: #ffffff;
background-color: black;
position: relative;
height: 90%;
overflow: scroll;
}
#error_container a {
color: #ff9999;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
}
</style>
<link rel="modulepreload" href="/surfer.js" crossorigin="anonymous" integrity="sha384-s5jcnzgSMjwjfa1Jq5kr3vQVXGQ7D+ZdMsCBdbbcmKefqvRKw652YAYaaHZJQob6"><link rel="preload" href="/surfer_bg.wasm" crossorigin="anonymous" integrity="sha384-YzYZZQJDXiKIAVpyBMziailnMHJ/sxzBq0VNMP854yLbTd2lneCR5ZgcvB4cYMFc" as="fetch" type="application/wasm"></head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<div id="error_container" style="display: none;">
<h1>Sorry, Surfer crashed 🔥</h1>
<p>
Something caused Surfer to crash. Please report the error on
<a href="https://gitlab.com/surfer-project/surfer/-/issues/new">
gitlab
</a>
</p>
<p>
Any report is appreciated, but it is extra helpful if you can attach the waveform that caused
the crash and/or the steps to reproduce the crash.
</p>
<h3>
Backtrace:
</h3>
<div class="error_container">
<!-- This is filled in by javascript -->
<code id="error_message"></code>
</div>
</div>
<!-- Register the message listener system -->
<script src="integration.js"></script>
<script>
register_message_listener()
</script>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

View File

@ -0,0 +1,52 @@
// Web apps which integrate Surfer as an iframe can give commands to surfer via
// the .postMessage [1] function on the iframe.
//
// For example, to tell Surfer to load waveforms from a URL, use
// `.postMessage({command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"})`
//
// For more complex functionality, one can also inject any `Message` defined
// in `surfer::Message` in surfer/main.rs. However, the API of these messages
// is not stable and may change at any time. If you add functionality via
// these, make sure to test the new functionality when changing Surfer version.
//
// [1] https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
function register_message_listener() {
window.addEventListener("message", (event) => {
// JSON decode the message
const decoded = event.data
switch (decoded.command) {
// Load a waveform from a URL. The format is inferred from the data.
// Example: `{command: "LoadUrl", url: "https://app.surfer-project.org/picorv32.vcd"}`
case 'LoadUrl': {
const msg = {
LoadWaveformFileFromUrl: [
decoded.url,
"Clear"
]
}
inject_message(JSON.stringify(msg))
break;
}
case 'ToggleMenu': {
const msg = "ToggleMenu"
inject_message(JSON.stringify(msg))
break;
}
// Inject any other message supported by Surfer in the surfer::Message enum.
// NOTE: The API of these is unstable.
case 'InjectMessage': {
inject_message(decoded.message);
break
}
default:
console.log(`Unknown message.command ${decoded.command}`)
break;
}
});
}

View File

@ -0,0 +1,10 @@
{
"background_color": "white",
"display": "standalone",
"id": "/index.html",
"lang": "en-US",
"name": "Surfer",
"short_name": "surfer",
"start_url": "./index.html",
"theme_color": "white"
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

37
media/surfer/surfer/sw.js Normal file
View File

@ -0,0 +1,37 @@
self.addEventListener("install", function () {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
event.respondWith(
fetch(event.request)
.then(function (response) {
// It seems like we only need to set the headers for index.html
// If you want to be on the safe side, comment this out
// if (!response.url.includes("index.html")) return response;
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
const moddedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
return moddedResponse;
})
.catch(function (e) {
console.error(e);
})
);
});

BIN
media/surfer/surfer_bg.wasm Normal file

Binary file not shown.

37
media/surfer/sw.js Normal file
View File

@ -0,0 +1,37 @@
self.addEventListener("install", function () {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") {
return;
}
event.respondWith(
fetch(event.request)
.then(function (response) {
// It seems like we only need to set the headers for index.html
// If you want to be on the safe side, comment this out
// if (!response.url.includes("index.html")) return response;
const newHeaders = new Headers(response.headers);
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
const moddedResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
return moddedResponse;
})
.catch(function (e) {
console.error(e);
})
);
});

View File

@ -9,7 +9,8 @@
},
"icon": "media/icon.png",
"categories": [
"Other"
"Chat",
"Programming Languages"
],
"keywords": [
"IC",
@ -70,26 +71,18 @@
"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临时配置"
}
"customEditors": [
{
"viewType": "ic-coder.vcdViewer",
"displayName": "VCD 波形查看器",
"selector": [
{
"filenamePattern": "*.vcd"
}
],
"priority": "default"
}
]
},
"scripts": {
"vscode:prepublish": "pnpm run package",
@ -120,7 +113,8 @@
"files": [
"dist",
"media",
"tools"
"tools",
"src/assets"
],
"dependencies": {
"@wavedrom/doppler": "^1.14.0",

BIN
rustup-init.exe Normal file

Binary file not shown.

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

@ -0,0 +1,237 @@
/**
* 代码高亮组件
*
* 功能说明:
* - 使用 highlight.js 提供专业的代码语法高亮
* - 支持多种编程语言Verilog, JavaScript, Python 等)
* - 提供行内代码和代码块的不同样式
* - 自动检测语言类型
*/
/**
* 获取 highlight.js 的 CDN 链接
*/
export function getHighlightJsLinks(): string {
return `
<!-- Highlight.js CSS (VS Code Dark+ 主题) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css">
<!-- Highlight.js 核心库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Verilog 语言支持 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/verilog.min.js"></script>
`;
}
/**
* 获取代码高亮的样式
*/
export function getCodeHighlightStyles(): string {
return `
/* 代码块基础样式 */
.segment-text pre {
background: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 12px 0;
position: relative;
white-space: pre;
}
.segment-text pre code {
background: transparent !important;
padding: 0;
border: none;
display: block;
line-height: 1.5;
white-space: pre;
font-family: 'Courier New', Consolas, 'Monaco', monospace;
font-size: 0.9em;
}
/* 行内代码样式 */
.segment-text code:not(pre code) {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
color: var(--vscode-textPreformat-foreground);
border: 1px solid var(--vscode-panel-border);
font-family: 'Courier New', Consolas, 'Monaco', monospace;
font-size: 0.9em;
}
/* 覆盖 highlight.js 的背景色,使用 VSCode 主题色 */
.segment-text pre code.hljs {
background: transparent !important;
padding: 0 !important;
}
/* 代码块语言标签 */
.code-block-wrapper {
position: relative;
margin: -20px 0;
}
.code-language-label {
position: absolute;
top: 8px;
right: 8px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
opacity: 0.8;
z-index: 1;
}
/* 代码块复制按钮 */
.code-copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 4px;
padding: 4px 8px;
font-size: 11px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.code-block-wrapper:hover .code-copy-btn {
opacity: 1;
}
.code-copy-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.code-copy-btn.copied {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
/* 代码块滚动条样式 */
.segment-text pre::-webkit-scrollbar {
height: 8px;
}
.segment-text pre::-webkit-scrollbar-track {
background: var(--vscode-scrollbarSlider-background);
border-radius: 4px;
}
.segment-text pre::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-hoverBackground);
border-radius: 4px;
}
.segment-text pre::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-activeBackground);
}
`;
}
/**
* 获取代码高亮的脚本
*/
export function getCodeHighlightScript(): string {
return `
/**
* 使用 highlight.js 进行代码高亮
*/
function highlightCodeBlocks() {
// 等待 highlight.js 加载完成
if (typeof hljs === 'undefined') {
setTimeout(highlightCodeBlocks, 100);
return;
}
const codeBlocks = document.querySelectorAll('.segment-text pre code:not(.hljs)');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
}
/**
* 为代码块添加复制按钮
*/
function enhanceCodeBlocks() {
const codeBlocks = document.querySelectorAll('.segment-text pre code');
codeBlocks.forEach((codeElement) => {
const preElement = codeElement.parentElement;
if (!preElement || preElement.classList.contains('enhanced')) {
return;
}
// 标记为已增强,避免重复处理
preElement.classList.add('enhanced');
// 应用语法高亮
if (typeof hljs !== 'undefined' && !codeElement.classList.contains('hljs')) {
hljs.highlightElement(codeElement);
}
// 创建包装器
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
preElement.parentNode.insertBefore(wrapper, preElement);
wrapper.appendChild(preElement);
// 添加复制按钮
const copyBtn = document.createElement('button');
copyBtn.className = 'code-copy-btn';
copyBtn.textContent = '复制';
copyBtn.onclick = function() {
const code = codeElement.textContent;
navigator.clipboard.writeText(code).then(() => {
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 2000);
});
};
wrapper.appendChild(copyBtn);
});
}
/**
* 监听 DOM 变化,自动增强新添加的代码块
*/
function observeCodeBlocks() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
enhanceCodeBlocks();
}
});
});
observer.observe(document.getElementById('messages'), {
childList: true,
subtree: true
});
}
// 初始化代码块增强
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
enhanceCodeBlocks();
observeCodeBlocks();
});
} else {
enhanceCodeBlocks();
observeCodeBlocks();
}
`;
}

View File

@ -1,37 +1,72 @@
/**
* 配置管理
* 从 VSCode 设置读取配置项
* 支持 dev本地开发和 test测试服务器两种环境
*/
import * as vscode from "vscode";
/** 环境类型 */
type Environment = "dev" | "test" | "prod";
/** 当前环境 - 修改这里切换环境 */
const CURRENT_ENV: Environment = "dev";
/** 服务等级类型 */
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
/** 配置项接口 */
export interface IccoderConfig {
/** 后端服务地址 */
backendUrl: string;
/** 登录页面地址 */
loginUrl: string;
/** 请求超时时间(毫秒) */
timeout: number;
/** 用户ID临时使用后续对接认证 */
userId: string;
/** 服务等级 */
serviceTier: ServiceTier;
}
/** 默认配置 */
const DEFAULT_CONFIG: IccoderConfig = {
backendUrl: "http://localhost:8080",
/** 环境配置 */
const ENV_CONFIG: Record<Environment, IccoderConfig> = {
/** 本地开发环境 */
dev: {
backendUrl: "http://localhost:2233",
loginUrl: "http://localhost/login",
timeout: 300000,
userId: "default-user",
serviceTier: "max", // 默认使用 max
},
/** 测试服务器环境 */
test: {
backendUrl: "http://192.168.1.108:2233",
loginUrl: "http://192.168.1.108:2005/login",
timeout: 60000,
userId: "default-user",
serviceTier: "max",
},
/** 生产环境 */
prod: {
backendUrl: "https://api.iccoder.com",
loginUrl: "https://iccoder.com/login",
timeout: 60000,
userId: "default-user",
serviceTier: "auto",
},
};
/**
* 获取当前环境
*/
export function getCurrentEnv(): Environment {
return CURRENT_ENV;
}
/**
* 获取配置项
*/
export function getConfig(): IccoderConfig {
const config = vscode.workspace.getConfiguration("icCoder");
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),
};
return { ...ENV_CONFIG[CURRENT_ENV] };
}
/**
@ -39,7 +74,6 @@ export function getConfig(): IccoderConfig {
*/
export function getApiUrl(path: string): string {
const { backendUrl } = getConfig();
// 确保 URL 格式正确
const baseUrl = backendUrl.endsWith("/")
? backendUrl.slice(0, -1)
: backendUrl;

View File

@ -70,3 +70,108 @@ 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>
</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>`;
/**
* Ask 模式图标 SVG
*/
export const askIconSvg = `<svg t="1767143500000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><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="M623.6 316.7C593.6 290.4 554 276 512 276s-81.6 14.5-111.6 40.7C369.2 344 352 380.7 352 420.4c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8 0-25.6 10.1-49.4 28.4-67.2 18.7-18.2 43.4-28.2 71.6-28.2s52.9 10 71.6 28.2c18.3 17.8 28.4 41.6 28.4 67.2 0 29.5-12.2 55.3-36.2 76.6-23.2 20.6-61.1 45.9-82.2 60.6-17.8 12.4-28.6 32.7-28.6 54.2V640c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8v-35.8c0-4.1 2.6-7.8 6.5-9.2 31.3-11.6 84.8-40.6 113.8-64.8 42.6-35.6 66.2-83.5 66.2-134.8 0-39.7-17.2-76.4-48.4-103.3z" fill="#8a8a8a"/><path d="M512 716m-40 0a40 40 0 1 0 80 0 40 40 0 1 0-80 0Z" fill="#8a8a8a"/></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>
`;
/**
* 用户提问图标 SVG
*/
export const userQuestionIconSvg = `<svg t="1767869230062" class="icon" viewBox="0 0 1068 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4819" width="14" height="14"><path d="M563.645217 578.782609c2.537739-35.350261 6.322087-58.189913 11.397566-68.518957 7.568696-15.449043 24.175304-34.370783 49.775304-56.631652 35.172174-30.72 58.546087-53.960348 70.121739-69.810087 11.575652-15.805217 17.408-36.418783 17.408-61.885217 0-41.939478-15.805217-76.399304-47.37113-103.379479-31.610435-26.980174-73.638957-40.470261-126.130087-40.47026-56.765217 0-101.376 15.760696-133.921392 47.282086C372.424348 256.934957 356.173913 298.562783 356.173913 350.386087h71.145739c1.335652-31.165217 6.811826-55.02887 16.384-71.590957 17.051826-29.740522 47.86087-44.610783 92.338087-44.610782 35.973565 0 61.796174 8.637217 77.378783 25.911652 15.582609 17.274435 23.373913 37.665391 23.373913 61.128348 0 16.784696-5.342609 32.990609-16.027826 48.573217-5.787826 8.904348-13.534609 17.363478-23.151305 25.555478l-31.966608 28.40487c-30.675478 27.113739-50.487652 51.155478-59.570087 72.125217-6.054957 13.979826-10.551652 41.627826-13.579131 82.899479h71.145739z m15.137392 89.043478a44.521739 44.521739 0 1 0-89.043479 0 44.521739 44.521739 0 0 0 89.043479 0z" fill="#8a8a8a" p-id="4820"></path><path d="M934.912 0h-801.391304a133.565217 133.565217 0 0 0-133.565218 133.565217v623.304348l0.222609 7.835826A133.565217 133.565217 0 0 0 133.565217 890.434783h222.608696v89.043478a44.521739 44.521739 0 0 0 64.556522 39.713391L675.661913 890.434783h259.294609a133.565217 133.565217 0 0 0 133.565217-133.565218V133.565217a133.565217 133.565217 0 0 0-133.565217-133.565217z m-801.391304 89.043478h801.391304a44.521739 44.521739 0 0 1 44.521739 44.521739v623.304348a44.521739 44.521739 0 0 1-44.521739 44.521739h-269.801739a44.521739 44.521739 0 0 0-20.034783 4.763826l-199.902608 100.930783V845.913043a44.521739 44.521739 0 0 0-44.52174-44.521739h-267.130434a44.521739 44.521739 0 0 1-44.521739-44.521739V133.565217a44.521739 44.521739 0 0 1 44.521739-44.521739z" fill="#8a8a8a" p-id="4821"></path></svg>`;

View File

@ -1,13 +1,27 @@
import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
import { VCDViewerPanel, VCDViewerEditorProvider } from "./panels/VCDViewerPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager";
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
import { VCDFileServer } from "./services/vcdFileServer";
export function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!");
// 初始化 VCD 文件服务器
const vcdFileServer = new VCDFileServer();
vcdFileServer.start().then((port) => {
console.log(`VCD 文件服务器已启动,端口: ${port}`);
}).catch((error) => {
console.error("启动 VCD 文件服务器失败:", error);
});
// 在插件停用时关闭服务器
context.subscriptions.push({
dispose: () => vcdFileServer.stop()
});
// 注册 Authentication Provider
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
@ -68,7 +82,7 @@ export function activate(context: vscode.ExtensionContext) {
}
}
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath);
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath, vcdFileServer);
}
);
@ -160,6 +174,9 @@ export function activate(context: vscode.ExtensionContext) {
viewProvider
);
// 注册 VCD 自定义编辑器
const vcdEditorProvider = VCDViewerEditorProvider.register(context, vcdFileServer);
// 添加到订阅
context.subscriptions.push(
openPanelCommand,
@ -174,7 +191,8 @@ export function activate(context: vscode.ExtensionContext) {
// deleteSessionCommand,
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration
viewRegistration,
vcdEditorProvider
);
}

View File

@ -9,7 +9,12 @@ import {
handleReplaceInFile,
handleUserAnswer,
abortCurrentDialog,
handlePlanAction,
setPendingPlanExecution,
getCurrentTaskId,
setLastTaskId,
} from "../utils/messageHandler";
import { compactDialog } from "../services/apiClient";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
@ -23,9 +28,13 @@ export async function showICHelperPanel(
) {
// 检查用户是否已登录
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (!session) {
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
vscode.window
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
@ -33,7 +42,9 @@ export async function showICHelperPanel(
return;
}
} catch (error) {
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
vscode.window
.showWarningMessage("请先登录后再使用 IC Coder", "立即登录")
.then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
@ -49,7 +60,10 @@ export async function showICHelperPanel(
{
enableScripts: 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")
);
// 获取模型图标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内容
panel.webview.html = getWebviewContent(iconUri.toString());
panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息
panel.webview.onDidReceiveMessage(
@ -107,7 +141,16 @@ export async function showICHelperPanel(
// 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId);
handleUserMessage(panel, message.text, context.extensionPath);
// 显示进度条
panel.webview.postMessage({ type: 'showProgress' });
handleUserMessage(
panel,
message.text,
context.extensionPath,
message.mode,
message.model // 传递服务等级
);
break;
case "readFile":
handleReadFile(panel, message.filePath);
@ -133,12 +176,10 @@ export async function showICHelperPanel(
vscode.window.showInformationMessage(message.text);
break;
case "openWaveformViewer":
// 打开波形查看器
// 打开波形查看器 - 使用 vscode.open 触发自定义编辑器
if (message.vcdFilePath) {
VCDViewerPanel.createOrShow(
context.extensionUri,
message.vcdFilePath
);
const vcdUri = vscode.Uri.file(message.vcdFilePath);
vscode.commands.executeCommand('vscode.open', vcdUri);
}
break;
case "getVCDInfo":
@ -179,7 +220,193 @@ export async function showICHelperPanel(
break;
// 新增:中止对话
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 "addContextFile":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件
const files = await vscode.workspace.findFiles(
"**/*",
"**/node_modules/**"
);
panel.webview.postMessage({
command: "showWorkspaceFileList",
files: files.map((uri) => ({
path: uri.fsPath,
relativePath: vscode.workspace.asRelativePath(uri),
})),
});
}
break;
// 添加文件夹上下文 - 显示工作区文件夹列表
case "addContextFolder":
{
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) {
vscode.window.showWarningMessage("请先打开一个工作区");
break;
}
// 获取工作区所有文件夹
const fs = require("fs");
const path = require("path");
const folders: Array<{ path: string; relativePath: string }> = [];
function scanFolders(dir: string, baseDir: string) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory() && item.name !== "node_modules" && !item.name.startsWith(".")) {
const fullPath = path.join(dir, item.name);
const relativePath = path.relative(baseDir, fullPath);
folders.push({ path: fullPath, relativePath });
scanFolders(fullPath, baseDir);
}
}
} catch (error) {
console.error("扫描文件夹失败:", error);
}
}
scanFolders(workspaceFolder.uri.fsPath, workspaceFolder.uri.fsPath);
panel.webview.postMessage({
command: "showWorkspaceFolderList",
folders: folders,
});
}
break;
// 添加图片上下文
case "addContextImage":
{
const imageUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择图片",
filters: {
"图片文件": ["png", "jpg", "jpeg", "gif", "bmp", "svg", "webp"],
},
});
if (imageUris && imageUris.length > 0) {
panel.webview.postMessage({
command: "contextImagesSelected",
images: imageUris.map((uri) => uri.fsPath),
});
}
}
break;
// 添加文档库上下文
case "addContextDocument":
{
const docUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: "选择文档",
filters: {
"文档文件": ["pdf", "doc", "docx", "txt", "md"],
"所有文件": ["*"],
},
});
if (docUris && docUris.length > 0) {
panel.webview.postMessage({
command: "contextDocumentsSelected",
documents: docUris.map((uri) => uri.fsPath),
});
}
}
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;
}
},
@ -464,6 +691,9 @@ async function selectConversation(
return;
}
// 设置 lastTaskId用于压缩等操作
setLastTaskId(taskId);
// 更新面板的任务映射,确保后续对话保存到正确的任务中
const panelId = (panel as any).__uniqueId;
historyManager.setPanelTask(panelId, taskId, workspacePath);

View File

@ -1,19 +1,77 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import { VCDFileServer } from "../services/vcdFileServer";
/**
* VCD 波形查看器面板
* VCD 波形查看器自定义编辑器提供者
*/
export class VCDViewerEditorProvider implements vscode.CustomReadonlyEditorProvider {
public static register(context: vscode.ExtensionContext, vcdFileServer: VCDFileServer): vscode.Disposable {
const provider = new VCDViewerEditorProvider(context, vcdFileServer);
const providerRegistration = vscode.window.registerCustomEditorProvider(
"ic-coder.vcdViewer",
provider,
{
webviewOptions: {
retainContextWhenHidden: true,
},
}
);
return providerRegistration;
}
constructor(
private readonly context: vscode.ExtensionContext,
private readonly vcdFileServer: VCDFileServer
) {}
async openCustomDocument(
uri: vscode.Uri,
openContext: vscode.CustomDocumentOpenContext,
token: vscode.CancellationToken
): Promise<vscode.CustomDocument> {
return {
uri,
dispose: () => {},
};
}
async resolveCustomEditor(
document: vscode.CustomDocument,
webviewPanel: vscode.WebviewPanel,
token: vscode.CancellationToken
): Promise<void> {
webviewPanel.webview.options = {
enableScripts: true,
localResourceRoots: [this.context.extensionUri],
};
// 使用公共工厂方法创建 VCD 查看器实例
VCDViewerPanel.createFromWebviewPanel(
webviewPanel,
this.context.extensionUri,
document.uri.fsPath,
this.vcdFileServer
);
}
}
/**
* VCD 波形查看器面板 (使用 Surfer)
*/
export class VCDViewerPanel {
public static currentPanel: VCDViewerPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri;
private _disposables: vscode.Disposable[] = [];
private _currentVcdPath: string | undefined;
private _vcdFileServer: VCDFileServer | undefined;
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, vcdFileServer?: VCDFileServer) {
this._panel = panel;
this._extensionUri = extensionUri;
this._vcdFileServer = vcdFileServer;
// 设置初始 HTML 内容
this._panel.webview.html = this._getLoadingHtml();
@ -24,12 +82,20 @@ export class VCDViewerPanel {
// 监听来自 webview 的消息
this._panel.webview.onDidReceiveMessage(
(message) => {
console.log("[VCDViewerPanel] 收到消息:", message);
switch (message.command) {
case "loadVCD":
if (message.filePath) {
this.loadVCDFile(message.filePath);
}
break;
case "loaded":
// Surfer iframe 加载完成,发送 VCD 文件
console.log("[VCDViewerPanel] Surfer 已加载,当前 VCD 路径:", this._currentVcdPath);
if (this._currentVcdPath) {
this.sendVcdToSurfer(this._currentVcdPath);
}
break;
}
},
null,
@ -40,7 +106,7 @@ export class VCDViewerPanel {
/**
* 创建或显示 VCD 查看器面板
*/
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string, vcdFileServer?: VCDFileServer) {
const column = vscode.ViewColumn.One;
// 如果已经有面板打开,则显示它
@ -64,7 +130,7 @@ export class VCDViewerPanel {
}
);
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri);
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
// 如果提供了 VCD 文件路径,加载它
if (vcdFilePath) {
@ -72,23 +138,44 @@ export class VCDViewerPanel {
}
}
/**
* 从已有的 webview panel 创建 VCD 查看器(用于自定义编辑器)
*/
public static createFromWebviewPanel(
panel: vscode.WebviewPanel,
extensionUri: vscode.Uri,
vcdFilePath: string,
vcdFileServer?: VCDFileServer
) {
const viewer = new VCDViewerPanel(panel, extensionUri, vcdFileServer);
viewer.loadVCDFile(vcdFilePath);
return viewer;
}
/**
* 加载 VCD 文件
*/
public loadVCDFile(vcdFilePath: string) {
try {
console.log("[VCDViewerPanel] 开始加载 VCD 文件:", vcdFilePath);
// 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) {
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
return;
}
// 保存当前 VCD 路径
this._currentVcdPath = vcdFilePath;
console.log("[VCDViewerPanel] VCD 路径已保存:", this._currentVcdPath);
// 更新面板标题
const fileName = path.basename(vcdFilePath);
this._panel.title = `VCD 波形查看器 - ${fileName}`;
this._panel.title = `Surfer 波形查看器 - ${fileName}`;
// 设置 HTML 内容
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
this._panel.webview.html = this._getWebviewContent();
console.log("[VCDViewerPanel] Webview HTML 已设置");
} catch (error) {
vscode.window.showErrorMessage(
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
@ -96,6 +183,104 @@ export class VCDViewerPanel {
}
}
/**
* 解析 VCD 文件获取根模块及其直接子模块名称
*/
private parseVcdRootScope(vcdFilePath: string): string[] {
try {
// 读取 VCD 文件
const buffer = fs.readFileSync(vcdFilePath, { encoding: 'utf8' });
const lines = buffer.split('\n');
const scopeNames: string[] = [];
let scopeDepth = 0;
const scopeStack: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// 遇到 $enddefinitions 就停止解析
if (trimmed.startsWith('$enddefinitions')) {
break;
}
// 查找 $scope 定义
const scopeMatch = trimmed.match(/^\$scope\s+(\w+)\s+(\w+)/);
if (scopeMatch) {
const scopeType = scopeMatch[1];
const scopeName = scopeMatch[2];
// 记录顶层 module (depth = 0)
if (scopeDepth === 0 && scopeType === 'module') {
scopeStack.push(scopeName);
console.log("[VCDViewerPanel] 找到顶层作用域:", scopeName);
}
// 记录顶层下的直接子模块 (depth = 1)
else if (scopeDepth === 1 && scopeType === 'module') {
const fullPath = [...scopeStack, scopeName];
scopeNames.push(fullPath.join('.'));
console.log("[VCDViewerPanel] 找到子模块:", fullPath.join('.'));
}
scopeDepth++;
}
// 遇到 $upscope 减少深度
if (trimmed.startsWith('$upscope')) {
scopeDepth--;
if (scopeDepth === 0) {
scopeStack.pop();
}
}
}
return scopeNames;
} catch (error) {
console.error("[VCDViewerPanel] 解析 VCD 文件失败:", error);
return [];
}
}
/**
* 发送 VCD 文件到 Surfer
*/
private sendVcdToSurfer(vcdFilePath: string) {
try {
console.log("[VCDViewerPanel] 准备发送 VCD 到 Surfer:", vcdFilePath);
if (!this._vcdFileServer) {
throw new Error("VCD 文件服务器未初始化");
}
// 解析 VCD 文件获取根模块名称
const scopeNames = this.parseVcdRootScope(vcdFilePath);
console.log("[VCDViewerPanel] 解析到的作用域名称:", scopeNames);
// 注册文件到 HTTP 服务器
const fileId = this._vcdFileServer.registerFile(vcdFilePath);
const httpUrl = this._vcdFileServer.getFileUrl(fileId);
const fileName = path.basename(vcdFilePath);
console.log("[VCDViewerPanel] 文件名:", fileName);
console.log("[VCDViewerPanel] HTTP URL:", httpUrl);
// 使用 LoadUrl 命令通过 HTTP 加载文件
this._panel.webview.postMessage({
command: "loadVcdUrl",
url: httpUrl,
fileName: fileName,
scopeNames: scopeNames, // 传递解析到的作用域名称
});
console.log("[VCDViewerPanel] 已发送 loadVcdUrl 消息到 webview");
} catch (error) {
console.error("[VCDViewerPanel] 发送 VCD 数据失败:", error);
vscode.window.showErrorMessage(
`发送 VCD 数据失败: ${error instanceof Error ? error.message : "未知错误"}`
);
}
}
/**
* 清理资源
*/
@ -163,188 +348,239 @@ export class VCDViewerPanel {
/**
* 获取 Webview 的 HTML 内容
*/
private _getWebviewContent(vcdFilePath: string): string {
// 获取资源 URI
const vcdromJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
private _getWebviewContent(): string {
// 获取 surfer 资源 URI
const surferJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer.js")
);
const vcdWasmUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
const surferWasmUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "surfer_bg.wasm")
);
const fontRegularUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
const integrationJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "surfer", "integration.js")
);
const fontObliqueUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Oblique.woff2")
);
const fontItalicUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Italic.woff2")
);
// 读取 VCD 文件内容并转换为 base64
const vcdContent = fs.readFileSync(vcdFilePath, "utf-8");
const vcdBase64 = Buffer.from(vcdContent).toString("base64");
return `<!DOCTYPE html>
<html lang="zh-CN">
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${this._panel.webview.cspSource}; style-src 'unsafe-inline' ${this._panel.webview.cspSource}; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; img-src ${this._panel.webview.cspSource} data:; connect-src ${this._panel.webview.cspSource};">
<title>VCD 波形查看器</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; worker-src blob:; connect-src ${this._panel.webview.cspSource} blob: http://127.0.0.1:*;">
<title>Surfer 波形查看器</title>
<script>
// 获取 VS Code API只能调用一次
const vscode = acquireVsCodeApi();
window.vscode = vscode;
window.surferReady = false;
window.pendingVcdData = null;
function on_surfer_error(msg) {
console.log("Surfer error:", msg);
document.getElementById("error_message").innerHTML = msg;
document.getElementById("error_container").style.display = "block";
}
window.on_surfer_error = on_surfer_error;
// 加载 VCD URL 的函数
function loadVcdUrl(data) {
try {
console.log('[Webview] ========== 开始加载 VCD URL ==========');
console.log('[Webview] URL:', data.url);
console.log('[Webview] Scope names from VCD:', data.scopeNames);
// 使用 setTimeout 确保 Surfer 完全准备好
setTimeout(() => {
console.log('[Webview] 通过 postMessage 发送 LoadUrl 命令');
// 使用 integration.js 提供的标准 LoadUrl 命令
window.postMessage({
command: 'LoadUrl',
url: data.url
}, '*');
console.log('[Webview] ✅ 已发送 LoadUrl 命令');
// 等待文件加载完成后,自动添加所有信号
setTimeout(async () => {
try {
console.log('[Webview] Attempting to add all signals automatically');
// 使用从 VCD 文件解析出来的作用域名称
let scopeNamesToTry = [];
if (data.scopeNames && data.scopeNames.length > 0) {
// 使用解析出来的实际子模块路径(例如 "tb.dut"
scopeNamesToTry = data.scopeNames.map(path => path.split('.'));
console.log('[Webview] Using parsed scope names:', scopeNamesToTry);
} else {
// 回退到常见的根作用域名称
scopeNamesToTry = [
['top'],
['testbench'],
['tb'],
['test'],
['dut']
];
console.log('[Webview] Using fallback scope names');
}
for (let i = 0; i < scopeNamesToTry.length; i++) {
const scopeName = scopeNamesToTry[i];
try {
const addScopeMsg = {
"AddScope": [
{
"strs": scopeName,
"id": {"Wellen": i + 1}
},
true // 递归添加子模块的所有信号
]
};
window.inject_message(JSON.stringify(addScopeMsg));
console.log('[Webview] Sent AddScope for: ' + scopeName.join('.') + ' (recursive)');
} catch (e) {
console.log('[Webview] Failed for scope: ' + scopeName.join('.'), e);
}
}
// 等待信号加载完成后,自动缩放到全部时间范围
setTimeout(() => {
try {
window.inject_message(JSON.stringify("ZoomToFit"));
console.log('[Webview] Sent ZoomToFit command');
} catch (e) {
console.log('[Webview] ZoomToFit failed:', e);
}
}, 500);
} catch (e) {
console.error('[Webview] Failed to add signals:', e);
}
}, 1500);
}, 100);
} catch (error) {
console.error('[Webview] ❌ 加载 VCD 失败:', error);
on_surfer_error(error.message + '\\n' + error.stack);
}
}
window.loadVcdUrl = loadVcdUrl;
</script>
<script type="module">
console.log('[Webview] 开始初始化 Surfer...');
import init from '${surferJsUri}';
await init({module_or_path: '${surferWasmUri}'});
console.log('[Webview] Surfer WASM 已加载');
import {WebHandle, inject_message, id_of_name, draw_text_arrow} from '${surferJsUri}';
window.inject_message = inject_message;
window.id_of_name = id_of_name;
window.draw_text_arrow = draw_text_arrow;
console.log('[Webview] Surfer 函数已导入inject_message 类型:', typeof window.inject_message);
// 等待一小段时间确保 Surfer 完全初始化
await new Promise(resolve => setTimeout(resolve, 100));
window.surferReady = true;
console.log('[Webview] Surfer 已完全初始化并准备就绪');
// 关闭 Surfer 的日志面板(如果打开的话)
try {
window.inject_message(JSON.stringify("ToggleLogs"));
console.log('[Webview] 已发送关闭日志面板命令');
} catch (e) {
console.log('[Webview] 关闭日志面板失败:', e);
}
// 如果有待处理的 VCD 数据,现在加载它
if (window.pendingVcdData) {
console.log('[Webview] 发现待处理的 VCD 数据,立即加载');
loadVcdUrl(window.pendingVcdData);
window.pendingVcdData = null;
} else {
console.log('[Webview] 没有待处理的 VCD 数据');
}
// 通知 VS Code surfer 已加载完成
console.log('[Webview] 发送 loaded 消息到 VS Code');
window.vscode.postMessage({ command: 'loaded' });
</script>
<style>
@font-face {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: normal;
src: url('${fontRegularUri}') format('woff2');
}
@font-face {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: oblique;
src: url('${fontObliqueUri}') format('woff2');
}
@font-face {
font-family: 'Iosevka Drom Web';
font-display: swap;
font-weight: 400;
font-stretch: normal;
font-style: italic;
src: url('${fontItalicUri}') format('woff2');
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Iosevka Drom Web', monospace;
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
html, body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
background: var(--vscode-editor-background);
}
#waveform-container {
width: 100vw;
height: 100vh;
overflow: auto;
}
#waveform1 {
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
flex-direction: column;
}
.spinner {
border: 4px solid var(--vscode-progressBar-background);
border-top: 4px solid var(--vscode-progressBar-foreground);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
padding: 20px;
#error_container {
padding: 1em;
border-radius: 0.5em;
margin: 0px auto;
max-width: 980px;
color: var(--vscode-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder);
border-radius: 4px;
margin: 20px;
position: relative;
height: 90%;
overflow: scroll;
}
#error_message {
overflow: scroll;
white-space: break-spaces;
}
</style>
<script src="${vcdromJsUri}"></script>
</head>
<body>
<div id="waveform-container">
<div class="loading">
<div class="spinner"></div>
<p>正在加载 VCD 波形...</p>
</div>
<div id="waveform1"></div>
<canvas id="the_canvas_id"></canvas>
<div id="error_container" style="display: none;">
<h3>❌ Surfer 加载失败</h3>
<code id="error_message"></code>
</div>
<script src="${integrationJsUri}"></script>
<script>
(async function() {
try {
// 设置 WASM 文件路径
window.wasmBinaryFile = '${vcdWasmUri}';
register_message_listener();
// 解码 base64 VCD 内容
const vcdBase64 = '${vcdBase64}';
const vcdContent = atob(vcdBase64);
console.log('[Webview] 注册 VS Code 消息监听器');
// 监听来自 VS Code 扩展的消息(使用 vscode API
window.addEventListener('message', event => {
const message = event.data;
// 隐藏加载提示
document.querySelector('.loading').style.display = 'none';
// 检查是否来自 VS Code
if (message.command === 'loadVcdUrl') {
console.log('[Webview] 收到 VS Code 消息,命令:', message.command);
console.log('[Webview] Surfer 就绪状态:', window.surferReady);
// 创建一个函数来提供 VCD 数据流
const vcdProvider = async (handler) => {
// 将 VCD 内容转换为 Uint8Array
const encoder = new TextEncoder();
const vcdData = encoder.encode(vcdContent);
// 创建一个 ReadableStream reader
const stream = new ReadableStream({
start(controller) {
controller.enqueue(vcdData);
controller.close();
}
});
const reader = stream.getReader();
// 调用 handler 并传递 reader
await handler([{
key: 'local',
value: 'waveform.vcd',
format: 'raw',
baseName: 'waveform.vcd',
ext: 'vcd',
reader: reader
}]);
};
// 初始化 VCDrom使用函数回调方式
if (typeof VCDrom === 'function') {
await VCDrom('waveform1', vcdProvider);
if (window.surferReady) {
// Surfer 已就绪,立即加载
loadVcdUrl(message);
} else {
throw new Error('VCDrom 未正确加载');
// Surfer 未就绪,保存数据等待加载
console.log('[Webview] Surfer 未就绪,保存数据待加载');
window.pendingVcdData = message;
}
} catch (error) {
console.error('加载 VCD 波形失败:', error);
document.getElementById('waveform-container').innerHTML =
'<div class="error-message">' +
'<h3>❌ 加载 VCD 波形失败</h3>' +
'<p>' + error.message + '</p>' +
'<p style="margin-top: 10px;">请确保 VCD 文件格式正确。</p>' +
'<pre style="margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); overflow: auto;">' + error.stack + '</pre>' +
'</div>';
}
})();
}, true); // 使用捕获阶段,优先于 integration.js 的监听器
</script>
</body>
</html>`;

View File

@ -6,7 +6,7 @@ import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
import { getApiUrl, getConfig } from '../config/settings';
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse } from '../types/api';
import type { ToolCallResult, AnswerRequest, ToolResultResponse, AnswerResponse, ToolConfirmResponse } from '../types/api';
/**
* HTTP 请求选项
@ -103,6 +103,18 @@ export async function submitAnswer(answer: AnswerRequest): Promise<AnswerRespons
});
}
/**
* 提交工具确认响应Ask 模式)
* POST /api/tool/confirm
*/
export async function submitToolConfirm(response: ToolConfirmResponse): Promise<ToolResultResponse> {
console.log(`[API] 提交工具确认: confirmId=${response.confirmId}, approved=${response.approved}`);
return request<ToolResultResponse>('/api/tool/confirm', {
method: 'POST',
body: response
});
}
/**
* 健康检查
* GET /api/dialog/health
@ -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,22 @@
* 整合 SSE 通信、工具执行、用户交互
*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { startStreamDialog, generateTaskId, SSEController, SSECallbacks } from './sseHandler';
import { executeToolCall, createToolExecutorContext, ToolExecutorContext } from './toolExecutor';
import { userInteractionManager } from './userInteraction';
import { getConfig } from '../config/settings';
import type { DialogRequest, ToolCallRequest, AskUserEvent } from '../types/api';
import type { DialogRequest, ToolCallRequest, AskUserEvent, RunMode, ServiceTier, ToolConfirmEvent, PlanConfirmEvent } from '../types/api';
import { submitToolConfirm, submitAnswer, stopDialog } from './apiClient';
import { ChatHistoryManager } from '../utils/chatHistoryManager';
import { getUserIdFromToken } from '../utils/jwtUtils';
/**
* 消息段落类型
*/
export interface MessageSegment {
type: 'text' | 'tool' | 'question';
type: 'text' | 'tool' | 'question' | 'agent' | 'plan' | 'progress';
content?: string;
toolName?: string;
toolStatus?: 'running' | 'success' | 'error';
@ -21,6 +26,29 @@ export interface MessageSegment {
askId?: string;
question?: string;
options?: string[];
// 智能体相关字段
agentId?: string;
agentName?: string;
agentStatus?: 'running' | 'completed' | 'error';
agentSteps?: AgentStep[];
// 计划相关字段
planTitle?: string;
planPhases?: import('../types/api').PlanPhase[];
planSteps?: string[];
planSummary?: string;
// 进度条相关字段(独立于 plan用于执行模式
progressPhases?: import('../types/api').PlanPhase[];
}
/**
* 智能体执行步骤
*/
export interface AgentStep {
step: number;
toolName: string;
toolInput?: unknown;
toolResult?: string;
status: 'running' | 'completed' | 'error';
}
/**
@ -35,6 +63,10 @@ export interface DialogCallbacks {
onToolComplete?: (toolName: string, result: string) => void;
/** 工具执行错误 */
onToolError?: (toolName: string, error: string) => void;
/** 工具确认请求Ask 模式) */
onToolConfirm?: (confirmId: number, toolName: string, toolInput: Record<string, unknown>) => void;
/** 计划确认请求Plan 模式) */
onPlanConfirm?: (confirmId: number, title: string, phases: import('../types/api').PlanPhase[] | undefined, steps: string[] | undefined, summary: string) => void;
/** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */
@ -45,6 +77,10 @@ export interface DialogCallbacks {
onError?: (message: string) => void;
/** 通知消息 */
onNotification?: (message: string) => void;
/** 上下文使用量更新 */
onContextUsage?: (data: { currentTokens: number; maxTokens: number; percentage: number }) => void;
/** 阶段进度更新 */
onPhaseProgress?: (phaseId: string, status: string) => void;
}
/**
@ -59,8 +95,9 @@ export class DialogSession {
private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null;
constructor(extensionPath: string) {
this.taskId = generateTaskId();
constructor(extensionPath: string, existingTaskId?: string) {
// 支持复用现有 taskId用于 Plan 模式确认后继续执行)
this.taskId = existingTaskId || generateTaskId();
this.toolContext = createToolExecutorContext(extensionPath);
}
@ -126,12 +163,167 @@ export class DialogSession {
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(
message: string,
callbacks: DialogCallbacks
callbacks: DialogCallbacks,
mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数
): Promise<void> {
if (this.isActive) {
callbacks.onError?.('当前有对话正在进行中');
@ -144,13 +336,51 @@ export class DialogSession {
this.currentTextSegment = null;
const config = getConfig();
// 从登录 session 获取真实 userId
let userId = config.userId; // 默认值
try {
console.log('[DialogSession] 尝试获取登录 session...');
const session = await vscode.authentication.getSession('iccoder', [], { silent: true });
console.log('[DialogSession] session 结果:', session ? '已获取' : 'null/undefined');
if (session?.accessToken) {
console.log('[DialogSession] accessToken 长度:', session.accessToken.length);
const parsedUserId = getUserIdFromToken(session.accessToken);
console.log('[DialogSession] 解析的 userId:', parsedUserId);
if (parsedUserId) {
userId = parsedUserId;
console.log('[DialogSession] 使用真实 userId:', userId);
}
} else {
console.log('[DialogSession] 未获取到 accessToken使用默认 userId:', userId);
}
} catch (error) {
console.warn('[DialogSession] 获取登录 session 失败:', error);
}
// 获取压缩数据和新消息(用于后端重启后恢复)
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 = {
taskId: this.taskId,
message,
userId: config.userId,
toolMode: 'AGENT'
userId,
mode: mode || 'agent',
serviceTier: serviceTier || config.serviceTier, // 优先使用传入的参数
compactedData: compactedData || undefined,
newMessages: newMessages.length > 0 ? newMessages : undefined,
knowledgeData: knowledgeData || undefined
};
// 追踪用户消息
historyManager.trackUserMessage(message);
const sseCallbacks: SSECallbacks = {
onTextDelta: (data) => {
this.accumulatedText += data.text;
@ -164,6 +394,15 @@ export class DialogSession {
onToolCall: async (data: ToolCallRequest) => {
const toolName = data.params.name;
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 添加)
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
@ -173,20 +412,25 @@ export class DialogSession {
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
}
// 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复
try {
await executeToolCall(data, this.toolContext);
if (!hasActiveAgent) {
this.updateToolSegment(toolName, 'success', '执行完成');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
// 也不调用 callbacks.onToolComplete避免重复
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误';
if (!hasActiveAgent) {
this.updateToolSegment(toolName, 'error', errorMsg);
callbacks.onToolError?.(toolName, errorMsg);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
callbacks.onToolError?.(toolName, errorMsg);
}
},
onToolStart: (data) => {
@ -218,6 +462,209 @@ export class DialogSession {
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 用于响应)
// 支持新格式phases和旧格式steps
this.segments.push({
type: 'plan',
askId: askId,
planTitle: data.title,
planPhases: data.phases,
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.phases, data.steps, data.summary);
},
onPhaseProgress: (data: import('../types/api').PhaseProgressEvent) => {
console.log('[DialogSession] onPhaseProgress:', data.phaseId, data.status);
// 1. 尝试更新 plan segment兼容旧逻辑
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
return { ...phase, status: data.status };
}
return phase;
});
callbacks.onSegmentUpdate?.(this.segments);
break;
}
}
// 2. 通知外部更新独立进度条
callbacks.onPhaseProgress?.(data.phaseId, data.status);
},
onPlanStepAdd: (data: import('../types/api').PlanStepAddEvent) => {
console.log('[DialogSession] onPlanStepAdd:', data.phaseId, data.step);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId) {
const newSteps = [...(phase.steps || [])];
if (data.index >= 0 && data.index < newSteps.length) {
newSteps.splice(data.index, 0, data.step);
} else {
newSteps.push(data.step);
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepRemove: (data: import('../types/api').PlanStepRemoveEvent) => {
console.log('[DialogSession] onPlanStepRemove:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
newSteps.splice(data.stepIndex, 1);
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanStepUpdate: (data: import('../types/api').PlanStepUpdateEvent) => {
console.log('[DialogSession] onPlanStepUpdate:', data.phaseId, data.stepIndex);
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan' && seg.planPhases) {
seg.planPhases = seg.planPhases.map(phase => {
if (phase.id === data.phaseId && phase.steps) {
const newSteps = [...phase.steps];
if (data.stepIndex >= 0 && data.stepIndex < newSteps.length) {
newSteps[data.stepIndex] = data.step;
}
return { ...phase, steps: newSteps };
}
return phase;
});
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onPlanSummaryUpdate: (data: import('../types/api').PlanSummaryUpdateEvent) => {
console.log('[DialogSession] onPlanSummaryUpdate');
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'plan') {
seg.planSummary = data.summary;
break;
}
}
callbacks.onSegmentUpdate?.(this.segments);
},
onAskUser: async (data: AskUserEvent) => {
this.finalizeTextSegment();
this.segments.push({
@ -240,6 +687,12 @@ export class DialogSession {
onComplete: (data) => {
this.isActive = false;
this.finalizeTextSegment();
// 追踪 AI 消息(用于后端重启后恢复)
if (this.accumulatedText) {
historyManager.trackAiMessage(this.accumulatedText);
}
// 发送所有段落
callbacks.onComplete?.(this.segments);
},
@ -257,6 +710,97 @@ export class DialogSession {
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);
},
onCreditUpdate: (data) => {
console.log('[DialogSession] onCreditUpdate: 扣除', data.deductedCredits, '剩余', data.remainingCredits);
// 资源点余额低于阈值时弹窗提醒
const LOW_CREDIT_THRESHOLD = 5;
if (data.remainingCredits < LOW_CREDIT_THRESHOLD) {
vscode.window.showWarningMessage(
`资源点余额不足!当前剩余 ${data.remainingCredits.toFixed(2)} 点,请及时充值。`,
'去充值'
).then(selection => {
if (selection === '去充值') {
// 打开充值页面
vscode.env.openExternal(vscode.Uri.parse('https://iccoder.com/recharge'));
}
});
}
},
onOpen: () => {
console.log('[DialogSession] SSE 连接已建立');
},
@ -287,6 +831,25 @@ export class DialogSession {
}
this.isActive = false;
userInteractionManager.cancelAll();
// 通知后端停止处理
stopDialog(this.taskId).catch(err => {
console.warn('[DialogSession] 停止对话请求失败:', err);
});
}
/**
* 获取当前的消息段落(用于中止时保存)
*/
getSegments(): MessageSegment[] {
return this.segments;
}
/**
* 获取累积的文本内容
*/
getAccumulatedText(): string {
return this.accumulatedText;
}
/**
@ -309,13 +872,15 @@ class DialogManager {
/**
* 创建新会话
* @param extensionPath 扩展路径
* @param existingTaskId 可选,复用现有的 taskId用于 Plan 模式确认后继续执行)
*/
createSession(extensionPath: string): DialogSession {
createSession(extensionPath: string, existingTaskId?: string): DialogSession {
// 如果有活跃会话,先中止
if (this.currentSession?.active) {
this.currentSession.abort();
}
this.currentSession = new DialogSession(extensionPath);
this.currentSession = new DialogSession(extensionPath, existingTaskId);
return this.currentSession;
}

View File

@ -2,6 +2,7 @@ import * as vscode from "vscode";
import * as http from "http";
import * as path from "path";
import * as fs from "fs";
import { getConfig } from "../config/settings";
/**
* IC Coder Authentication Provider
@ -12,7 +13,6 @@ export class ICCoderAuthenticationProvider
{
private static readonly AUTH_TYPE = "iccoder";
private static readonly AUTH_NAME = "IC Coder";
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
private static loginServer: http.Server | null = null;
private static currentPort: number | null = null;
@ -149,9 +149,8 @@ export class ICCoderAuthenticationProvider
// 构建登录 URL
const callbackUrl = `http://localhost:${port}/callback`;
const loginUrl = `${
ICCoderAuthenticationProvider.LOGIN_URL
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
const config = getConfig();
const loginUrl = `${config.loginUrl}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
console.log("🔐 登录服务器已启动,监听端口:", port);
console.log("🌐 登录 URL:", loginUrl);

View File

@ -13,6 +13,8 @@ import type {
SSEEventType,
TextDeltaEvent,
ToolCallRequest,
ToolConfirmEvent,
PlanConfirmEvent,
AskUserEvent,
CompleteEvent,
ErrorEvent,
@ -21,8 +23,15 @@ import type {
ToolErrorEvent,
WarningEvent,
NotificationEvent,
DepthUpdateEvent
DepthUpdateEvent,
AgentStartEvent,
AgentProgressEvent,
AgentCompleteEvent,
AgentErrorEvent,
ContextUsageEvent,
CreditUpdateEvent
} from '../types/api';
import type { MemoryCompactedEvent } from '../types/memory';
/**
* SSE 事件回调接口
@ -32,6 +41,20 @@ export interface SSECallbacks {
onTextDelta?: (data: TextDeltaEvent) => void;
/** 收到工具调用请求 */
onToolCall?: (data: ToolCallRequest) => void;
/** 收到工具确认请求Ask 模式) */
onToolConfirm?: (data: ToolConfirmEvent) => void;
/** 收到计划确认请求Plan 模式) */
onPlanConfirm?: (data: PlanConfirmEvent) => void;
/** 阶段进度更新 */
onPhaseProgress?: (data: import('../types/api').PhaseProgressEvent) => void;
/** 添加计划步骤 */
onPlanStepAdd?: (data: import('../types/api').PlanStepAddEvent) => void;
/** 删除计划步骤 */
onPlanStepRemove?: (data: import('../types/api').PlanStepRemoveEvent) => void;
/** 更新计划步骤 */
onPlanStepUpdate?: (data: import('../types/api').PlanStepUpdateEvent) => void;
/** 更新计划摘要 */
onPlanSummaryUpdate?: (data: import('../types/api').PlanSummaryUpdateEvent) => void;
/** 工具开始执行 */
onToolStart?: (data: ToolStartEvent) => void;
/** 工具执行完成 */
@ -50,6 +73,20 @@ export interface SSECallbacks {
onNotification?: (data: NotificationEvent) => 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;
/** 资源点余额更新 */
onCreditUpdate?: (data: CreditUpdateEvent) => void;
/** 连接打开 */
onOpen?: () => void;
/** 连接关闭 */
@ -124,7 +161,7 @@ export async function startStreamDialog(
const body = JSON.stringify(request);
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, mode=${request.mode}, url=${urlString}`);
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {
@ -256,6 +293,27 @@ function dispatchEvent(
case 'tool_call':
callbacks.onToolCall?.(data as ToolCallRequest);
break;
case 'tool_confirm':
callbacks.onToolConfirm?.(data as ToolConfirmEvent);
break;
case 'plan_confirm':
callbacks.onPlanConfirm?.(data as PlanConfirmEvent);
break;
case 'phase_progress':
callbacks.onPhaseProgress?.(data as import('../types/api').PhaseProgressEvent);
break;
case 'plan_step_add':
callbacks.onPlanStepAdd?.(data as import('../types/api').PlanStepAddEvent);
break;
case 'plan_step_remove':
callbacks.onPlanStepRemove?.(data as import('../types/api').PlanStepRemoveEvent);
break;
case 'plan_step_update':
callbacks.onPlanStepUpdate?.(data as import('../types/api').PlanStepUpdateEvent);
break;
case 'plan_summary_update':
callbacks.onPlanSummaryUpdate?.(data as import('../types/api').PlanSummaryUpdateEvent);
break;
case 'tool_start':
callbacks.onToolStart?.(data as ToolStartEvent);
break;
@ -283,6 +341,32 @@ function dispatchEvent(
case 'depth_update':
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
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;
case 'credit_update':
callbacks.onCreditUpdate?.(data as CreditUpdateEvent);
break;
case 'heartbeat':
// 心跳事件:仅用于保持连接,不需要特殊处理
// Node.js req.setTimeout 会在收到数据时自动重置计时器
console.log('[SSE] 收到心跳');
break;
default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
}

View File

@ -9,6 +9,8 @@ import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles';
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
import { analyzeVcdFile } from '../utils/vcdParser';
import { executeWaveformTrace, WaveformTraceArgs } from '../utils/waveformTracer';
import {
submitToolResult,
createSuccessResult,
@ -20,10 +22,13 @@ import type {
ToolName,
FileReadArgs,
FileWriteArgs,
FileDeleteArgs,
FileListArgs,
SyntaxCheckArgs,
SimulationArgs,
WaveformSummaryArgs
WaveformSummaryArgs,
KnowledgeSaveArgs,
KnowledgeLoadArgs
} from '../types/api';
/**
@ -61,6 +66,9 @@ export async function executeToolCall(
case 'file_write':
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break;
case 'file_delete':
resultText = await executeFileDelete(args as unknown as FileDeleteArgs);
break;
case 'file_list':
resultText = await executeFileList(args as unknown as FileListArgs);
break;
@ -73,6 +81,15 @@ export async function executeToolCall(
case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
break;
case 'waveform_trace':
resultText = await executeWaveformTrace(args as unknown as WaveformTraceArgs, context);
break;
case 'knowledge_save':
resultText = await executeKnowledgeSave(args as unknown as KnowledgeSaveArgs);
break;
case 'knowledge_load':
resultText = await executeKnowledgeLoad();
break;
default:
throw new Error(`未知工具: ${toolName}`);
}
@ -105,9 +122,59 @@ async function executeFileRead(args: FileReadArgs): Promise<string> {
*/
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
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}`;
}
/**
* 执行 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 工具
*/
@ -238,12 +305,95 @@ async function executeSimulation(
/**
* 执行 waveform_summary 工具
* TODO: 实现 VCD 波形分析
* 解析 VCD 文件并返回波形摘要
*/
async function executeWaveformSummary(args: WaveformSummaryArgs): Promise<string> {
// TODO: 使用 vcdrom/vcd-stream 解析 VCD 文件
// 目前返回一个占位响应
return `波形分析功能暂未实现。\n请求参数:\n- VCD文件: ${args.vcdPath}\n- 信号: ${args.signals}\n- 检查点: ${args.checkpoints || '无'}`;
const { vcdPath, signals, checkpoints } = args;
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const workspacePath = workspaceFolders[0].uri.fsPath;
// 解析 VCD 文件路径(支持相对路径)
const absolutePath = path.isAbsolute(vcdPath)
? vcdPath
: path.join(workspacePath, vcdPath);
// 检查文件是否存在
if (!fs.existsSync(absolutePath)) {
throw new Error(`VCD 文件不存在: ${vcdPath}`);
}
// 解析检查点时间
const checkpoint = checkpoints ? parseInt(checkpoints, 10) : undefined;
// 调用 VCD 解析器
const result = analyzeVcdFile(absolutePath, signals, checkpoint);
return result;
}
/**
* 执行 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];
}
/**

View File

@ -3,7 +3,7 @@
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
*/
import * as vscode from 'vscode';
import { submitAnswer } from './apiClient';
import { submitAnswer, submitToolConfirm } from './apiClient';
import type { AskUserEvent, AnswerRequest } from '../types/api';
/**
@ -32,6 +32,13 @@ export class UserInteractionManager {
this.webviewPanel = panel;
}
/**
* 获取 WebView 面板
*/
getWebviewPanel(): vscode.WebviewPanel | null {
return this.webviewPanel;
}
/**
* 处理 ask_user 事件
* @param event ask_user 事件数据
@ -60,13 +67,13 @@ export class UserInteractionManager {
reject
});
// 设置超时(5分钟
// 设置超时(2小时
setTimeout(() => {
if (this.pendingQuestions.has(askId)) {
this.pendingQuestions.delete(askId);
reject(new Error('用户回答超时'));
}
}, 300000);
}, 7200000);
});
}
@ -107,6 +114,30 @@ export class UserInteractionManager {
taskId: string,
answer: string
): 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 = {
askId,
taskId,
@ -124,6 +155,7 @@ export class UserInteractionManager {
throw error;
}
}
}
/**
* 取消所有待处理的问题

View File

@ -0,0 +1,145 @@
import * as http from "http";
import * as fs from "fs";
import * as path from "path";
/**
* VCD 文件 HTTP 服务器
* 用于为 Surfer 波形查看器提供 VCD 文件访问
*/
export class VCDFileServer {
private server: http.Server | null = null;
private port: number = 0;
private vcdFiles: Map<string, string> = new Map(); // fileId -> filePath
/**
* 启动服务器
*/
public async start(): Promise<number> {
if (this.server) {
return this.port;
}
return new Promise((resolve, reject) => {
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
// 监听随机端口
this.server.listen(0, "127.0.0.1", () => {
const address = this.server!.address();
if (address && typeof address === "object") {
this.port = address.port;
console.log(`[VCDFileServer] 服务器已启动,端口: ${this.port}`);
resolve(this.port);
} else {
reject(new Error("无法获取服务器端口"));
}
});
this.server.on("error", (error) => {
console.error("[VCDFileServer] 服务器错误:", error);
reject(error);
});
});
}
/**
* 停止服务器
*/
public stop(): void {
if (this.server) {
this.server.close();
this.server = null;
this.port = 0;
this.vcdFiles.clear();
console.log("[VCDFileServer] 服务器已停止");
}
}
/**
* 注册 VCD 文件
*/
public registerFile(filePath: string): string {
const fileId = this.generateFileId(filePath);
this.vcdFiles.set(fileId, filePath);
console.log(`[VCDFileServer] 注册文件: ${fileId} -> ${filePath}`);
return fileId;
}
/**
* 获取文件 URL
*/
public getFileUrl(fileId: string): string {
return `http://127.0.0.1:${this.port}/vcd/${fileId}`;
}
/**
* 生成文件 ID
*/
private generateFileId(filePath: string): string {
const timestamp = Date.now();
const fileName = path.basename(filePath);
return `${timestamp}-${fileName}`;
}
/**
* 处理 HTTP 请求
*/
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
const url = req.url || "";
console.log(`[VCDFileServer] 收到请求: ${url}`);
// 设置 CORS 头
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
// 处理 OPTIONS 请求
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
// 解析 URL提取文件 ID
const match = url.match(/^\/vcd\/(.+)$/);
if (!match) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
return;
}
const fileId = match[1];
const filePath = this.vcdFiles.get(fileId);
if (!filePath) {
console.error(`[VCDFileServer] 文件 ID 不存在: ${fileId}`);
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("File Not Found");
return;
}
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
console.error(`[VCDFileServer] 文件不存在: ${filePath}`);
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("File Not Found");
return;
}
// 读取并发送文件
try {
const fileContent = fs.readFileSync(filePath);
res.writeHead(200, {
"Content-Type": "text/plain",
"Content-Length": fileContent.length,
});
res.end(fileContent);
console.log(`[VCDFileServer] 成功发送文件: ${filePath}`);
} catch (error) {
console.error(`[VCDFileServer] 读取文件失败:`, error);
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
}
}
}

View File

@ -3,8 +3,28 @@
* 对应后端 IC Coder Backend 的接口格式
*/
import { CompactedMemory, CompactedMessage } from "./memory";
// ============== 对话请求/响应 ==============
/**
* 运行模式类型
* - plan: 只读模式,只能查询分析
* - ask: 逐个确认,每个写操作需确认
* - agent: 智能体自主(默认)
* - auto: 完全自动
*/
export type RunMode = "plan" | "ask" | "agent" | "auto";
/**
* 服务等级类型
* - lite: 轻量级
* - syntaxic: 语法级
* - max: 最大性能
* - auto: 自动选择
*/
export type ServiceTier = "lite" | "syntaxic" | "max" | "auto";
/**
* 对话请求
* POST /api/dialog/stream
@ -16,25 +36,48 @@ export interface DialogRequest {
message: string;
/** 用户ID */
userId: string;
/** 工具模式 */
toolMode: 'ASK' | 'AGENT';
/** 运行模式 */
mode: RunMode;
/** 服务等级 */
serviceTier?: ServiceTier;
/** 压缩后的记忆数据(用于后端重启后恢复) */
compactedData?: CompactedMemory;
/** 压缩后产生的新消息 */
newMessages?: CompactedMessage[];
/** 知识图谱数据JSON 字符串,用于恢复知识图谱) */
knowledgeData?: string;
}
// ============== SSE 事件类型 ==============
/** SSE 事件类型枚举 */
export type SSEEventType =
| 'text_delta' // 文本增量
| 'tool_call' // 客户端工具调用请求
| 'tool_start' // 工具开始执行
| 'tool_complete' // 工具执行完成
| 'tool_error' // 工具执行错误
| 'ask_user' // 向用户提问
| 'complete' // 对话完成
| 'error' // 错误
| 'warning' // 警告
| 'notification' // 通知
| 'depth_update'; // 深度更新
| "text_delta" // 文本增量
| "tool_call" // 客户端工具调用请求
| "tool_confirm" // 工具确认请求Ask 模式)
| "plan_confirm" // 计划确认请求Plan 模式)
| "phase_progress" // 阶段进度更新
| "plan_step_add" // 添加计划步骤
| "plan_step_remove" // 删除计划步骤
| "plan_step_update" // 更新计划步骤
| "plan_summary_update" // 更新计划摘要
| "tool_start" // 工具开始执行
| "tool_complete" // 工具执行完成
| "tool_error" // 工具执行错误
| "ask_user" // 向用户提问
| "agent_start" // 子智能体启动
| "agent_progress" // 子智能体进度
| "agent_complete" // 子智能体完成
| "agent_error" // 子智能体错误
| "memory_compacted" // 记忆压缩完成
| "context_usage" // 上下文使用量
| "credit_update" // 资源点余额更新
| "complete" // 对话完成
| "error" // 错误
| "warning" // 警告
| "notification" // 通知
| "depth_update" // 深度更新
| "heartbeat"; // 心跳
/** text_delta 事件数据 */
export interface TextDeltaEvent {
@ -59,6 +102,95 @@ export interface ToolErrorEvent {
error: string;
}
/** tool_confirm 事件数据Ask 模式确认请求) */
export interface ToolConfirmEvent {
/** 确认ID用于匹配响应 */
confirmId: number;
/** 工具名称 */
toolName: string;
/** 工具输入参数 */
toolInput: Record<string, unknown>;
/** 时间戳 */
timestamp: number;
}
/** 计划步骤 */
export interface PlanStep {
/** 步骤名称 */
name: string;
/** 步骤描述 */
description?: string;
}
/** 计划阶段 */
export interface PlanPhase {
/** 阶段ID: spec/design/sim/done */
id: string;
/** 阶段名称 */
name: string;
/** 阶段状态: skipped/completed/current/pending */
status: string;
/** 跳过原因 */
reason?: string;
/** 阶段内的步骤 */
steps: PlanStep[];
}
/** plan_confirm 事件数据Plan 模式计划确认) */
export interface PlanConfirmEvent {
/** 确认ID */
confirmId: number;
/** 计划标题 */
title: string;
/** 四阶段计划列表(新格式) */
phases?: PlanPhase[];
/** 执行步骤列表(旧格式,兼容) */
steps?: string[];
/** 计划摘要 */
summary: string;
/** 时间戳 */
timestamp: number;
}
/** phase_progress 事件数据(阶段进度更新) */
export interface PhaseProgressEvent {
/** 阶段ID: spec/design/sim/done */
phaseId: string;
/** 状态: current/completed */
status: string;
/** 时间戳 */
timestamp: number;
}
/** plan_step_add 事件数据(添加计划步骤) */
export interface PlanStepAddEvent {
phaseId: string;
step: PlanStep;
index: number;
timestamp: number;
}
/** plan_step_remove 事件数据(删除计划步骤) */
export interface PlanStepRemoveEvent {
phaseId: string;
stepIndex: number;
timestamp: number;
}
/** plan_step_update 事件数据(更新计划步骤) */
export interface PlanStepUpdateEvent {
phaseId: string;
stepIndex: number;
step: PlanStep;
timestamp: number;
}
/** plan_summary_update 事件数据(更新计划摘要) */
export interface PlanSummaryUpdateEvent {
summary: string;
timestamp: number;
}
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
@ -92,6 +224,58 @@ export interface DepthUpdateEvent {
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;
}
/** credit_update 事件数据 */
export interface CreditUpdateEvent {
deductedCredits: number;
remainingCredits: number;
}
// ============== 工具调用协议 (MCP 格式) ==============
/**
@ -100,11 +284,11 @@ export interface DepthUpdateEvent {
*/
export interface ToolCallRequest {
/** JSON-RPC版本固定为"2.0" */
jsonrpc: '2.0';
jsonrpc: "2.0";
/** 请求ID用于匹配响应 */
id: number;
/** 方法名,固定为"tools/call" */
method: 'tools/call';
method: "tools/call";
/** 调用参数 */
params: {
/** 工具名称 */
@ -120,7 +304,7 @@ export interface ToolCallRequest {
*/
export interface ToolCallResult {
/** JSON-RPC版本 */
jsonrpc: '2.0';
jsonrpc: "2.0";
/** 请求ID与ToolCallRequest.id对应 */
id: number;
/** 执行结果与error互斥 */
@ -186,16 +370,35 @@ export interface ToolResultResponse {
error?: string;
}
// ============== 工具确认响应 ==============
/**
* 工具确认响应请求
* POST /api/tool/confirm
*/
export interface ToolConfirmResponse {
/** 确认ID与 ToolConfirmEvent.confirmId 对应 */
confirmId: number;
/** 任务ID */
taskId: string;
/** 是否批准执行 */
approved: boolean;
}
// ============== 辅助类型 ==============
/** 后端工具名称 */
export type ToolName =
| 'file_read'
| 'file_write'
| 'file_list'
| 'syntax_check'
| 'simulation'
| 'waveform_summary';
| "file_read"
| "file_write"
| "file_delete"
| "file_list"
| "syntax_check"
| "simulation"
| "waveform_summary"
| "waveform_trace"
| "knowledge_save"
| "knowledge_load";
/** file_read 工具参数 */
export interface FileReadArgs {
@ -208,6 +411,12 @@ export interface FileWriteArgs {
content: string;
}
/** file_delete 工具参数 */
export interface FileDeleteArgs {
/** 要删除的文件路径 */
path: string;
}
/** file_list 工具参数 */
export interface FileListArgs {
path?: string;
@ -233,11 +442,38 @@ export interface WaveformSummaryArgs {
checkpoints?: string;
}
/** waveform_trace 工具参数 */
export interface WaveformTraceArgs {
/** Verilog 源文件路径(相对于项目根目录) */
verilogPath: string;
/** VCD 波形文件路径(相对于项目根目录) */
vcdPath: string;
/** 仿真工具的输出字符串(包含 mismatch 信息) */
simOutput: string;
/** BFS 回溯层数,默认 2 */
traceLevel?: number;
}
/** knowledge_save 工具参数 */
export interface KnowledgeSaveArgs {
/** 知识图谱 JSON 数据 */
data: string;
}
/** knowledge_load 工具参数 */
export interface KnowledgeLoadArgs {
// 无参数,直接读取 .iccoder/knowledge.json
}
/** 工具参数联合类型 */
export type ToolArgs =
| FileReadArgs
| FileWriteArgs
| FileDeleteArgs
| FileListArgs
| SyntaxCheckArgs
| SimulationArgs
| WaveformSummaryArgs;
| WaveformSummaryArgs
| WaveformTraceArgs
| KnowledgeSaveArgs
| KnowledgeLoadArgs;

View File

@ -5,7 +5,8 @@ export enum MessageType {
SYSTEM = "SYSTEM",
USER = "USER",
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字符串
}
/**
* 压缩摘要消息
*/
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,
AiMessage,
SystemMessage,
ToolExecutionResultMessage
ToolExecutionResultMessage,
CompactionSummaryMessage
} from '../types/chatHistory';
import { CompactedMemory, CompactedMessage } from '../types/memory';
/**
* 会话历史管理器
@ -23,6 +25,8 @@ export class ChatHistoryManager {
private currentProjectPath: string | null = null;
// 存储每个面板的任务信息taskId 和 projectPath
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
// 追踪压缩后产生的新消息
private newMessagesSinceCompaction: CompactedMessage[] = [];
private constructor() {
// 设置存储路径: ~/.iccoder
@ -690,4 +694,203 @@ export class ChatHistoryManager {
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) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法创建相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
);
}
}
@ -28,7 +30,7 @@ export async function createFile(
throw new Error(`文件已存在: ${absolutePath}`);
} catch (error: any) {
// 如果文件不存在,继续创建
if (error.code !== 'FileNotFound') {
if (error.code !== "FileNotFound") {
throw error;
}
}
@ -65,7 +67,9 @@ export async function createOrOverwriteFile(
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法创建相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建文件了"
);
}
}
@ -99,7 +103,9 @@ export async function createDirectory(dirPath: string): Promise<void> {
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, dirPath);
} else {
throw new Error("没有打开的工作区,无法创建相对路径的目录");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您创建目录了"
);
}
}
@ -115,7 +121,7 @@ export async function createDirectory(dirPath: string): Promise<void> {
}
} catch (error: any) {
// 如果目录不存在,继续创建
if (error.code !== 'FileNotFound') {
if (error.code !== "FileNotFound") {
throw error;
}
}
@ -161,7 +167,9 @@ export async function deleteFile(filePath: string): Promise<void> {
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法删除相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您删除文件了"
);
}
}
@ -197,7 +205,9 @@ export async function updateFile(
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法修改相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
}
}
@ -236,7 +246,9 @@ export async function appendToFile(
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法追加相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您追加文件内容了"
);
}
}
@ -274,7 +286,9 @@ export async function replaceFile(
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法修改相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
}
}
@ -291,14 +305,17 @@ export async function replaceFile(
// 转义特殊字符,将字符串作为字面量处理
const escapeRegExp = (str: string) => {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
// 替换内容 - 如果是字符串,先转义特殊字符
let newContent: string;
if (typeof searchValue === 'string') {
if (typeof searchValue === "string") {
const escapedSearch = escapeRegExp(searchValue);
newContent = fileContent.replace(new RegExp(escapedSearch, "g"), replaceValue);
newContent = fileContent.replace(
new RegExp(escapedSearch, "g"),
replaceValue
);
} else {
newContent = fileContent.replace(searchValue, replaceValue);
}
@ -330,7 +347,9 @@ export async function insertAtLine(
if (workspaceFolders && workspaceFolders.length > 0) {
absolutePath = path.join(workspaceFolders[0].uri.fsPath, filePath);
} else {
throw new Error("没有打开的工作区,无法修改相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您修改文件了"
);
}
}
@ -382,7 +401,9 @@ export async function renameFile(
absoluteNewPath = path.join(workspaceRoot, newPath);
}
} else {
throw new Error("没有打开的工作区,无法重命名相对路径的文件");
throw new Error(
"请先打开一个文件夹作为工作区,这样我就能为您重命名文件了"
);
}
const oldUri = vscode.Uri.file(absoluteOldPath);
@ -401,10 +422,13 @@ export async function renameFile(
throw new Error(`目标文件已存在: ${absoluteNewPath}`);
} catch (error: any) {
// 如果文件不存在,继续重命名
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
if (
error.code !== "FileNotFound" &&
!error.message.includes("目标文件已存在")
) {
throw error;
}
if (error.message.includes('目标文件已存在')) {
if (error.message.includes("目标文件已存在")) {
throw error;
}
}

73
src/utils/jwtUtils.ts Normal file
View File

@ -0,0 +1,73 @@
/**
* JWT 工具函数
*/
/**
* JWT Payload 接口
*/
export interface JwtPayload {
sub?: string; // subject (通常是 userId)
userId?: number; // 用户ID (驼峰命名)
user_id?: number; // 用户ID (下划线命名)
exp?: number; // 过期时间
iat?: number; // 签发时间
[key: string]: unknown;
}
/**
* 解析 JWT token 的 payload
* @param token JWT token
* @returns 解析后的 payload解析失败返回 null
*/
export function parseJwtPayload(token: string): JwtPayload | null {
try {
const parts = token.split('.');
if (parts.length !== 3) {
console.warn('[JWT] token 格式不正确期望3部分实际:', parts.length);
return null;
}
// payload 是第二部分base64url 编码
const payload = parts[1];
// base64url 转 base64
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
// 解码
const jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
const parsed = JSON.parse(jsonStr);
console.log('[JWT] 解析成功, payload 字段:', Object.keys(parsed));
console.log('[JWT] payload 内容:', JSON.stringify(parsed));
return parsed;
} catch (error) {
console.error('[JWT] 解析失败:', error);
return null;
}
}
/**
* 从 JWT token 中获取用户ID
* @param token JWT token
* @returns 用户ID字符串获取失败返回 null
*/
export function getUserIdFromToken(token: string): string | null {
const payload = parseJwtPayload(token);
if (!payload) {
return null;
}
// 支持多种字段名user_id, userId, sub
if (payload.user_id !== undefined) {
return String(payload.user_id);
}
if (payload.userId !== undefined) {
return String(payload.userId);
}
if (payload.sub !== undefined) {
return String(payload.sub);
}
console.warn('[JWT] payload 中没有 user_id, userId 或 sub 字段');
return null;
}

View File

@ -19,19 +19,47 @@ import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient";
import type { RunMode, ServiceTier } from "../types/api";
/** 是否使用后端服务(可通过配置控制) */
let useBackendService = true;
/** 当前对话会话 */
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(
panel: vscode.WebviewPanel,
text: string,
extensionPath?: string
extensionPath?: string,
mode?: RunMode,
serviceTier?: ServiceTier // 新增:服务等级参数
) {
console.log("收到用户消息:", text);
@ -63,32 +91,31 @@ export async function handleUserMessage(
// 尝试使用后端服务
if (useBackendService && extensionPath) {
try {
await handleUserMessageWithBackend(panel, text, extensionPath);
await handleUserMessageWithBackend(panel, text, extensionPath, mode, undefined, serviceTier);
return;
} catch (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(() => {
console.error("后端服务不可用:", error);
panel.webview.postMessage({
command: "receiveMessage",
text: reply,
command: "updateStatus",
text: "后端服务不可用",
type: "error",
});
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
throw error;
}
}
// 如果没有 extensionPath显示错误
panel.webview.postMessage({
command: "updateStatus",
text: "无法处理消息:缺少必要参数",
type: "error",
});
}, 500);
}
/**
@ -97,15 +124,25 @@ export async function handleUserMessage(
async function handleUserMessageWithBackend(
panel: vscode.WebviewPanel,
text: string,
extensionPath: string
extensionPath: string,
mode?: RunMode,
reuseTaskId?: string, // 可选,复用现有 taskId用于 Plan 模式确认后继续执行)
serviceTier?: ServiceTier // 新增:服务等级参数
): Promise<void> {
const historyManager = ChatHistoryManager.getInstance();
// 获取 historyManager 中的 taskId由 ICHelperPanel 创建)
// 优先使用 reuseTaskId其次使用 historyManager 的 taskId
const taskIdToUse = reuseTaskId || historyManager.getCurrentTaskId();
// 创建或复用会话
if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath);
currentSession = dialogManager.createSession(extensionPath, taskIdToUse || undefined);
// 保存 taskId 用于后续操作(如压缩)
lastTaskId = currentSession.getTaskId();
console.log("[MessageHandler] 创建会话: taskId=", lastTaskId, "来源=", taskIdToUse ? "historyManager" : "新生成");
}
const historyManager = ChatHistoryManager.getInstance();
// 显示状态栏
panel.webview.postMessage({
command: "updateStatus",
@ -114,7 +151,9 @@ async function handleUserMessageWithBackend(
});
return new Promise((resolve, reject) => {
currentSession!.sendMessage(text, {
currentSession!.sendMessage(
text,
{
onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
},
@ -160,30 +199,62 @@ async function handleUserMessageWithBackend(
});
// 最后一次发送完整的段落
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
console.log("[MessageHandler] 对话完成, 段落数:", segments.length);
const result = await panel.webview.postMessage({
command: "updateSegments",
segments: segments,
isComplete: true,
});
console.log('[MessageHandler] postMessage 返回值:', result);
console.log("[MessageHandler] postMessage 返回值:", result);
// 保存完整的 segments 到历史记录
try {
// 将完整的 segments 保存到一条 AI 消息中
// 这样加载时可以完整还原对话样式
const textContent = segments
.filter(s => s.type === 'text' && s.content)
.map(s => s.content)
.join('\n');
.filter((s) => s.type === "text" && s.content)
.map((s) => s.content)
.join("\n");
await historyManager.addAiMessage(textContent, undefined, segments);
} catch (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();
},
@ -195,13 +266,62 @@ async function handleUserMessageWithBackend(
command: "receiveMessage",
text: `❌ 错误: ${message}`,
});
// 恢复输入状态
panel.webview.postMessage({
command: "updateSegments",
segments: [],
isComplete: true,
});
reject(new Error(message));
},
onNotification: (message) => {
vscode.window.showInformationMessage(message);
},
onContextUsage: (data) => {
// 发送上下文使用量到 WebView
panel.webview.postMessage({
command: "contextUsage",
currentTokens: data.currentTokens,
maxTokens: data.maxTokens,
percentage: data.percentage,
});
},
onPhaseProgress: (phaseId, status) => {
// 发送阶段进度更新到 WebView
// 映射 phaseId: sim -> simulation
const stepMap: Record<string, string> = {
spec: "spec",
design: "design",
sim: "simulation",
done: "done",
};
const step = stepMap[phaseId] || phaseId;
if (status === "current") {
// 显示进度条并更新到当前步骤
panel.webview.postMessage({ type: "showProgress" });
panel.webview.postMessage({ type: "updateProgress", step });
} else if (status === "completed") {
// 更新到下一步(或完成)
const steps = ["spec", "design", "simulation", "done"];
const currentIndex = steps.indexOf(step);
if (currentIndex < steps.length - 1) {
panel.webview.postMessage({
type: "updateProgress",
step: steps[currentIndex + 1],
});
} else {
panel.webview.postMessage({ type: "completeProgress" });
}
}
},
},
mode,
serviceTier // 传递服务等级
);
});
}
@ -221,11 +341,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();
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 +486,9 @@ function parseFileOperation(text: string): {
}
// 匹配重命名文件:将 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) {
const oldPath = renameMatch[1].trim();
const newPath = renameMatch[2].trim();
@ -276,7 +503,9 @@ function parseFileOperation(text: string): {
// 格式1: 在 xxx.ts 中将 "aaa" 替换为 "bbb"
// 格式2: 将 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) {
const filePath = replaceMatch1[1].trim();
const searchText = replaceMatch1[2].trim();
@ -290,7 +519,9 @@ function parseFileOperation(text: string): {
}
// 格式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) {
const filePath = replaceMatch2[1].trim();
const searchText = replaceMatch2[2].trim();
@ -618,41 +849,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 +941,8 @@ async function handleVCDGeneration(
if (!projectCheck.hasTestbench) {
errorMsg += "• ❌ 缺少 testbench 文件\n";
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
errorMsg +=
"\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
} else {
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
}
@ -789,9 +986,7 @@ async function handleVCDGeneration(
fileName: fileName,
});
vscode.window.showInformationMessage(
`VCD 文件生成成功: ${fileName}`
);
vscode.window.showInformationMessage(`VCD 文件生成成功: ${fileName}`);
} else {
panel.webview.postMessage({
command: "receiveMessage",

View File

@ -94,6 +94,13 @@ export async function readDirectory(
const results = [];
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 (extensions && extensions.length > 0) {
@ -108,7 +115,7 @@ export async function readDirectory(
const fileUri = vscode.Uri.file(filePath);
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
const content = Buffer.from(contentBytes).toString("utf-8");
results.push({ path: fileName, content });
results.push({ path: fileName, content, isDirectory: false });
} catch (error) {
// 跳过无法读取的文件
continue;

467
src/utils/vcdParser.ts Normal file
View File

@ -0,0 +1,467 @@
/**
* VCD (Value Change Dump) 解析器
* 纯 TypeScript 实现,参照 VerilogCoder 项目格式
*
* @deprecated 当前未使用,保留备用
* 目前使用 waveformTracer.ts 调用 Python 打包的 waveform_trace.exe
* 未来可能用此文件替换 Python 实现
*/
import * as fs from 'fs';
import * as path from 'path';
// ==================== 类型定义 ====================
/** 信号定义 */
export interface VcdSignal {
name: string; // 完整路径名,如 "tb.top_module.data"
shortName: string; // 短名,如 "data"
symbolId: string; // VCD 符号 ID如 "!", "#"
width: number; // 位宽
varType: string; // 变量类型wire, reg
module: string; // 所属模块
}
/** 时间-值对 */
export interface TimeValue {
time: number;
value: string;
}
/** 信号波形数据 */
export interface SignalWaveform {
signal: VcdSignal;
changes: TimeValue[];
}
/** VCD 解析结果 */
export interface VcdData {
date?: string;
version?: string;
timescale: string;
endTime: number;
signals: Map<string, VcdSignal>; // symbolId -> signal
waveforms: Map<string, TimeValue[]>; // symbolId -> changes
}
/** Mismatch 信息 */
export interface MismatchInfo {
time: number;
signal: string;
dutValue: string;
refValue: string;
}
// ==================== VCD 解析器 ====================
export class VcdParser {
private signals: Map<string, VcdSignal> = new Map();
private waveforms: Map<string, TimeValue[]> = new Map();
private scopeStack: string[] = [];
private timescale: string = '1ns';
private currentTime: number = 0;
private endTime: number = 0;
private date?: string;
private version?: string;
/**
* 解析 VCD 文件
*/
parse(filePath: string): VcdData {
const content = fs.readFileSync(filePath, 'utf-8');
return this.parseContent(content);
}
/**
* 解析 VCD 内容
*/
parseContent(content: string): VcdData {
// 预处理:将多行指令合并成单行
const normalizedContent = this.normalizeVcdContent(content);
const lines = normalizedContent.split('\n');
let inDefinitions = true;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
if (inDefinitions) {
// 解析定义区
if (line.startsWith('$enddefinitions')) {
inDefinitions = false;
continue;
}
this.parseDefinition(line);
} else {
// 解析数据区
this.parseValueChange(line);
}
}
return {
date: this.date,
version: this.version,
timescale: this.timescale,
endTime: this.endTime,
signals: this.signals,
waveforms: this.waveforms
};
}
private parseDefinition(line: string): void {
if (line.startsWith('$date')) {
this.date = this.extractValue(line);
} else if (line.startsWith('$version')) {
this.version = this.extractValue(line);
} else if (line.startsWith('$timescale')) {
this.timescale = this.extractValue(line) || '1ns';
} else if (line.startsWith('$scope')) {
const match = line.match(/\$scope\s+\w+\s+(\S+)/);
if (match) {
this.scopeStack.push(match[1]);
}
} else if (line.startsWith('$upscope')) {
this.scopeStack.pop();
} else if (line.startsWith('$var')) {
this.parseVariable(line);
}
}
private parseVariable(line: string): void {
// $var wire 8 # data [7:0] $end
// $var reg 1 ! clk $end
const match = line.match(/\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)/);
if (!match) return;
const [, varType, widthStr, symbolId, name] = match;
const width = parseInt(widthStr, 10);
const module = this.scopeStack.join('.');
const fullName = module ? `${module}.${name}` : name;
const signal: VcdSignal = {
name: fullName,
shortName: name.replace(/\[\d+:\d+\]/, ''), // 移除位宽标注
symbolId,
width,
varType,
module
};
this.signals.set(symbolId, signal);
this.waveforms.set(symbolId, []);
}
private parseValueChange(line: string): void {
if (line.startsWith('#')) {
// 时间戳: #100
this.currentTime = parseInt(line.substring(1), 10);
this.endTime = Math.max(this.endTime, this.currentTime);
} else if (line.startsWith('b') || line.startsWith('B')) {
// 多位值: b10101010 #
const spaceIdx = line.indexOf(' ');
if (spaceIdx > 0) {
const value = line.substring(1, spaceIdx);
const symbolId = line.substring(spaceIdx + 1).trim();
this.addChange(symbolId, value);
}
} else if (line.length >= 2 && !line.startsWith('$')) {
// 单位值: 0! 或 1# 或 x$
const value = line[0];
const symbolId = line.substring(1).trim();
if (symbolId && this.signals.has(symbolId)) {
this.addChange(symbolId, value);
}
}
}
private addChange(symbolId: string, value: string): void {
const changes = this.waveforms.get(symbolId);
if (changes) {
changes.push({ time: this.currentTime, value });
}
}
private extractValue(line: string): string {
// 提取 $xxx value $end 中的 value
const match = line.match(/\$\w+\s+(.+?)\s*\$end/);
return match ? match[1].trim() : '';
}
/**
* 预处理 VCD 内容,将多行指令合并成单行
*/
private normalizeVcdContent(content: string): string {
// 将多行 $xxx ... $end 合并成单行
return content.replace(/(\$\w+)\s*\n\s*([^\$]+?)\s*\n\s*(\$end)/g, '$1 $2 $3');
}
}
// ==================== 波形分析工具 ====================
/**
* 二进制字符串转十六进制
*/
export function binaryToHex(binary: string): string {
if (binary === 'x' || binary === 'X' || binary.includes('x')) {
return 'xx';
}
if (binary === 'z' || binary === 'Z' || binary.includes('z')) {
return 'zz';
}
if (binary.length <= 1) {
return binary;
}
// 补齐到 4 的倍数
const padded = binary.padStart(Math.ceil(binary.length / 4) * 4, '0');
let hex = '';
for (let i = 0; i < padded.length; i += 4) {
hex += parseInt(padded.substring(i, i + 4), 2).toString(16);
}
return hex;
}
/**
* 获取信号在指定时间的值
*/
export function getValueAtTime(
changes: TimeValue[],
time: number
): string {
let value = 'x';
for (const change of changes) {
if (change.time <= time) {
value = change.value;
} else {
break;
}
}
return value;
}
/**
* 查找 DUT 和 REF 信号的第一个 mismatch
*/
export function findFirstMismatch(
vcdData: VcdData,
dutSignals: string[],
refSignals: string[]
): MismatchInfo | null {
// 收集所有时间点
const allTimes = new Set<number>();
for (const changes of vcdData.waveforms.values()) {
for (const c of changes) {
allTimes.add(c.time);
}
}
const sortedTimes = Array.from(allTimes).sort((a, b) => a - b);
// 按信号名匹配 DUT 和 REF
for (const time of sortedTimes) {
for (let i = 0; i < dutSignals.length; i++) {
const dutSig = findSignalByName(vcdData, dutSignals[i]);
const refSig = findSignalByName(vcdData, refSignals[i]);
if (!dutSig || !refSig) continue;
const dutChanges = vcdData.waveforms.get(dutSig.symbolId) || [];
const refChanges = vcdData.waveforms.get(refSig.symbolId) || [];
const dutVal = getValueAtTime(dutChanges, time);
const refVal = getValueAtTime(refChanges, time);
// 跳过未知值
if (dutVal.includes('x') || refVal.includes('x')) continue;
if (dutVal !== refVal) {
return {
time,
signal: dutSig.shortName,
dutValue: binaryToHex(dutVal),
refValue: binaryToHex(refVal)
};
}
}
}
return null;
}
/**
* 按名称查找信号
*/
function findSignalByName(vcdData: VcdData, name: string): VcdSignal | null {
for (const signal of vcdData.signals.values()) {
if (signal.name.endsWith(name) || signal.shortName === name) {
return signal;
}
}
return null;
}
/**
* 生成波形表格(参照 VerilogCoder 格式)
*/
export function generateWaveformTable(
vcdData: VcdData,
signalNames: string[],
startTime: number = 0,
endTime?: number,
windowSize: number = 20
): string {
const actualEndTime = endTime ?? vcdData.endTime;
// 查找信号
const signals: VcdSignal[] = [];
for (const name of signalNames) {
const sig = findSignalByName(vcdData, name);
if (sig) signals.push(sig);
}
if (signals.length === 0) {
return '未找到指定信号';
}
// 收集时间点
const times = new Set<number>();
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
for (const c of changes) {
if (c.time >= startTime && c.time <= actualEndTime) {
times.add(c.time);
}
}
}
let sortedTimes = Array.from(times).sort((a, b) => a - b);
if (sortedTimes.length > windowSize) {
sortedTimes = sortedTimes.slice(0, windowSize);
}
// 生成表头
const headers = ['time(ns)', ...signals.map(s => s.shortName)];
const colWidths = headers.map(h => Math.max(h.length, 8));
let table = '### Waveform Trace ###\n';
table += headers.map((h, i) => h.padEnd(colWidths[i])).join(' ') + '\n';
table += colWidths.map(w => '─'.repeat(w)).join('──') + '\n';
// 生成数据行
for (const time of sortedTimes) {
const row = [time.toString()];
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
const val = getValueAtTime(changes, time);
row.push(binaryToHex(val));
}
table += row.map((v, i) => v.padEnd(colWidths[i])).join(' ') + '\n';
}
table += '### Waveform Trace End ###\n';
return table;
}
/**
* 获取信号显示名称(模块.信号名[位宽]
*/
function getSignalDisplayName(sig: VcdSignal): string {
const moduleParts = sig.module.split('.');
const moduleShort = moduleParts[moduleParts.length - 1] || '';
const bitInfo = sig.width > 1 ? `[${sig.width - 1}:0]` : '';
return moduleShort ? `${moduleShort}.${sig.shortName}${bitInfo}` : `${sig.shortName}${bitInfo}`;
}
/**
* 生成变化日志格式(只记录信号变化)
*/
export function generateChangeLog(vcdData: VcdData): string {
// 筛选信号(排除 parameter
const signals: VcdSignal[] = [];
for (const sig of vcdData.signals.values()) {
if (sig.varType !== 'parameter') {
signals.push(sig);
}
}
// 收集所有时间点
const times = new Set<number>();
for (const sig of signals) {
const changes = vcdData.waveforms.get(sig.symbolId) || [];
for (const c of changes) {
times.add(c.time);
}
}
const sortedTimes = Array.from(times).sort((a, b) => a - b);
// 记录每个信号的上一个值
const lastValues = new Map<string, string | null>();
for (const sig of signals) {
lastValues.set(sig.symbolId, null);
}
let log = '';
for (const time of sortedTimes) {
const changes: string[] = [];
for (const sig of signals) {
const waveform = vcdData.waveforms.get(sig.symbolId) || [];
const currentVal = binaryToHex(getValueAtTime(waveform, time));
const lastVal = lastValues.get(sig.symbolId);
if (lastVal === null) {
changes.push(`${getSignalDisplayName(sig)}=${currentVal}`);
} else if (currentVal !== lastVal) {
changes.push(`${getSignalDisplayName(sig)} ${lastVal}${currentVal}`);
}
lastValues.set(sig.symbolId, currentVal);
}
if (changes.length > 0) {
log += `#${time}: ${changes.join(', ')}\n`;
}
}
return log;
}
/**
* 分析 VCD 文件(主入口)
*/
export function analyzeVcdFile(
filePath: string,
signalFilter?: string,
checkpoint?: number
): string {
// 解析 VCD
const parser = new VcdParser();
const vcdData = parser.parse(filePath);
// 解析信号过滤器
const signalNames = signalFilter
? signalFilter.split(',').map(s => s.trim())
: Array.from(vcdData.signals.values()).map(s => s.shortName);
// 生成摘要
let result = `=== VCD 波形分析 ===\n`;
result += `文件: ${path.basename(filePath)}\n`;
result += `时间单位: ${vcdData.timescale}\n`;
result += `仿真时长: 0 - ${vcdData.endTime}${vcdData.timescale}\n\n`;
// 信号列表
result += `--- 信号列表 (${vcdData.signals.size} 个) ---\n`;
let idx = 1;
for (const sig of vcdData.signals.values()) {
if (idx <= 10) {
result += `${idx}. ${sig.shortName} (${sig.width}-bit, ${sig.varType})\n`;
}
idx++;
}
if (vcdData.signals.size > 10) {
result += `... 还有 ${vcdData.signals.size - 10} 个信号\n`;
}
result += '\n';
// 变化日志
result += generateChangeLog(vcdData);
return result;
}

145
src/utils/waveformTracer.ts Normal file
View File

@ -0,0 +1,145 @@
/**
* 波形追踪工具
* 调用 PyInstaller 打包的 waveform_trace 可执行文件
*/
import { spawn } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import * as vscode from 'vscode';
/**
* 波形追踪参数
*/
export interface WaveformTraceArgs {
/** Verilog 源文件路径(相对于项目根目录) */
verilogPath: string;
/** VCD 波形文件路径(相对于项目根目录) */
vcdPath: string;
/** 仿真工具的输出字符串(包含 mismatch 信息) */
simOutput: string;
/** BFS 回溯层数,默认 2 */
traceLevel?: number;
}
/**
* 执行波形追踪
* @param args 追踪参数
* @param context 执行上下文
* @returns 追踪结果字符串
*/
export async function executeWaveformTrace(
args: WaveformTraceArgs,
context: { extensionPath: string }
): Promise<string> {
// 获取可执行文件路径
const tracerPath = getWaveformTracerPath(context.extensionPath);
// 检查可执行文件是否存在
if (!fs.existsSync(tracerPath)) {
throw new Error(
`waveform_trace 工具未安装: ${tracerPath}\n` +
'请确保插件包含 tools/waveform_trace/bin/ 目录'
);
}
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const workspacePath = workspaceFolders[0].uri.fsPath;
// 解析路径(支持相对路径)
const verilogAbsPath = path.isAbsolute(args.verilogPath)
? args.verilogPath
: path.join(workspacePath, args.verilogPath);
const vcdAbsPath = path.isAbsolute(args.vcdPath)
? args.vcdPath
: path.join(workspacePath, args.vcdPath);
// 验证文件存在
if (!fs.existsSync(verilogAbsPath)) {
throw new Error(`Verilog 文件不存在: ${args.verilogPath}`);
}
if (!fs.existsSync(vcdAbsPath)) {
throw new Error(`VCD 文件不存在: ${args.vcdPath}`);
}
// 调用可执行文件
return new Promise((resolve, reject) => {
const child = spawn(tracerPath, [
'--verilog', verilogAbsPath,
'--vcd', vcdAbsPath,
'--sim-output', args.simOutput,
'--trace-level', String(args.traceLevel || 2),
'--output-format', 'text'
], {
windowsHide: true,
cwd: workspacePath,
shell: false
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
child.on('close', (code: number | null) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(
`waveform_trace 执行失败 (code=${code}):\n${stderr || stdout}`
));
}
});
child.on('error', (error: Error) => {
reject(new Error(`waveform_trace 启动失败: ${error.message}`));
});
});
}
/**
* 获取 waveform_trace 可执行文件路径
*/
function getWaveformTracerPath(extensionPath: string): string {
const platform = process.platform;
let binName = 'waveform_trace';
if (platform === 'win32') {
binName = 'waveform_trace.exe';
}
return path.join(extensionPath, 'tools', 'waveform_trace', 'bin', binName);
}
/**
* 检查 waveform_trace 工具是否可用
*/
export function checkWaveformTraceAvailable(extensionPath: string): {
available: boolean;
message: string;
path?: string;
} {
const tracerPath = getWaveformTracerPath(extensionPath);
if (fs.existsSync(tracerPath)) {
return {
available: true,
message: 'waveform_trace 工具可用',
path: tracerPath
};
} else {
return {
available: false,
message: `waveform_trace 工具未找到: ${tracerPath}`
};
}
}

View File

@ -24,7 +24,10 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
{
enableScripts: 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(
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内容
panel.webview.html = getWebviewContent(iconUri.toString());
panel.webview.html = getWebviewContent(
iconUri.toString(),
autoIconUri.toString(),
liteIconUri.toString(),
syIconUri.toString(),
maxIconUri.toString()
);
// 处理消息
panel.webview.onDidReceiveMessage(
(message) => {
switch (message.command) {
case "sendMessage":
handleUserMessage(panel, message.text, context.extensionPath);
handleUserMessage(panel, message.text, context.extensionPath, message.mode);
break;
case "readFile":
handleReadFile(panel, message.filePath);
@ -90,7 +114,7 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
break;
// 新增:中止对话
case "abortDialog":
abortCurrentDialog();
void abortCurrentDialog();
break;
}
},
@ -136,13 +160,17 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
});
// 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage((message) => {
webviewView.webview.onDidReceiveMessage(
(message) => {
if (message.command === "openChat") {
vscode.commands.executeCommand("ic-coder.openChat");
} else if (message.command === "login") {
vscode.commands.executeCommand("ic-coder.login");
}
});
},
undefined,
this.context.subscriptions
);
}
private getWebviewContent(

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

@ -0,0 +1,204 @@
/**
* 智能体卡片组件
*
* 功能说明:
* - 提供智能体执行状态的可视化展示
* - 显示智能体名称、状态和执行步骤
* - 支持实时更新步骤信息
*/
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;
}
/* 低调显示的工具调用样式 */
.agent-step.low-profile {
opacity: 0.5;
font-size: 10px;
padding: 2px 6px;
background: transparent;
margin-bottom: 2px;
}
.agent-step.low-profile .step-icon {
opacity: 0.4;
font-size: 10px;
}
.agent-step.low-profile .step-name {
font-weight: 300;
color: var(--vscode-descriptionForeground);
opacity: 0.7;
}
.agent-step.low-profile .step-result {
opacity: 0.6;
font-size: 9px;
}
`;
}
/**
* 获取智能体卡片的脚本
*/
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': '代码探索',
'spawnDebugger': '波形调试',
'queryByBFS': 'BFS查询',
'queryStateTransitions': '查询状态转移',
'addStateTransition': '添加状态转移'
};
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 ? '...' : ''}\` : '';
// 所有工具调用都使用低调样式
const stepClass = 'agent-step low-profile';
return \`<div class="\${stepClass}"><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,8 +1,19 @@
/**
* 模式选择器组件
* 提供 Agent/Ask/Auto 三种模式的选择功能
* 提供 Plan/Ask/Agent 四种模式的选择功能
*
* 模式说明:
* - Plan: 只读模式,只能查询分析,不能写文件
* - Ask: 逐个确认,每个写操作需用户确认
* - Agent: 智能体自主,自动执行大部分操作
*/
import {
plannerIconSvg,
askIconSvg,
agentIconSvg,
} from "../constants/toolIcons";
/**
* 获取模式选择器的 HTML 内容
*/
@ -17,12 +28,30 @@ export function getModeSelectorContent(): string {
</svg>
</div>
<div class="mode-dropdown" id="modeDropdown">
<div class="mode-option" data-value="agent" onclick="selectMode('agent', 'Agent')">Agent</div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">Ask</div>
<div class="mode-option" data-value="auto" onclick="selectMode('auto', 'Auto')">Auto</div>
<div class="mode-option" data-value="plan" onclick="selectMode('plan', 'Plan')">
<div class="mode-option-header">
<span class="mode-option-icon">${plannerIconSvg}</span>
<span class="mode-option-label">Plan</span>
</div>
<span class="mode-option-desc">仅根据需求生成设计文档,之后由用户决定下一步,可以提高工程质量</span>
</div>
<div class="mode-option" data-value="ask" onclick="selectMode('ask', 'Ask')">
<div class="mode-option-header">
<span class="mode-option-icon">${askIconSvg}</span>
<span class="mode-option-label">Ask</span>
</div>
<span class="mode-option-desc">仅给与智能体读权限,用于依据项目回答用户问题,或者与用户进行探讨</span>
</div>
<div class="mode-option selected" data-value="agent" onclick="selectMode('agent', 'Agent')">
<div class="mode-option-header">
<span class="mode-option-icon">${agentIconSvg}</span>
<span class="mode-option-label">Agent</span>
</div>
<span class="mode-option-desc">用于快速生成工程、调试修改现有代码</span>
</div>
</div>
<span class="tooltiptext">切换模式</span>
</div>
<span class="tooltiptext" id="modeTooltip">智能体自主模式</span>
</div>
`;
}
@ -69,7 +98,8 @@ export function getModeSelectorStyles(): string {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 100%;
min-width: 200px;
max-width: 300px;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
@ -83,18 +113,45 @@ export function getModeSelectorStyles(): string {
}
/* 模式选择器的选项样式 */
.mode-option {
padding: 6px 12px;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s ease;
white-space: nowrap;
}
.mode-option:hover {
background: rgba(128, 128, 128, 0.3);
}
.mode-option.selected {
background: rgba(128, 128, 128, 0.5);
color: var(--vscode-foreground);
background: rgba(64, 158, 255, 0.2);
}
.mode-option-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.mode-option-icon {
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.mode-option-icon svg {
width: 16px;
height: 16px;
display: block;
}
.mode-option-label {
font-weight: 500;
}
.mode-option-desc {
font-size: 10px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
word-wrap: break-word;
white-space: normal;
}
`;
}
@ -124,10 +181,22 @@ export function getModeSelectorScript(): string {
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) {
modeValue.textContent = label;
}
// 更新 tooltip
if (modeTooltip) {
const tooltipMap = {
'plan': 'plan模式',
'ask': 'ask模式',
'agent': 'agent模式'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
// 更新选中状态
const options = document.querySelectorAll('.mode-option');
options.forEach(option => {

View File

@ -7,14 +7,78 @@
*/
export function getContextButtonContent(): string {
return `
<div class="context-selector-wrapper">
<div class="tooltip">
<button class="add-context-button" onclick="handleAddContext()">
<button class="add-context-button" onclick="toggleContextMenu()">
<svg t="1766915545722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4994" width="200" height="200">
<path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#8a8a8a" p-id="4995"></path>
</svg>
<span class="add-context-label">添加上下文</span>
<svg class="dropdown-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 714.666667L213.333333 416l42.666667-42.666667L512 629.333333l256-256 42.666667 42.666667z" fill="currentColor"/>
</svg>
</button>
<span class="tooltiptext">添加文件或代码片段作为上下文</span>
<span class="tooltiptext">添加文件、文件夹、图片或文档作为上下文</span>
</div>
<!-- 上拉菜单 -->
<div class="context-menu" id="contextMenu">
<!-- 主菜单 -->
<div class="context-menu-main" id="contextMenuMain">
<div class="context-menu-item" onclick="showFileList()">
<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="currentColor"/>
</svg>
<span>文件</span>
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg>
</div>
<div class="context-menu-item" onclick="showFolderList()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32zM840 768H184V256h188.5l119.6 114.4H840V768z" fill="currentColor"/>
</svg>
<span>文件夹</span>
<svg class="arrow-right" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M340.864 149.312l384 384-384 384-45.248-45.248L634.368 533.312 295.616 194.56z" fill="currentColor"/>
</svg>
</div>
<div class="context-menu-item" onclick="handleAddImage()">
<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 m-120-240c0 55.2-44.8 100-100 100s-100-44.8-100-100 44.8-100 100-100 100 44.8 100 100z m-476 0l164 164h476L696 480 536 640l-84-84-160 160z" fill="currentColor"/>
</svg>
<span>图片</span>
</div>
<div class="context-menu-item" onclick="handleAddDocument()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z m-40 824H232V136h560v752z m-120-568H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z m0 144H352c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h320c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z" fill="currentColor"/>
</svg>
<span>文档库</span>
</div>
</div>
<!-- 文件/文件夹列表视图 -->
<div class="context-menu-list" id="contextMenuList" style="display: none;">
<div class="context-menu-list-header">
<button class="context-menu-back" onclick="backToMainMenu()">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8c-16.4 12.8-16.4 37.5 0 50.3l450.8 352.1c5.3 4.1 12.9 0.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z" fill="currentColor"/>
</svg>
</button>
<span id="contextMenuListTitle">选择文件</span>
</div>
<div class="context-menu-list-body" id="contextMenuListBody">
<!-- 动态加载列表 -->
</div>
<div class="context-menu-list-footer">
<input type="text" id="contextMenuSearch" placeholder="搜索..." />
<div class="context-menu-list-actions">
<span id="contextMenuListCount">已选择 0 项</span>
<button class="primary" onclick="confirmSelection()">确定</button>
</div>
</div>
</div>
</div>
</div>
`;
}
@ -24,6 +88,12 @@ export function getContextButtonContent(): string {
*/
export function getContextButtonStyles(): string {
return `
/* 上下文选择器容器 */
.context-selector-wrapper {
position: relative;
display: inline-block;
}
/* 添加上下文按钮样式 */
.add-context-button {
display: flex;
@ -45,15 +115,218 @@ export function getContextButtonStyles(): string {
border-color: var(--vscode-focusBorder);
}
.add-context-button svg {
.add-context-button svg.icon {
width: 16px;
height: 16px;
color: #409eff;
}
.add-context-button .dropdown-arrow {
width: 12px;
height: 12px;
transition: transform 0.2s ease;
}
.add-context-button.active .dropdown-arrow {
transform: rotate(180deg);
}
.add-context-label {
white-space: nowrap;
}
/* 上拉菜单样式 */
.context-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
min-width: 180px;
z-index: 1000;
display: none;
overflow: hidden;
}
.context-menu.show {
display: block;
animation: slideUp 0.2s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.context-menu-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s ease;
color: var(--vscode-foreground);
}
.context-menu-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-menu-item svg {
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--vscode-foreground);
opacity: 0.8;
}
.context-menu-item span {
font-size: 13px;
white-space: nowrap;
flex: 1;
}
.context-menu-item .arrow-right {
width: 14px;
height: 14px;
opacity: 0.6;
margin-left: auto;
}
/* 列表视图样式 */
.context-menu-list {
display: flex;
flex-direction: column;
max-height: 350px;
}
.context-menu-list-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--vscode-panel-border);
}
.context-menu-back {
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--vscode-foreground);
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.context-menu-back:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.context-menu-back svg {
width: 16px;
height: 16px;
}
.context-menu-list-header span {
font-size: 14px;
font-weight: 500;
flex: 1;
}
.context-menu-list-body {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.context-menu-list-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s ease;
}
.context-menu-list-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-menu-list-item.selected {
background: var(--vscode-list-activeSelectionBackground);
}
.context-menu-list-item input[type="checkbox"] {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.context-menu-list-item label {
flex: 1;
font-size: 12px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-menu-list-footer {
padding: 8px 12px;
border-top: 1px solid var(--vscode-panel-border);
display: flex;
flex-direction: column;
gap: 8px;
}
.context-menu-list-footer input {
width: 100%;
padding: 6px 10px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
color: var(--vscode-input-foreground);
font-size: 12px;
box-sizing: border-box;
}
.context-menu-list-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.context-menu-list-footer span {
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.context-menu-list-footer button {
padding: 4px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.context-menu-list-footer button:hover {
background: var(--vscode-button-hoverBackground);
}
`;
}
@ -62,10 +335,174 @@ export function getContextButtonStyles(): string {
*/
export function getContextButtonScript(): string {
return `
// 添加上下文处理函数
function handleAddContext() {
// 发送添加上下文请求到扩展
vscode.postMessage({ command: 'addContext' });
// 上下文菜单状态
let currentListData = [];
let currentListType = '';
let selectedItems = new Set();
// 切换上下文菜单显示/隐藏
function toggleContextMenu() {
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (menu && button) {
const isShown = menu.classList.contains('show');
if (isShown) {
menu.classList.remove('show');
button.classList.remove('active');
backToMainMenu(); // 关闭时回到主菜单
} else {
menu.classList.add('show');
button.classList.add('active');
}
}
}
// 点击外部关闭菜单
document.addEventListener('click', function(event) {
const wrapper = document.querySelector('.context-selector-wrapper');
const menu = document.getElementById('contextMenu');
const button = document.querySelector('.add-context-button');
if (wrapper && menu && button && !wrapper.contains(event.target)) {
menu.classList.remove('show');
button.classList.remove('active');
backToMainMenu();
}
});
// 显示文件列表
function showFileList() {
vscode.postMessage({ command: 'addContextFile' });
}
// 显示文件夹列表
function showFolderList() {
vscode.postMessage({ command: 'addContextFolder' });
}
// 返回主菜单
function backToMainMenu() {
const mainMenu = document.getElementById('contextMenuMain');
const listView = document.getElementById('contextMenuList');
if (mainMenu && listView) {
mainMenu.style.display = 'block';
listView.style.display = 'none';
}
selectedItems.clear();
currentListData = [];
}
// 切换到列表视图
function switchToListView(title, type, data) {
const mainMenu = document.getElementById('contextMenuMain');
const listView = document.getElementById('contextMenuList');
const titleEl = document.getElementById('contextMenuListTitle');
if (mainMenu && listView && titleEl) {
mainMenu.style.display = 'none';
listView.style.display = 'flex';
titleEl.textContent = title;
currentListType = type;
currentListData = data;
selectedItems.clear();
renderList(data);
updateSelectedCount();
}
}
// 渲染列表
function renderList(data) {
const body = document.getElementById('contextMenuListBody');
if (!body) return;
body.innerHTML = data.map((item, index) => \`
<div class="context-menu-list-item" onclick="toggleItemSelection(\${index})">
<input type="checkbox" id="item-\${index}" />
<label for="item-\${index}">\${item.relativePath}</label>
</div>
\`).join('');
}
// 切换项选择
function toggleItemSelection(index) {
const checkbox = document.getElementById('item-' + index);
const item = document.querySelectorAll('.context-menu-list-item')[index];
if (checkbox && item) {
checkbox.checked = !checkbox.checked;
if (checkbox.checked) {
selectedItems.add(index);
item.classList.add('selected');
} else {
selectedItems.delete(index);
item.classList.remove('selected');
}
updateSelectedCount();
}
}
// 更新选中数量
function updateSelectedCount() {
const countEl = document.getElementById('contextMenuListCount');
if (countEl) {
countEl.textContent = '已选择 ' + selectedItems.size + ' 项';
}
}
// 确认选择
function confirmSelection() {
const selected = Array.from(selectedItems).map(index => currentListData[index]);
if (selected.length > 0) {
selected.forEach(item => {
addContextItem(currentListType, item.path);
});
}
toggleContextMenu();
}
// 添加图片
function handleAddImage() {
vscode.postMessage({ command: 'addContextImage' });
toggleContextMenu();
}
// 添加文档
function handleAddDocument() {
vscode.postMessage({ command: 'addContextDocument' });
toggleContextMenu();
}
// 搜索功能
const searchInput = document.getElementById('contextMenuSearch');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const keyword = e.target.value.toLowerCase();
const filtered = currentListData.filter(item =>
item.relativePath.toLowerCase().includes(keyword)
);
renderList(filtered);
});
}
// 处理后端消息
window.addEventListener('message', event => {
const message = event.data;
if (message.command === 'showWorkspaceFileList') {
switchToListView('选择文件', 'file', message.files);
} else if (message.command === 'showWorkspaceFolderList') {
switchToListView('选择文件夹', 'folder', message.folders);
}
});
`;
}

225
src/views/contextDisplay.ts Normal file
View File

@ -0,0 +1,225 @@
/**
* 上下文显示组件
* 用于显示已选择的文件、文件夹、图片和文档
*/
/**
* 获取上下文显示区域的 HTML 内容
*/
export function getContextDisplayContent(): string {
return `
<div class="context-display-area" id="contextDisplayArea" style="display: none;">
<div class="context-items-container" id="contextItemsContainer">
<!-- 动态添加的上下文项将显示在这里 -->
</div>
</div>
`;
}
/**
* 获取上下文显示区域的样式
*/
export function getContextDisplayStyles(): string {
return `
/* 上下文显示区域 */
.context-display-area {
margin-bottom: 8px;
padding: 8px;
background: rgba(128, 128, 128, 0.1);
border-radius: 6px;
border: 1px solid var(--vscode-input-border);
}
.context-items-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* 上下文项样式 */
.context-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-size: 12px;
color: var(--vscode-foreground);
max-width: 300px;
transition: all 0.2s ease;
}
.context-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-item svg {
width: 14px;
height: 14px;
flex-shrink: 0;
opacity: 0.8;
}
.context-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-item-remove {
width: 14px;
height: 14px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease;
flex-shrink: 0;
}
.context-item-remove:hover {
opacity: 1;
color: #f56c6c;
}
/* 图片预览样式 */
.context-item.image-item {
position: relative;
}
.context-item-preview {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 3px;
border: 1px solid var(--vscode-input-border);
}
`;
}
/**
* 获取上下文显示区域的脚本
*/
export function getContextDisplayScript(): string {
return `
// 存储上下文项
let contextItems = [];
// 获取文件图标 SVG
function getFileIcon() {
return '<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.7z" fill="currentColor"/></svg>';
}
// 获取文件夹图标 SVG
function getFolderIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M880 298.4H521L403.7 186.2c-1.5-1.4-3.5-2.2-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取图片图标 SVG
function getImageIcon() {
return '<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" fill="currentColor"/></svg>';
}
// 获取文档图标 SVG
function getDocumentIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32z" fill="currentColor"/></svg>';
}
// 获取删除图标 SVG
function getRemoveIcon() {
return '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" fill="currentColor"/></svg>';
}
// 提取文件名
function getFileName(path) {
return path.split(/[\\\\/]/).pop();
}
// 添加上下文项
function addContextItem(type, path) {
const id = Date.now() + Math.random();
contextItems.push({ id, type, path });
renderContextItems();
}
// 删除上下文项
function removeContextItem(id) {
contextItems = contextItems.filter(item => item.id !== id);
renderContextItems();
}
// 渲染上下文项
function renderContextItems() {
const container = document.getElementById('contextItemsContainer');
const displayArea = document.getElementById('contextDisplayArea');
if (!container || !displayArea) return;
if (contextItems.length === 0) {
displayArea.style.display = 'none';
return;
}
displayArea.style.display = 'block';
container.innerHTML = contextItems.map(item => {
let icon = '';
switch(item.type) {
case 'file': icon = getFileIcon(); break;
case 'folder': icon = getFolderIcon(); break;
case 'image': icon = getImageIcon(); break;
case 'document': icon = getDocumentIcon(); break;
}
return \`
<div class="context-item" title="\${item.path}">
\${icon}
<span class="context-item-name">\${getFileName(item.path)}</span>
<span class="context-item-remove" onclick="removeContextItem(\${item.id})">
\${getRemoveIcon()}
</span>
</div>
\`;
}).join('');
}
// 处理后端返回的文件选择结果
window.addEventListener('message', event => {
const message = event.data;
switch(message.command) {
case 'contextFilesSelected':
if (message.files && message.files.length > 0) {
message.files.forEach(file => addContextItem('file', file));
}
break;
case 'contextFoldersSelected':
if (message.folders && message.folders.length > 0) {
message.folders.forEach(folder => addContextItem('folder', folder));
}
break;
case 'contextImagesSelected':
if (message.images && message.images.length > 0) {
message.images.forEach(image => addContextItem('image', image));
}
break;
case 'contextDocumentsSelected':
if (message.documents && message.documents.length > 0) {
message.documents.forEach(doc => addContextItem('document', doc));
}
break;
}
});
// 获取所有上下文项(供发送消息时使用)
window.getContextItems = function() {
return contextItems;
};
// 清空上下文项(供清空对话时使用)
window.clearContextItems = function() {
contextItems = [];
renderContextItems();
};
`;
}

View File

@ -2,60 +2,63 @@ import { getWaveformPreviewContent } from "./waveformPreviewContent";
import {
getModelSelectorContent,
getModelSelectorStyles,
getModelSelectorScript
getModelSelectorScript,
} from "./modelSelector";
import {
getModeSelectorContent,
getModeSelectorStyles,
getModeSelectorScript
getModeSelectorScript,
} from "./agentModeSelector";
import {
getContextButtonContent,
getContextButtonStyles,
getContextButtonScript
getContextButtonScript,
} from "./contextButton";
import {
getContextDisplayContent,
getContextDisplayStyles,
getContextDisplayScript,
} from "./contextDisplay";
import {
getContextCompressContent,
getContextCompressStyles,
getContextCompressScript
getContextCompressScript,
} from "./contextCompress";
import {
getPlanToggleContent,
getPlanToggleStyles,
getPlanToggleScript
} from "./planToggle";
import {
getOptimizeButtonContent,
getOptimizeButtonStyles,
getOptimizeButtonScript
getOptimizeButtonScript,
} from "./optimizeButton";
import {
sendIconSvg,
stopIconSvg
} from "../constants/toolIcons";
import { sendIconSvg, stopIconSvg } from "../constants/toolIcons";
/**
* 获取输入区域的 HTML 内容
*/
export function getInputAreaContent(): string {
export function getInputAreaContent(
autoIcon: string = '',
liteIcon: string = '',
syIcon: string = '',
maxIcon: string = ''
): string {
return `
<div class="input-area">
<div class="input-area centered" id="inputArea">
<div class="input-group">
<div class="input-wrapper">
<!-- 顶部工具栏 -->
<div class="input-top-toolbar">
${getContextButtonContent()}
${getPlanToggleContent()}
</div>
<!-- 上下文显示区域 -->
${getContextDisplayContent()}
<textarea
id="messageInput"
placeholder="输入您的问题..."
placeholder="输入您的问题,按 Enter 发送Shift + Enter 换行..."
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea>
<div class="input-bottom-row">
<div class="mode-selector">
${getModeSelectorContent()}
${getModelSelectorContent()}
${getModelSelectorContent(autoIcon, liteIcon, syIcon, maxIcon)}
</div>
<div class="input-actions">
${getContextCompressContent()}
@ -80,13 +83,30 @@ export function getInputAreaStyles(): string {
${getModeSelectorStyles()}
${getModelSelectorStyles()}
${getContextButtonStyles()}
${getContextDisplayStyles()}
${getContextCompressStyles()}
${getPlanToggleStyles()}
${getOptimizeButtonStyles()}
.input-area {
border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px;
flex-shrink: 0;
transition: all 0.3s ease;
}
/* 居中模式:未发起对话时 */
.input-area.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% - 40px);
max-width: 800px;
border-top: none;
padding-top: 0;
}
/* 底部模式:发起对话后 */
.input-area.bottom {
position: relative;
transform: none;
}
.input-group {
display: flex;
@ -207,6 +227,11 @@ export function getInputAreaStyles(): string {
overflow-y: auto;
line-height: 1.5;
}
textarea:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(128, 128, 128, 0.1);
}
/* 简洁的滚动条样式 */
textarea::-webkit-scrollbar {
width: 8px;
@ -261,15 +286,36 @@ export function getInputAreaStyles(): string {
*/
export function getInputAreaScript(): string {
return `
${getModeSelectorScript()}
// 注意:getModeSelectorScript() 已在 webviewContent.ts 开头加载,这里不再重复加载
${getModelSelectorScript()}
${getContextButtonScript()}
${getContextDisplayScript()}
${getContextCompressScript()}
${getPlanToggleScript()}
${getOptimizeButtonScript()}
// 对话状态管理
let isConversationActive = false;
let hasMessages = false; // 是否已有消息
// 工作区检测状态
let hasCheckedWorkspace = false; // 是否已经检测过工作区
let hasWorkspace = true; // 工作区状态
// 切换输入框布局模式
function updateInputAreaLayout() {
const inputArea = document.getElementById('inputArea');
if (!inputArea) return;
if (hasMessages) {
// 有消息时,移到底部
inputArea.classList.remove('centered');
inputArea.classList.add('bottom');
} else {
// 无消息时,居中显示
inputArea.classList.add('centered');
inputArea.classList.remove('bottom');
}
}
// 自动调整 textarea 高度
function autoResizeTextarea() {
@ -283,11 +329,16 @@ export function getInputAreaScript(): string {
if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea);
// 监听点击事件,检测工作区状态
messageInput.addEventListener('focus', () => {
if (!hasCheckedWorkspace) {
hasCheckedWorkspace = true;
vscode.postMessage({ command: 'checkWorkspace' });
}
});
// 初始化时调整一次高度
autoResizeTextarea();
// 聚焦到输入框
messageInput.focus();
}
// 切换发送按钮状态
@ -302,11 +353,17 @@ export function getInputAreaScript(): string {
sendIconContainer.style.display = 'none';
stopIconContainer.style.display = 'block';
isConversationActive = true;
// 禁用输入框
messageInput.disabled = true;
messageInput.placeholder = '正在处理中,请稍候...';
} else {
sendButton.classList.remove('sending');
sendIconContainer.style.display = 'block';
stopIconContainer.style.display = 'none';
isConversationActive = false;
// 启用输入框
messageInput.disabled = false;
messageInput.placeholder = '输入您的问题,按 Enter 发送Shift + Enter 换行...';
}
}
@ -326,15 +383,43 @@ export function getInputAreaScript(): string {
const text = messageInput.value.trim();
if (!text) return;
// 如果正在对话中,阻止发送新消息
if (isConversationActive) {
return;
}
// 检查工作区状态
if (!hasWorkspace) {
// 如果没有工作区,阻止发送并清空输入框
messageInput.value = '';
autoResizeTextarea();
return;
}
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
const planMode = document.getElementById('planToggle')?.checked || false;
// 获取上下文项
const contextItems = window.getContextItems ? window.getContextItems() : [];
addMessage(text, 'user');
// 标记已有消息,切换布局到底部
hasMessages = true;
updateInputAreaLayout();
// 切换按钮为暂停状态
setSendButtonState(true);
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
vscode.postMessage({
command: 'sendMessage',
text: text,
mode: mode,
model: model,
planMode: planMode,
contextItems: contextItems
});
messageInput.value = '';
autoResizeTextarea(); // 重置输入框高度
messageInput.focus();
@ -342,5 +427,28 @@ export function getInputAreaScript(): string {
// 重置优化状态
resetOptimizeButton();
}
// 全局函数:重置输入框布局(用于清空对话时)
window.resetInputAreaLayout = function() {
hasMessages = false;
updateInputAreaLayout();
};
// 全局函数:检查是否有消息(用于页面加载时)
window.checkMessagesAndUpdateLayout = function() {
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
const messageElements = messagesContainer.querySelectorAll('.message');
hasMessages = messageElements.length > 0;
updateInputAreaLayout();
}
};
// 页面加载时检查消息状态
setTimeout(() => {
if (window.checkMessagesAndUpdateLayout) {
window.checkMessagesAndUpdateLayout();
}
}, 100);
`;
}

View File

@ -13,13 +13,28 @@
import {
collapseIconSvg,
fileWriteIconSvg,
fileReadIconSvg,
fileDeleteIconSvg,
syntaxCheckIconSvg,
SearchCode,
agentIconSvg,
saveKnowledgeIconSvg,
simulationIconSvg,
waveformIconSvg,
knowledgeLoadIconSvg,
stateTransitionIconSvg,
userQuestionIconSvg,
} from "../constants/toolIcons";
import {
getWaveformPreviewContent,
getWaveformPreviewScript,
} from "./waveformPreviewContent";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import { getPlanCardStyles, getPlanCardScript } from "./planCard";
import {
getCodeHighlightStyles,
getCodeHighlightScript,
} from "../components/codeHighlight";
/**
* 获取消息区域的 HTML 内容
@ -284,7 +299,7 @@ export function getMessageAreaStyles(): string {
padding: 0;
}
.message-segment {
padding: 10px 22px;
padding: 10px 0;
}
.segment-text {
line-height: 1.6;
@ -293,76 +308,80 @@ export function getMessageAreaStyles(): string {
/* Markdown 样式 */
.segment-text h1,
.segment-text h2,
.segment-text h3 {
margin: 16px 0 8px 0;
.segment-text h3,
.question-text h1,
.question-text h2,
.question-text h3 {
margin: 0px 0 -10px 0;
font-weight: 600;
line-height: 1.3;
}
.segment-text h1 {
.segment-text h1,
.question-text h1 {
font-size: 1.5em;
border-bottom: 1px solid var(--vscode-panel-border);
padding-bottom: 8px;
}
.segment-text h2 {
.segment-text h2,
.question-text h2 {
font-size: 1.3em;
}
.segment-text h3 {
.segment-text h3,
.question-text h3 {
font-size: 1.1em;
}
.segment-text pre {
background: var(--vscode-textCodeBlock-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 6px;
padding: 12px;
overflow-x: auto;
margin: 12px 0;
}
.segment-text code {
font-family: 'Courier New', Consolas, monospace;
font-size: 0.9em;
}
.segment-text pre code {
background: transparent;
padding: 0;
border: none;
}
.segment-text code:not(pre code) {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
color: var(--vscode-textPreformat-foreground);
}
.segment-text ul,
.segment-text ol {
.segment-text ol,
.question-text ul,
.question-text ol {
margin: 8px 0;
padding-left: 24px;
}
.segment-text li {
margin: 4px 0;
line-height: 1.6;
.segment-text li,
.question-text li {
line-height: 1;
}
.segment-text strong {
.segment-text strong,
.question-text strong {
font-weight: 600;
color: var(--vscode-foreground);
}
.segment-text em {
.segment-text em,
.question-text em {
font-style: italic;
}
.segment-text a {
.segment-text a,
.question-text a {
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
.segment-text a:hover {
.segment-text a:hover,
.question-text a:hover {
text-decoration: underline;
}
.segment-text p {
.segment-text p,
.question-text p {
margin: 8px 0;
}
.segment-text code,
.question-text code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 0.9em;
}
.segment-tool {
margin: 4px 0;
padding: 4px 0;
}
/* 低调显示的工具调用 - 移除边距和背景 */
.segment-tool.low-profile {
margin: 2px 0px;
padding: 0;
background: none;
}
.tool-segment-header {
display: flex;
align-items: center;
@ -400,7 +419,7 @@ export function getMessageAreaStyles(): string {
display: block;
}
.tool-segment-header.collapsed .tool-collapse-icon {
transform: rotate(0deg);
transform: rotate(-90deg);
}
.tool-segment-header:not(.collapsed) .tool-collapse-icon {
transform: rotate(0deg);
@ -416,6 +435,28 @@ export function getMessageAreaStyles(): string {
height: 100%;
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 {
width: 16px;
height: 16px;
@ -438,6 +479,61 @@ export function getMessageAreaStyles(): string {
height: 100%;
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 {
overflow: hidden;
transition: max-height 0.3s ease;
@ -445,11 +541,28 @@ export function getMessageAreaStyles(): string {
.tool-segment-content.collapsed {
max-height: 0;
}
/* 低调显示的工具调用样式 */
.segment-tool.low-profile .tool-segment-header {
opacity: 0.65;
font-size: 12px;
}
.segment-tool.low-profile .tool-segment-icon {
opacity: 0.55;
font-size: 11px;
}
.segment-tool.low-profile .tool-segment-name {
font-weight: 300;
opacity: 0.8;
}
.segment-tool.low-profile .tool-segment-result {
opacity: 0.7;
font-size: 10px;
}
.segment-question {
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
padding: 12px 14px;
padding: 12px 35px;
border-left: 3px solid var(--vscode-charts-orange);
}
.segment-question .question-text {
@ -497,6 +610,7 @@ export function getMessageAreaStyles(): string {
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
font-size: 13px;
margin-left: -20px;
}
.segment-question .custom-submit {
padding: 8px 16px;
@ -528,6 +642,12 @@ export function getMessageAreaStyles(): string {
border-radius: 4px;
font-size: 12px;}
${getAgentCardStyles()}
${getPlanCardStyles()}
${getCodeHighlightStyles()}
${getWaveformPreviewContent()}
`;
}
@ -540,18 +660,79 @@ export function getMessageAreaScript(): string {
// 工具图标定义
const collapseIconSvg = \`${collapseIconSvg}\`;
const fileWriteIconSvg = \`${fileWriteIconSvg}\`;
const fileReadIconSvg = \`${fileReadIconSvg}\`;
const fileDeleteIconSvg = \`${fileDeleteIconSvg}\`;
const syntaxCheckIconSvg = \`${syntaxCheckIconSvg}\`;
const searchCodeIconSvg = \`${SearchCode}\`;
const saveKnowledgeIconSvg = \`${saveKnowledgeIconSvg}\`;
const simulationIconSvg = \`${simulationIconSvg}\`;
const waveformIconSvg = \`${waveformIconSvg}\`;
const knowledgeLoadIconSvg = \`${knowledgeLoadIconSvg}\`;
const stateTransitionIconSvg = \`${stateTransitionIconSvg}\`;
const userQuestionIconSvg = \`${userQuestionIconSvg}\`;
${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,
'askUser': userQuestionIconSvg,
};
return iconMap[toolName] || '';
}
// 工具名称映射
function getToolDisplayName(toolName) {
const toolNameMap = {
'file_read': '已完成文件读取',
'file_write': '已完成文件写入',
'file_delete': '已完成文件删除',
'file_list': '已检索代码文件',
'syntax_check': '已完成语法检查',
'simulation': '已完成仿真',
'waveform_summary': '已完成波形分析'
'waveform_summary': '已完成波形分析',
'knowledge_save': '已保存知识库',
'knowledge_load': '已加载知识库',
'queryKnowledgeSummary': '已查询知识摘要',
'queryRules': '已查询规则',
'setModule': '已设置模块',
'addSignal': '信号分析完成',
'addSignalExample': '信号示例处理完成',
'validateKnowledgeGraph': '已验证知识图谱',
'querySignals': '已查询信号',
'addPlan': '已添加计划',
'addEdge': '已添加边',
'showPlan': '已显示计划',
'addRule': '已添加规则',
'updateNode': '已更新节点',
'addStateTransition': '已添加状态转换',
'spawnExplorer': '代码探索',
'spawnDebugger': '波形调试',
'askUser': '用户提问',
};
return toolNameMap[toolName] || toolName;
}
@ -742,9 +923,13 @@ export function getMessageAreaScript(): string {
// 存储已回答问题的状态
const answeredQuestions = new Map(); // askId -> answer
// 存储工具展开/折叠状态
const toolCollapseStates = new Map(); // index -> isCollapsed
// 实时更新分段消息(按后端返回顺序)
function updateSegmentsRealtime(segments, isComplete) {
console.log('[WebView] updateSegmentsRealtime 被调用, segments:', segments, 'isComplete:', isComplete);
if (!segments || segments.length === 0) {
console.log('[WebView] segments 为空,跳过渲染');
return;
@ -773,10 +958,42 @@ export function getMessageAreaScript(): string {
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 = '';
segments.forEach((segment, index) => {
// 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
let toolIndex = 0; // 用于跟踪工具段落的索引
mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type;
@ -784,22 +1001,35 @@ export function getMessageAreaScript(): string {
segmentDiv.className += ' segment-text';
segmentDiv.innerHTML = formatText(segment.content);
} else if (segment.type === 'tool') {
// 过滤掉不需要显示的工具
if (segment.toolName === 'spawnExplorer') {
return;
}
// 所有工具调用都使用低调样式
segmentDiv.className += ' low-profile';
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60;
// 恢复之前保存的展开/折叠状态
const savedState = toolCollapseStates.get(toolIndex);
const isCollapsed = savedState !== undefined ? savedState : shouldCollapse;
const currentToolIndex = toolIndex;
toolIndex++; // 递增工具索引
segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? collapseIconSvg : ''}
\${!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>
<div class="tool-segment-header\${isCollapsed ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}" data-tool-index="\${currentToolIndex}">
\${shouldCollapse ? \`<span class="tool-collapse-icon">\${collapseIconSvg}</span>\` : getToolIcon(segment.toolName)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</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 +1055,24 @@ export function getMessageAreaScript(): string {
setTimeout(() => {
const header = segmentDiv.querySelector('.tool-segment-header');
const content = segmentDiv.querySelector('.tool-segment-content');
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
if (header && content) {
header.addEventListener('click', function() {
const isCollapsed = header.classList.contains('collapsed');
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
if (isCollapsed) {
// 展开
header.classList.remove('collapsed');
content.classList.remove('collapsed');
content.style.maxHeight = content.scrollHeight + 'px';
if (iconCollapsed) iconCollapsed.style.display = 'none';
if (iconExpanded) iconExpanded.style.display = 'block';
toolCollapseStates.set(toolIdx, false);
} else {
// 折叠
header.classList.add('collapsed');
content.classList.add('collapsed');
content.style.maxHeight = '0';
if (iconCollapsed) iconCollapsed.style.display = 'block';
if (iconExpanded) iconExpanded.style.display = 'none';
toolCollapseStates.set(toolIdx, true);
}
});
}
@ -873,7 +1100,7 @@ export function getMessageAreaScript(): string {
: '';
segmentDiv.innerHTML = \`
<div class="question-text">\${segment.question || ''}</div>
<div class="question-text">\${formatText(segment.question || '')}</div>
\${hasOptions ? \`<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="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
@ -916,6 +1143,12 @@ export function getMessageAreaScript(): string {
}
}, 0);
}
} else if (segment.type === 'agent') {
// 智能体卡片渲染
renderAgentCard(segment, segmentDiv);
} else if (segment.type === 'plan') {
// 计划卡片渲染(使用独立组件)
renderPlanCardInSegment(segment, segmentDiv, answeredQuestions);
}
currentSegmentedMessage.appendChild(segmentDiv);
@ -926,9 +1159,11 @@ export function getMessageAreaScript(): string {
console.log('[WebView] 对话完成,添加操作按钮');
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button');
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 = () => {
const textContent = segments
.filter(s => s.type === 'text' && s.content)
@ -936,7 +1171,22 @@ export function getMessageAreaScript(): string {
.join('\\n');
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(likeBtn);
actionsDiv.appendChild(dislikeBtn);
currentSegmentedMessage.appendChild(actionsDiv);
// 重置当前分段消息容器
@ -974,7 +1224,29 @@ export function getMessageAreaScript(): string {
const container = document.createElement('div');
container.className = 'message bot-message segmented-message';
segments.forEach((segment, index) => {
// 合并连续相同的工具调用
const mergedSegments = [];
let i = 0;
while (i < segments.length) {
const segment = segments[i];
if (segment.type === 'tool') {
// 统计连续相同的工具调用
let count = 1;
while (i + count < segments.length &&
segments[i + count].type === 'tool' &&
segments[i + count].toolName === segment.toolName) {
count++;
}
// 添加合并后的段落(带计数)
mergedSegments.push({ ...segment, toolCount: count });
i += count;
} else {
mergedSegments.push(segment);
i++;
}
}
mergedSegments.forEach((segment, index) => {
const segmentDiv = document.createElement('div');
segmentDiv.className = 'message-segment segment-' + segment.type;
@ -982,19 +1254,26 @@ export function getMessageAreaScript(): string {
segmentDiv.className += ' segment-text';
segmentDiv.innerHTML = formatText(segment.content);
} else if (segment.type === 'tool') {
// 过滤掉不需要显示的工具
if (segment.toolName === 'spawnExplorer') {
return;
}
// 所有工具调用都使用低调样式
segmentDiv.className += ' low-profile';
const statusIcon = segment.toolStatus === 'error' ? '❌' : '🔧';
const toolResult = segment.toolResult || '';
const toolCount = segment.toolCount || 1;
const countSuffix = toolCount > 1 ? \` x\${toolCount}\` : '';
// 检查工具结果是否过长(超过一行显示不下)
const shouldCollapse = toolResult && toolResult.length > 60;
segmentDiv.innerHTML = \`
<div class="tool-segment-header\${shouldCollapse ? ' collapsed' : ''}" data-collapsible="\${shouldCollapse}">
\${shouldCollapse ? collapseIconSvg : ''}
\${!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>
\${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)}
<span class="tool-segment-name">\${getToolDisplayName(segment.toolName) || '工具'}\${countSuffix}</span>
\${toolResult && !shouldCollapse ? \`<span class="tool-segment-result">\${toolResult}</span>\` : ''}
</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>\` : ''}
@ -1023,27 +1302,24 @@ export function getMessageAreaScript(): string {
setTimeout(() => {
const header = segmentDiv.querySelector('.tool-segment-header');
const content = segmentDiv.querySelector('.tool-segment-content');
const iconCollapsed = segmentDiv.querySelector('.icon-collapsed');
const iconExpanded = segmentDiv.querySelector('.icon-expanded');
if (header && content) {
header.addEventListener('click', function() {
const isCollapsed = header.classList.contains('collapsed');
const toolIdx = parseInt(header.getAttribute('data-tool-index') || '0');
if (isCollapsed) {
// 展开
header.classList.remove('collapsed');
content.classList.remove('collapsed');
content.style.maxHeight = content.scrollHeight + 'px';
if (iconCollapsed) iconCollapsed.style.display = 'none';
if (iconExpanded) iconExpanded.style.display = 'block';
toolCollapseStates.set(toolIdx, false);
} else {
// 折叠
header.classList.add('collapsed');
content.classList.add('collapsed');
content.style.maxHeight = '0';
if (iconCollapsed) iconCollapsed.style.display = 'block';
if (iconExpanded) iconExpanded.style.display = 'none';
toolCollapseStates.set(toolIdx, true);
}
});
}
@ -1052,12 +1328,18 @@ export function getMessageAreaScript(): string {
} else if (segment.type === 'question') {
segmentDiv.innerHTML = \`
<div class="question-segment">
<div class="question-text">\${segment.question || ''}</div>
<div class="question-text">\${formatText(segment.question || '')}</div>
<div class="question-options">
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
</div>
</div>
\`;
} else if (segment.type === 'agent') {
// 智能体卡片渲染
renderAgentCard(segment, segmentDiv);
} else if (segment.type === 'plan') {
// 计划卡片渲染(使用独立组件)
renderPlanCardStatic(segment, segmentDiv);
}
container.appendChild(segmentDiv);
@ -1066,9 +1348,11 @@ export function getMessageAreaScript(): string {
// 添加操作按钮
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
// 复制按钮
const copyBtn = document.createElement('button');
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 = () => {
const textContent = segments
.filter(s => s.type === 'text' && s.content)
@ -1076,7 +1360,22 @@ export function getMessageAreaScript(): string {
.join('\\n');
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(likeBtn);
actionsDiv.appendChild(dislikeBtn);
container.appendChild(actionsDiv);
messagesEl.appendChild(container);
@ -1087,20 +1386,40 @@ export function getMessageAreaScript(): string {
function formatText(text) {
if (!text) return '';
// 先转义 HTML 特殊字符
let html = text
let html = text;
// 先提取并处理代码块(避免被转义)
const codeBlocks = [];
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
const language = lang || 'plaintext';
// 转义代码内容
const escapedCode = code.trim()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 处理代码块(三个反引号包裹的代码)
html = html.replace(/\`\`\`(\\w+)?\\n([\\s\\S]*?)\`\`\`/g, function(match, lang, code) {
const language = lang || 'plaintext';
return '<pre><code class="language-' + language + '">' + code.trim() + '</code></pre>';
// 不再手动高亮,让 highlight.js 处理
const placeholder = \`___CODE_BLOCK_\${codeBlocks.length}___\`;
codeBlocks.push('<pre><code class="language-' + language + '">' + escapedCode + '</code></pre>');
return placeholder;
});
// 处理行内代码(单个反引号包裹
html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
// 提取行内代码(避免被转义
const inlineCodes = [];
html = html.replace(/\`([^\`]+)\`/g, function(match, code) {
const escapedCode = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
const placeholder = \`___INLINE_CODE_\${inlineCodes.length}___\`;
inlineCodes.push('<code>' + escapedCode + '</code>');
return placeholder;
});
// 转义其他 HTML 特殊字符
html = html
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 处理标题 ### Title
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
@ -1123,9 +1442,19 @@ export function getMessageAreaScript(): string {
// 处理链接 [text](url)
html = html.replace(/\\[([^\\]]+)\\]\\(([^\\)]+)\\)/g, '<a href="$2" target="_blank">$1</a>');
// 处理换行
// 处理换行(在恢复代码块之前)
html = html.replace(/\\n/g, '<br>');
// 恢复代码块(在最后恢复,避免被其他处理影响)
codeBlocks.forEach((block, index) => {
html = html.replace(\`___CODE_BLOCK_\${index}___\`, block);
});
// 恢复行内代码
inlineCodes.forEach((code, index) => {
html = html.replace(\`___INLINE_CODE_\${index}___\`, code);
});
return html;
}
@ -1275,5 +1604,7 @@ export function getMessageAreaScript(): string {
}
${getWaveformPreviewScript()}
${getCodeHighlightScript()}
`;
}

View File

@ -5,7 +5,12 @@
/**
* 获取模型选择器的 HTML 内容
*/
export function getModelSelectorContent(): string {
export function getModelSelectorContent(
autoIcon: string = "",
liteIcon: string = "",
syIcon: string = "",
maxIcon: string = ""
): string {
return `
<!-- 模型选择 -->
<div class="tooltip">
@ -17,13 +22,51 @@ export function getModelSelectorContent(): string {
</svg>
</div>
<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')">Auto</div>
<div class="select-option" data-value="syntaxic" data-tooltip="语法分析和代码理解" onclick="selectModel('syntaxic', 'Syntaxic')">Syntaxic</div>
<div class="select-option" data-value="max" data-tooltip="最强性能,复杂任务" onclick="selectModel('max', 'Max')">Max</div>
<div class="select-option selected" data-value="auto" onclick="selectModel('auto', 'Auto')">
${
autoIcon
? `<img src="${autoIcon}" class="model-icon" alt="Auto">`
: ""
}
<div class="option-content">
<span class="option-label">Auto</span>
<span class="option-desc">智能匹配最优模型</span>
</div>
</div>
<div class="select-option" data-value="lite" onclick="selectModel('lite', 'Lite')">
${
liteIcon
? `<img src="${liteIcon}" class="model-icon" alt="Lite">`
: ""
}
<div class="option-content">
<span class="option-label">Lite</span>
<span class="option-desc">基础模型,快速相应,适合简单任务</span>
</div>
</div>
<div class="select-option" data-value="syntaxic" onclick="selectModel('syntaxic', 'Syntaxic')">
${
syIcon
? `<img src="${syIcon}" class="model-icon" alt="Syntaxic">`
: ""
}
<div class="option-content">
<span class="option-label">Syntaxic</span>
<span class="option-desc">均衡成本和性能节省credits同时保持可靠输出</span>
</div>
</div>
<div class="select-option" data-value="max" onclick="selectModel('max', 'Max')">
${
maxIcon
? `<img src="${maxIcon}" class="model-icon" alt="Max">`
: ""
}
<div class="option-content">
<span class="option-label">Max</span>
<span class="option-desc">最强性能,质量优先,适合复杂任务</span>
</div>
</div>
</div>
<!-- 模型选择器的 tooltip 容器 -->
<div id="modelTooltip" class="model-tooltip"></div>
</div>
<span class="tooltiptext">选择模型</span>
</div>
@ -87,11 +130,13 @@ export function getModelSelectorStyles(): string {
/* 模型选择器的选项样式 */
#modelDropdown .select-option {
position: relative;
padding: 6px 12px;
font-size: 12px;
padding: 8px 12px;
cursor: pointer;
transition: background 0.2s ease;
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
#modelDropdown .select-option:hover {
background: rgba(128, 128, 128, 0.3);
@ -100,49 +145,28 @@ export function getModelSelectorStyles(): string {
background: rgba(128, 128, 128, 0.5);
color: var(--vscode-foreground);
}
/* 模型选择器的 tooltip 样式 */
.model-tooltip {
position: fixed;
background: #1e1e1e;
color: #ffffff;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
.model-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
object-fit: contain;
}
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.option-label {
font-size: 13px;
color: var(--vscode-foreground);
font-weight: 500;
white-space: nowrap;
pointer-events: none;
z-index: 10000;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.model-tooltip.show {
opacity: 1;
visibility: visible;
}
/* tooltip 箭头 */
.model-tooltip::before {
content: "";
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border-width: 7px;
border-style: solid;
border-color: transparent rgba(255, 255, 255, 0.2) transparent transparent;
z-index: -1;
}
.model-tooltip::after {
content: "";
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border-width: 6px;
border-style: solid;
border-color: transparent #1e1e1e transparent transparent;
margin-right: 1px;
.option-desc {
font-size: 11px;
color: var(--vscode-descriptionForeground);
white-space: nowrap;
}
`;
}
@ -205,46 +229,5 @@ export function getModelSelectorScript(): string {
function getCurrentModel() {
return currentModel;
}
// 模型选择器 tooltip 功能
(function initModelTooltip() {
const modelDropdown = document.getElementById('modelDropdown');
const modelTooltip = document.getElementById('modelTooltip');
if (!modelDropdown || !modelTooltip) return;
// 为每个选项添加鼠标事件
const options = modelDropdown.querySelectorAll('.select-option');
options.forEach(option => {
option.addEventListener('mouseenter', function(e) {
const tooltipText = this.getAttribute('data-tooltip');
if (!tooltipText) return;
// 设置 tooltip 内容
modelTooltip.textContent = tooltipText;
// 获取选项的位置
const rect = this.getBoundingClientRect();
// 计算 tooltip 位置(在选项右侧)
const tooltipRect = modelTooltip.getBoundingClientRect();
const left = rect.right + 12;
const top = rect.top + (rect.height / 2) - (tooltipRect.height / 2);
// 设置位置
modelTooltip.style.left = left + 'px';
modelTooltip.style.top = top + 'px';
// 显示 tooltip
modelTooltip.classList.add('show');
});
option.addEventListener('mouseleave', function() {
// 隐藏 tooltip
modelTooltip.classList.remove('show');
});
});
})();
`;
}

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

@ -0,0 +1,731 @@
/**
* 计划卡片组件
*
* 功能说明:
* - 显示执行计划的卡片界面
* - 包含计划标题、摘要和步骤列表
* - 摘要支持 Markdown 格式渲染
* - 提供确认执行、修改计划、取消等操作按钮
*/
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-foreground);
margin-bottom: 12px;
font-size: 13px;
line-height: 1.6;
}
/* Markdown 渲染样式 */
.plan-summary h1, .plan-summary h2, .plan-summary h3, .plan-summary h4 {
margin: 16px 0 8px 0;
font-weight: 600;
color: var(--vscode-foreground);
}
.plan-summary h1 { font-size: 18px; border-bottom: 1px solid var(--vscode-input-border); padding-bottom: 6px; }
.plan-summary h2 { font-size: 16px; }
.plan-summary h3 { font-size: 14px; }
.plan-summary h4 { font-size: 13px; }
.plan-summary p { margin: 8px 0; }
.plan-summary ul, .plan-summary ol {
margin: 8px 0;
padding-left: 24px;
}
.plan-summary li { margin: 4px 0; }
.plan-summary code {
background: var(--vscode-textCodeBlock-background);
padding: 2px 6px;
border-radius: 3px;
font-family: var(--vscode-editor-font-family);
font-size: 12px;
}
.plan-summary pre {
background: var(--vscode-textCodeBlock-background);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin: 8px 0;
}
.plan-summary pre code {
background: none;
padding: 0;
}
.plan-summary table {
border-collapse: collapse;
width: 100%;
margin: 8px 0;
font-size: 12px;
}
.plan-summary th, .plan-summary td {
border: 1px solid var(--vscode-input-border);
padding: 6px 10px;
text-align: left;
}
.plan-summary th {
background: var(--vscode-sideBar-background);
font-weight: 600;
}
.plan-summary strong { font-weight: 600; }
.plan-summary em { font-style: italic; }
.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 strong {
color: var(--vscode-textLink-foreground);
}
.step-details {
margin-top: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
line-height: 1.4;
}
.plan-step:last-child {
margin-bottom: 0;
}
.step-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-right: 8px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 4px;
background: transparent;
flex-shrink: 0;
opacity: 0.6;
transition: all 0.2s ease;
}
.step-checkbox.completed {
background: var(--vscode-textLink-foreground);
border-color: var(--vscode-textLink-foreground);
opacity: 1;
}
.step-checkbox.completed::after {
content: '✓';
color: var(--vscode-editor-background);
font-size: 11px;
font-weight: bold;
}
.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);
}
/* 阶段进度条样式 */
.phase-progress {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--vscode-sideBar-background);
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.phase-item.current {
color: var(--vscode-textLink-foreground);
font-weight: 600;
}
.phase-item.completed {
color: #4caf50;
}
.phase-item.skipped {
color: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vscode-input-border);
flex-shrink: 0;
}
.phase-dot.current {
background: var(--vscode-textLink-foreground);
box-shadow: 0 0 0 3px rgba(0, 122, 204, 0.2);
}
.phase-dot.completed {
background: #4caf50;
}
.phase-dot.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.5;
}
.phase-line {
flex: 1;
height: 2px;
background: var(--vscode-input-border);
margin: 0 8px;
}
.phase-line.completed {
background: #4caf50;
}
/* 阶段列表样式 */
.plan-phases {
font-size: 13px;
}
.plan-phase {
margin-bottom: 12px;
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
overflow: hidden;
}
.plan-phase:last-child {
margin-bottom: 0;
}
.phase-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--vscode-list-hoverBackground);
cursor: pointer;
user-select: none;
}
.phase-header:hover {
background: var(--vscode-list-activeSelectionBackground);
}
.phase-toggle {
font-size: 10px;
color: var(--vscode-descriptionForeground);
transition: transform 0.2s;
}
.phase-toggle.expanded {
transform: rotate(90deg);
}
.phase-name {
flex: 1;
font-weight: 500;
}
.phase-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.phase-status.current {
background: var(--vscode-textLink-foreground);
color: white;
}
.phase-status.skipped {
background: var(--vscode-descriptionForeground);
opacity: 0.6;
}
.phase-status.completed {
background: #4caf50;
color: white;
}
.phase-content {
padding: 0 12px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.phase-content.expanded {
padding: 12px;
max-height: 500px;
}
.phase-reason {
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-style: italic;
margin-bottom: 8px;
}
.phase-steps {
margin: 0;
padding: 0;
list-style: none;
}
.phase-step-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid var(--vscode-input-border);
}
.phase-step-item:last-child {
border-bottom: none;
}
.phase-step-checkbox {
width: 14px;
height: 14px;
border: 2px solid var(--vscode-textLink-foreground);
border-radius: 3px;
flex-shrink: 0;
margin-top: 2px;
}
.phase-step-text {
flex: 1;
}
.phase-step-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.phase-step-desc {
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-top: 2px;
}
`;
}
/**
* 获取计划卡片的脚本
*/
export function getPlanCardScript(): string {
return `
// 简单的 Markdown 渲染函数
function renderPlanMarkdown(text) {
if (!text) return '';
let html = text;
// 转义 HTML 特殊字符(保留换行)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 代码块 (\`\`\`code\`\`\`)
html = html.replace(/\\x60\\x60\\x60([\\s\\S]*?)\\x60\\x60\\x60/g, '<pre><code>$1</code></pre>');
// 行内代码 (\`code\`)
html = html.replace(/\\x60([^\\x60]+)\\x60/g, '<code>$1</code>');
// 表格处理
html = html.replace(/^\\|(.+)\\|\\s*\\n\\|[-:\\s|]+\\|\\s*\\n((?:\\|.+\\|\\s*\\n?)+)/gm, function(match, header, body) {
const headers = header.split('|').map(h => h.trim()).filter(h => h);
const rows = body.trim().split('\\n').map(row =>
row.split('|').map(cell => cell.trim()).filter(cell => cell)
);
let table = '<table><thead><tr>';
headers.forEach(h => table += '<th>' + h + '</th>');
table += '</tr></thead><tbody>';
rows.forEach(row => {
table += '<tr>';
row.forEach(cell => table += '<td>' + cell + '</td>');
table += '</tr>';
});
table += '</tbody></table>';
return table;
});
// 标题
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// 粗体和斜体
html = html.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
html = html.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
// 无序列表
html = html.replace(/^[\\s]*[-*] (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\\/li>\\n?)+/g, '<ul>$&</ul>');
// 有序列表
html = html.replace(/^[\\s]*\\d+\\. (.+)$/gm, '<li>$1</li>');
// 段落(连续的非空行)
html = html.replace(/^(?!<[hupolt]|$)(.+)$/gm, '<p>$1</p>');
// 清理多余的空行
html = html.replace(/<p><\\/p>/g, '');
html = html.replace(/\\n{2,}/g, '\\n');
return html;
}
// 解析并渲染步骤列表
function renderPlanSteps(steps) {
if (!steps || steps.length === 0) return '';
// 尝试解析 JSON 格式的步骤
let parsedSteps = steps;
// 如果是单个字符串且看起来像 JSON 数组,尝试解析
if (steps.length === 1 && typeof steps[0] === 'string') {
const str = steps[0].trim();
if (str.startsWith('[') && str.endsWith(']')) {
try {
parsedSteps = JSON.parse(str);
} catch (e) {
// 解析失败,保持原样
}
}
}
return parsedSteps.map((step, i) => {
// 如果是对象,格式化显示
if (typeof step === 'object' && step !== null) {
const name = step.name || step.id || ('步骤 ' + (i + 1));
const desc = step.description || '';
const inputs = step.inputs || '';
const outputs = step.outputs || '';
const logic = step.logic || '';
let content = '<strong>' + name + '</strong>';
if (desc) content += '' + desc;
let details = [];
if (inputs) details.push('输入: ' + inputs);
if (outputs) details.push('输出: ' + outputs);
if (logic) details.push('逻辑: ' + logic);
if (details.length > 0) {
content += '<div class="step-details">' + details.join(' | ') + '</div>';
}
return '<div class="plan-step"><span class="step-checkbox"></span>' + content + '</div>';
}
// 普通字符串
return '<div class="plan-step"><span class="step-checkbox"></span> ' + step + '</div>';
}).join('');
}
// 渲染阶段进度条
function renderPhaseProgress(phases) {
if (!phases || phases.length === 0) return '';
const phaseNames = { spec: 'Spec', design: 'Design', sim: 'Sim', done: 'Done' };
let html = '<div class="phase-progress">';
phases.forEach((phase, i) => {
const name = phaseNames[phase.id] || phase.name || phase.id;
const status = phase.status || 'pending';
html += \`<div class="phase-item \${status}">
<span class="phase-dot \${status}"></span>
<span>\${name}</span>
</div>\`;
// 添加连接线(最后一个不加)
if (i < phases.length - 1) {
const lineStatus = (status === 'completed' || status === 'skipped') ? 'completed' : '';
html += \`<div class="phase-line \${lineStatus}"></div>\`;
}
});
html += '</div>';
return html;
}
// 渲染阶段列表(两级结构)
function renderPlanPhases(phases) {
if (!phases || phases.length === 0) return '';
const statusLabels = {
skipped: '跳过',
completed: '已完成',
current: '当前',
pending: '待执行'
};
return phases.map((phase, i) => {
const status = phase.status || 'pending';
const statusLabel = statusLabels[status] || status;
const isExpanded = status === 'current';
const hasSteps = phase.steps && phase.steps.length > 0;
const hasReason = phase.reason && status === 'skipped';
let stepsHtml = '';
if (phase.steps && phase.steps.length > 0) {
stepsHtml = phase.steps.map(step => \`
<li class="phase-step-item">
<span class="phase-step-checkbox"></span>
<div class="phase-step-text">
<div class="phase-step-name">\${step.name || ''}</div>
\${step.description ? \`<div class="phase-step-desc">\${step.description}</div>\` : ''}
</div>
</li>
\`).join('');
}
return \`
<div class="plan-phase" data-phase-id="\${phase.id}">
<div class="phase-header" onclick="togglePhase(this)">
<span class="phase-toggle \${isExpanded ? 'expanded' : ''}">▶</span>
<span class="phase-name">\${phase.name || phase.id}</span>
<span class="phase-status \${status}">\${statusLabel}</span>
</div>
<div class="phase-content \${isExpanded ? 'expanded' : ''}">
\${hasReason ? \`<div class="phase-reason">\${phase.reason}</div>\` : ''}
\${hasSteps ? \`<ul class="phase-steps">\${stepsHtml}</ul>\` : ''}
\${!hasSteps && !hasReason ? '<div class="phase-reason">暂无步骤</div>' : ''}
</div>
</div>
\`;
}).join('');
}
// 切换阶段展开/折叠
function togglePhase(header) {
const toggle = header.querySelector('.phase-toggle');
const content = header.nextElementSibling;
toggle.classList.toggle('expanded');
content.classList.toggle('expanded');
}
// 渲染计划卡片(在 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');
}
// 判断是否有 phases新格式还是 steps旧格式
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 选项按钮
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('');
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \`
<div class="plan-card">
<div class="plan-header">
<span class="plan-icon">${plannerIconSvg}</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div>
\${progressHtml}
<div class="plan-body">
<div class="plan-summary">\${summaryHtml}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</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';
// 判断是否有 phases新格式还是 steps旧格式
const hasPhases = segment.planPhases && segment.planPhases.length > 0;
// 渲染阶段进度条和阶段列表(新格式)
const progressHtml = hasPhases ? renderPhaseProgress(segment.planPhases) : '';
const phasesHtml = hasPhases ? renderPlanPhases(segment.planPhases) : '';
// 兼容旧格式:渲染步骤列表
const stepsHtml = !hasPhases ? renderPlanSteps(segment.planSteps || []) : '';
// 渲染 Markdown 格式的摘要
const summaryHtml = renderPlanMarkdown(segment.planSummary || '');
segmentDiv.innerHTML = \`
<div class="plan-card">
<div class="plan-header">
<span class="plan-icon">📋</span>
<span class="plan-title">\${segment.planTitle || '执行计划'}</span>
</div>
\${progressHtml}
<div class="plan-body">
<div class="plan-summary">\${summaryHtml}</div>
\${hasPhases ? \`<div class="plan-phases">\${phasesHtml}</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 开关的 HTML 内容
* 已禁用,仅保留样式展示
*/
export function getPlanToggleContent(): string {
return `
<div class="tooltip">
<label class="plan-toggle">
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
<label class="plan-toggle plan-toggle-disabled">
<input type="checkbox" id="planToggle" disabled>
<span class="plan-toggle-slider"></span>
<span class="plan-toggle-label">Plan</span>
</label>
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
<span class="tooltiptext" id="planTooltip">请使用模式选择器切换 Plan 模式</span>
</div>
`;
}
@ -73,6 +75,17 @@ export function getPlanToggleStyles(): string {
font-weight: 500;
color: var(--vscode-foreground);
}
/* 禁用状态样式 */
.plan-toggle-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.plan-toggle-disabled .plan-toggle-slider {
background: var(--vscode-input-background);
border-color: var(--vscode-input-border);
}
`;
}

411
src/views/progressBar.ts Normal file
View File

@ -0,0 +1,411 @@
/**
* 进度条模块
*
* 功能说明:
* - 显示开发流程进度: Spec -> Design代码编写 -> 仿真检查 -> AST -> Done
* - 支持动态更新当前进度状态
* - 提供视觉反馈显示已完成和进行中的步骤
*/
/**
* 获取进度条的 HTML 内容
*/
export function getProgressBarContent(): string {
return `
<div class="progress-bar-container" style="display: none;">
<div class="progress-bar-header">
<span class="progress-bar-title">开发流程</span>
<button class="progress-bar-toggle" title="收起/展开">
<span class="toggle-icon">▼</span>
</button>
</div>
<div class="progress-steps">
<div class="progress-step" data-step="spec">
<div class="step-circle">
<span class="step-number">1</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Spec设计文档</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="design">
<div class="step-circle">
<span class="step-number">2</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Design代码编写</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="simulation">
<div class="step-circle">
<span class="step-number">3</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Sim仿真检查</div>
</div>
<div class="progress-line"></div>
<div class="progress-step" data-step="done">
<div class="step-circle">
<span class="step-number">4</span>
<span class="step-check">✓</span>
</div>
<div class="step-label">Done完成</div>
</div>
</div>
</div>
`;
}
/**
* 获取进度条的样式
*/
export function getProgressBarStyles(): string {
return `
.progress-bar-container {
background: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-panel-border);
margin-bottom: 5px;
}
.progress-bar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 20px;
cursor: pointer;
user-select: none;
}
.progress-bar-title {
font-size: 11px;
font-weight: 600;
color: var(--vscode-foreground);
}
.progress-bar-toggle {
background: none;
border: none;
color: var(--vscode-foreground);
cursor: pointer;
padding: 2px 6px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s;
}
.progress-bar-toggle:hover {
opacity: 1;
}
.toggle-icon {
font-size: 10px;
transition: transform 0.3s ease;
}
.progress-bar-container.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.progress-steps {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 700px;
margin: 0 auto;
padding: 0 20px 10px 20px;
max-height: 60px;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.progress-bar-container.collapsed .progress-steps {
max-height: 0;
padding: 0 20px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex: 0 0 auto;
}
.step-circle {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--vscode-input-background);
border: 2px solid var(--vscode-input-border);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s ease;
z-index: 2;
}
.step-number {
font-size: 10px;
font-weight: 600;
color: var(--vscode-foreground);
}
.step-check {
display: none;
font-size: 12px;
color: var(--vscode-button-foreground);
}
.step-label {
margin-top: 4px;
font-size: 10px;
color: var(--vscode-descriptionForeground);
text-align: center;
white-space: nowrap;
transition: color 0.3s ease;
}
.progress-line {
flex: 1;
height: 2px;
background: var(--vscode-input-border);
margin: 0 6px;
position: relative;
top: -10px;
transition: background 0.3s ease;
}
/* 已完成状态 */
.progress-step.completed .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
}
.progress-step.completed .step-number {
display: none;
}
.progress-step.completed .step-check {
display: block;
}
.progress-step.completed .step-label {
color: var(--vscode-foreground);
font-weight: 500;
}
.progress-step.completed + .progress-line {
background: var(--vscode-button-background);
}
/* 进行中状态 */
.progress-step.active .step-circle {
background: var(--vscode-button-background);
border-color: var(--vscode-button-background);
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
animation: pulse 2s infinite;
}
.progress-step.active .step-number {
color: var(--vscode-button-foreground);
}
.progress-step.active .step-label {
color: var(--vscode-foreground);
font-weight: 600;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 2px var(--vscode-button-background)33;
}
50% {
box-shadow: 0 0 0 4px var(--vscode-button-background)1a;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.progress-steps {
flex-wrap: wrap;
}
.step-label {
font-size: 9px;
}
.step-circle {
width: 20px;
height: 20px;
}
.step-number {
font-size: 9px;
}
.progress-line {
margin: 0 4px;
}
}
`;
}
/**
* 获取进度条的脚本
*/
export function getProgressBarScript(): string {
return `
// 进度条管理
const ProgressBar = {
steps: ['spec', 'design', 'simulation', 'done'],
currentStep: 'spec',
isCollapsed: false,
/**
* 初始化进度条
*/
init() {
this.updateProgress('spec');
this.initToggle();
},
/**
* 初始化收起/展开功能
*/
initToggle() {
const container = document.querySelector('.progress-bar-container');
const header = document.querySelector('.progress-bar-header');
const toggle = document.querySelector('.progress-bar-toggle');
if (!container || !header || !toggle) return;
// 点击头部或按钮都可以切换
const handleToggle = (e) => {
e.stopPropagation();
this.isCollapsed = !this.isCollapsed;
if (this.isCollapsed) {
container.classList.add('collapsed');
} else {
container.classList.remove('collapsed');
}
};
header.addEventListener('click', handleToggle);
toggle.addEventListener('click', handleToggle);
},
/**
* 显示进度条
*/
show() {
const container = document.querySelector('.progress-bar-container');
if (container) {
container.style.display = 'block';
}
},
/**
* 隐藏进度条
*/
hide() {
const container = document.querySelector('.progress-bar-container');
if (container) {
container.style.display = 'none';
}
},
/**
* 更新进度到指定步骤
* @param {string} stepName - 步骤名称
*/
updateProgress(stepName) {
if (!this.steps.includes(stepName)) {
console.warn('Invalid step name:', stepName);
return;
}
this.currentStep = stepName;
const currentIndex = this.steps.indexOf(stepName);
// 更新所有步骤的状态
document.querySelectorAll('.progress-step').forEach((step, index) => {
step.classList.remove('completed', 'active');
if (index < currentIndex) {
step.classList.add('completed');
} else if (index === currentIndex) {
step.classList.add('active');
}
});
// 更新连接线
document.querySelectorAll('.progress-line').forEach((line, index) => {
if (index < currentIndex) {
line.style.background = 'var(--vscode-button-background)';
} else {
line.style.background = 'var(--vscode-input-border)';
}
});
},
/**
* 前进到下一步
*/
nextStep() {
const currentIndex = this.steps.indexOf(this.currentStep);
if (currentIndex < this.steps.length - 1) {
this.updateProgress(this.steps[currentIndex + 1]);
}
},
/**
* 重置进度条
*/
reset() {
this.updateProgress('spec');
},
/**
* 完成所有步骤
*/
complete() {
this.updateProgress('done');
// 将最后一步也标记为完成
const lastStep = document.querySelector('.progress-step[data-step="done"]');
if (lastStep) {
lastStep.classList.remove('active');
lastStep.classList.add('completed');
}
}
};
// 初始化进度条
ProgressBar.init();
// 监听来自扩展的消息以更新进度
window.addEventListener('message', (event) => {
const message = event.data;
if (message.type === 'updateProgress') {
ProgressBar.updateProgress(message.step);
} else if (message.type === 'resetProgress') {
ProgressBar.reset();
} else if (message.type === 'completeProgress') {
ProgressBar.complete();
} else if (message.type === 'showProgress') {
ProgressBar.show();
} else if (message.type === 'hideProgress') {
ProgressBar.hide();
}
});
`;
}

View File

@ -0,0 +1,178 @@
/**
* 思考过程组件
*
* 功能说明:
* - 显示 AI 的思考过程
* - 支持展开/折叠功能
* - 提供打字机效果的流式显示
*/
/**
* 获取思考过程组件的 HTML 内容
* @param thinking - 思考内容
* @param isExpanded - 是否默认展开
*/
export function getThinkingProcessContent(
thinking: string = "",
isExpanded: boolean = false
): string {
return `
<div class="thinking-process-container ${isExpanded ? "expanded" : ""}">
<div class="thinking-header">
<div class="thinking-icon-wrapper">
<svg class="thinking-icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<span class="thinking-title">思考过程</span>
</div>
<div class="thinking-content">
<div class="thinking-text">${thinking || "正在思考中..."}</div>
</div>
</div>
`;
}
/**
* 获取思考过程组件的样式
*/
export function getThinkingProcessStyles(): string {
return `
.thinking-process-container {
margin: 12px 0;
border-radius: 6px;
background: var(--vscode-editor-background);
overflow: hidden;
transition: all 0.3s ease;
}
.thinking-header {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 5px;
cursor: pointer;
user-select: none;
background: var(--vscode-input-background);
transition: background 0.2s ease;
width: 85px;
border-radius: 20px;
position: relative;
overflow: hidden;
}
.thinking-header::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
animation: none;
}
.thinking-process-container.typing .thinking-header::before {
animation: shine 2s ease-in-out infinite;
}
@keyframes shine {
0% {
left: -100%;
}
50%, 100% {
left: 100%;
}
}
.thinking-header:hover {
background: var(--vscode-list-hoverBackground);
}
.thinking-icon-wrapper {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.thinking-process-container.expanded .thinking-icon-wrapper {
transform: rotate(90deg);
}
.thinking-icon {
color: var(--vscode-descriptionForeground);
}
.thinking-title {
flex: 1;
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
.thinking-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
padding: 0 12px;
}
.thinking-process-container.expanded .thinking-content {
max-height: 500px;
padding: 12px;
overflow-y: auto;
}
.thinking-text {
font-size: 12px;
line-height: 1.6;
color: var(--vscode-descriptionForeground);
white-space: pre-wrap;
word-wrap: break-word;
padding-left: 12px;
border-left: 2px solid var(--vscode-textBlockQuote-border);
position: relative;
}
/* 打字机效果 */
.thinking-text.typing::after {
content: '▋';
animation: blink 1s step-end infinite;
margin-left: 2px;
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
/* 滚动条样式 */
.thinking-content::-webkit-scrollbar {
width: 6px;
}
.thinking-content::-webkit-scrollbar-track {
background: transparent;
}
.thinking-content::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background);
border-radius: 3px;
}
.thinking-content::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
`;
}

View File

@ -5,65 +5,79 @@ export function getWaveformPreviewContent(): string {
return `
/* 波形预览组件样式 */
.waveform-preview {
margin-top: 12px;
margin: 16px 0;
border: 1px solid var(--vscode-panel-border);
border-radius: 8px;
border-radius: 12px;
overflow: hidden;
background: var(--vscode-editor-background);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease, transform 0.2s ease;
}
.waveform-preview:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.waveform-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--vscode-input-background);
padding: 14px 16px;
background: linear-gradient(135deg, var(--vscode-input-background) 0%, var(--vscode-editor-background) 100%);
border-bottom: 1px solid var(--vscode-panel-border);
backdrop-filter: blur(10px);
}
.waveform-preview-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
gap: 10px;
font-size: 14px;
font-weight: 600;
color: var(--vscode-foreground);
letter-spacing: 0.3px;
}
.waveform-preview-title svg {
width: 16px;
height: 16px;
width: 18px;
height: 18px;
color: var(--vscode-button-background);
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
}
.waveform-expand-btn {
padding: 4px 12px;
padding: 6px 14px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
gap: 4px;
transition: opacity 0.2s ease;
gap: 6px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.waveform-expand-btn:hover {
opacity: 0.9;
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.waveform-expand-btn:active {
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.waveform-expand-btn svg {
width: 14px;
height: 14px;
}
.waveform-preview-content {
padding: 0;
min-height: 200px;
max-height: 300px;
padding: 12px;
overflow: hidden;
position: relative;
background: var(--vscode-editor-background);
}
.waveform-preview-canvas {
width: 100%;
height: 100%;
min-height: 200px;
height: auto;
}
.waveform-preview-placeholder {
display: flex;
@ -88,7 +102,8 @@ export function getWaveformPreviewContent(): string {
}
.waveform-mini-viewer {
width: 100%;
height: 200px;
height: auto;
min-height: 120px;
background: var(--vscode-editor-background);
position: relative;
overflow: hidden;
@ -263,61 +278,63 @@ export function getWaveformPreviewScript(): string {
const timeRange = maxTime - minTime || 1;
// 绘制波形
if (signal.width === 1) {
// 单比特信号 - 绘制数字波形
let pathData = '';
let lastX = leftMargin;
let lastValue = signal.values[0].value;
const yHigh = y;
const yLow = y + signalHeight;
signal.values.forEach((point, i) => {
const x = leftMargin + ((point.time - minTime) / timeRange) * waveformWidth;
const value = point.value;
if (signal.width === 1) {
// 单比特信号 - 绘制数字波形
const yHigh = y;
const yLow = y + signalHeight;
const currentY = (value === '1') ? yHigh : yLow;
const currentY = (point.value === '1') ? yHigh : yLow;
if (i === 0) {
pathData = \`M \${x} \${currentY}\`;
} else {
// 绘制垂直跳变
const prevY = (lastValue === '1') ? yHigh : yLow;
const prevValue = signal.values[i - 1].value;
const prevY = (prevValue === '1') ? yHigh : yLow;
if (prevY !== currentY) {
pathData += \` L \${x} \${prevY} L \${x} \${currentY}\`;
} else {
pathData += \` L \${x} \${currentY}\`;
}
}
lastValue = value;
lastX = x;
} else {
// 多比特信号 - 绘制总线波形(梯形)
const yTop = y + 5;
const yBottom = y + signalHeight - 5;
const transitionWidth = 5;
if (i === 0) {
pathData = \`M \${x} \${yTop + (yBottom - yTop) / 2}\`;
} else {
// 绘制梯形过渡
pathData += \` L \${x - transitionWidth} \${yTop} L \${x} \${yTop + (yBottom - yTop) / 2}\`;
}
lastX = x;
}
});
// 延伸到右边界
if (signal.width === 1) {
const lastY = (lastValue === '1') ? y : (y + signalHeight);
const lastValue = signal.values[signal.values.length - 1].value;
const lastY = (lastValue === '1') ? yHigh : yLow;
pathData += \` L \${leftMargin + waveformWidth} \${lastY}\`;
} else {
const yMid = y + signalHeight / 2;
pathData += \` L \${leftMargin + waveformWidth} \${yMid}\`;
}
svgContent += \`<path d="\${pathData}" stroke="\${color}" stroke-width="1.5" fill="none"/>\`;
} 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,16 +17,35 @@ import {
getMessageAreaStyles,
getMessageAreaScript,
} from "./messageArea";
import { getAgentCardStyles, getAgentCardScript } from "./agentCard";
import {
getProgressBarContent,
getProgressBarStyles,
getProgressBarScript,
} from "./progressBar";
import { getHighlightJsLinks } from "../components/codeHighlight";
import { getCurrentEnv } from "../config/settings";
/**
* 获取 WebView 面板的 HTML 内容
*/
export function getWebviewContent(iconUri?: string): string {
export function getWebviewContent(
iconUri?: string,
autoIconUri?: string,
liteIconUri?: string,
syIconUri?: string,
maxIconUri?: string
): string {
// 获取当前环境,只在 dev 和 test 环境下显示快速操作按钮
const currentEnv = getCurrentEnv();
const showQuickActions = currentEnv === "dev" || currentEnv === "test";
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IC Coder</title>
${getHighlightJsLinks()}
<style>
body {
font-family: var(--vscode-font-family);
@ -65,8 +84,10 @@ export function getWebviewContent(iconUri?: string): string {
padding: 0 20px 20px 20px;
}
${getMessageAreaStyles()}
${getAgentCardStyles()}
${getWaveformPreviewContent()}
${getConversationHistoryBarStyles()}
${getProgressBarStyles()}
${getInputAreaStyles()}
.file-editor-section {
@ -250,7 +271,7 @@ export function getWebviewContent(iconUri?: string): string {
padding: 0;
}
.message-segment {
padding: 10px 22 px;
padding: 10px 0;
}
.segment-text {
line-height: 1.6;
@ -293,7 +314,7 @@ export function getWebviewContent(iconUri?: string): string {
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
padding: 12px 14px;
padding: 12px 35px;
border-left: 3px solid var(--vscode-charts-orange);
}
.question-segment .question-text {
@ -372,6 +393,7 @@ export function getWebviewContent(iconUri?: string): string {
</head>
<body>
${getConversationHistoryBarContent()}
${getProgressBarContent()}
<div class="header">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
@ -389,13 +411,18 @@ export function getWebviewContent(iconUri?: string): string {
<span id="statusText">思考中...</span>
</div>
<div class="quick-actions">
${
showQuickActions
? `<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('counter')">生成计数器</button>
<button class="quick-btn" onclick="quickAction('fsm')">生成状态机</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>
<script>
@ -411,11 +438,67 @@ export function getWebviewContent(iconUri?: string): string {
let loadingIndicator = null;
let currentSegmentedMessage = null; // 当前分段消息容器
// ========== 模式选择器脚本(直接内联,避免模板字符串嵌套问题)==========
let currentMode = 'agent';
function toggleModeDropdown() {
const modeSelectEl = document.getElementById('modeSelect');
const modelSelectEl = document.getElementById('modelSelect');
if (modeSelectEl) {
modeSelectEl.classList.toggle('active');
if (modelSelectEl) {
modelSelectEl.classList.remove('active');
}
}
}
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
const modeTooltip = document.getElementById('modeTooltip');
if (modeValue) {
modeValue.textContent = label;
}
if (modeTooltip) {
const tooltipMap = {
'plan': 'plan模式',
'ask': 'ask模式',
'agent': 'agent模式'
};
modeTooltip.textContent = tooltipMap[value] || '切换模式';
}
const options = document.querySelectorAll('.mode-option');
options.forEach(option => {
if (option.getAttribute('data-value') === value) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl) {
modeSelectEl.classList.remove('active');
}
}
function getCurrentMode() {
return currentMode;
}
document.addEventListener('click', (event) => {
const modeSelectEl = document.getElementById('modeSelect');
if (modeSelectEl && !modeSelectEl.contains(event.target)) {
modeSelectEl.classList.remove('active');
}
});
// ========== 模式选择器脚本结束 ==========
function quickAction(type) {
const questions = {
counter: '生成一个4位同步计数器',
fsm: '生成一个状态机',
testbench: '生成测试平台'
testbench: '生成测试平台',
explore: '请启动知识探索智能体,分析当前项目结构'
};
if (questions[type]) {
messageInput.value = questions[type];
@ -436,8 +519,6 @@ export function getWebviewContent(iconUri?: string): string {
}
}
messageInput.focus();
// 监听来自插件的消息
window.addEventListener('message', event => {
const message = event.data;
@ -504,11 +585,32 @@ export function getWebviewContent(iconUri?: string): string {
}
break;
case 'resetSegmentedMessage':
// 重置分段消息容器(停止对话时调用)
console.log('[WebView] 重置分段消息容器');
currentSegmentedMessage = null;
break;
case 'contextUsage':
// 更新上下文使用量显示
if (typeof updateContextDisplay === 'function') {
updateContextDisplay(message.currentTokens, message.maxTokens);
}
break;
case 'hideLoading':
// 隐藏加载指示器
hideLoadingIndicator();
break;
case 'workspaceStatus':
// 更新工作区状态
if (typeof hasWorkspace !== 'undefined') {
hasWorkspace = message.hasWorkspace;
console.log('[WebView] 工作区状态:', hasWorkspace);
}
break;
case 'vcdInfo':
// 渲染迷你波形预览信息
try {
@ -565,6 +667,10 @@ export function getWebviewContent(iconUri?: string): string {
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
// 重置输入框布局到居中
if (typeof window.resetInputAreaLayout === 'function') {
window.resetInputAreaLayout();
}
break;
case 'addUserMessage':
@ -572,6 +678,10 @@ export function getWebviewContent(iconUri?: string): string {
if (message.text) {
addMessage(message.text, 'user');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
break;
case 'addAiMessage':
@ -579,6 +689,31 @@ export function getWebviewContent(iconUri?: string): string {
if (message.text) {
addMessage(message.text, 'bot');
}
// 检查并更新输入框布局
if (typeof window.checkMessagesAndUpdateLayout === 'function') {
window.checkMessagesAndUpdateLayout();
}
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:
@ -587,8 +722,10 @@ export function getWebviewContent(iconUri?: string): string {
});
${getMessageAreaScript()}
${getAgentCardScript()}
${getWaveformPreviewScript()}
${getConversationHistoryBarScript()}
${getProgressBarScript()}
${getInputAreaScript()}
</script></body>
</html>`;

View File

@ -0,0 +1,42 @@
@echo off
REM waveform_trace 打包脚本 (Windows)
REM 用法: build.bat
echo ========================================
echo waveform_trace 打包脚本
echo ========================================
cd /d "%~dp0src"
echo.
echo [1/3] 安装依赖...
pip install -r requirements.txt
if %errorlevel% neq 0 (
echo 错误: 依赖安装失败
exit /b 1
)
echo.
echo [2/3] 清理旧文件...
if exist build rmdir /s /q build
if exist dist rmdir /s /q dist
if exist waveform_trace.spec del waveform_trace.spec
echo.
echo [3/3] PyInstaller 打包...
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
if %errorlevel% neq 0 (
echo 错误: 打包失败
exit /b 1
)
echo.
echo [4/4] 复制到 bin 目录...
if not exist "..\bin" mkdir "..\bin"
copy /y "dist\waveform_trace.exe" "..\bin\"
echo.
echo ========================================
echo 打包完成!
echo 输出: tools/waveform_trace/bin/waveform_trace.exe
echo ========================================

View File

@ -0,0 +1,35 @@
#!/bin/bash
# waveform_trace 打包脚本 (Linux/macOS)
# 用法: ./build.sh
set -e
echo "========================================"
echo " waveform_trace 打包脚本"
echo "========================================"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR/src"
echo ""
echo "[1/4] 安装依赖..."
pip install -r requirements.txt
echo ""
echo "[2/4] 清理旧文件..."
rm -rf build dist *.spec
echo ""
echo "[3/4] PyInstaller 打包..."
pyinstaller --onefile --name waveform_trace --collect-all pyverilog waveform_trace_cli.py
echo ""
echo "[4/4] 复制到 bin 目录..."
mkdir -p ../bin
cp dist/waveform_trace ../bin/
echo ""
echo "========================================"
echo " 打包完成!"
echo " 输出: tools/waveform_trace/bin/waveform_trace"
echo "========================================"

View File

@ -0,0 +1,115 @@
# AST 波形调试核心代码
## 文件说明
| 文件 | 作用 | 核心函数 | TS重写需要 |
|------|------|----------|------------|
| `ast_node.py` | AST节点定义遍历建图 | `toplogic_tree_traverse()` | ✅ 已完成 |
| `graph_builder.py` | 入口函数,调用解析器 | `generate_top_logic_graph()` | ✅ 已完成 |
| `debug_graph_analyzer.py` | BFS回溯控制信号 | `get_k_control_signals()` | ⚠️ 需重写 |
| `vcd_waveform_analyzer.py` | VCD波形文件解析 | `parse_mismatch()`, `get_tabular()` | ⚠️ 需重写 |
| `waveform_trace_tool.py` | 完整追踪工具封装 | `waveform_trace_tool()` | ⚠️ 需重写 |
---
## 调用流程
```
Verilog代码文件
┌─────────────────────────────────────┐
│ graph_builder.py │
│ generate_top_logic_graph(filelist) │
│ │ │
│ ▼ │
│ PyVerilog.parse() → AST │
│ │ │
│ ▼ │
│ ast.toplogic_tree_traverse() │
│ │ │
│ ▼ │
│ NetworkX 有向图(信号依赖图) │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ debug_graph_analyzer.py │
│ DebugGraph.get_k_control_signals() │
│ │ │
│ ▼ │
│ BFS回溯K层找到控制信号链 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ vcd_waveform_analyzer.py │
│ parse_mismatch() + get_tabular() │
│ │ │
│ ▼ │
│ 提取相关信号的波形表 │
└─────────────────────────────────────┘
```
---
## 核心代码位置
### 1. AST遍历建图 (ast_node.py:32-137)
```python
def toplogic_tree_traverse(self, network_G, rvalue=False, lvalue=False, offset=0):
"""
递归遍历AST提取信号依赖关系填充到NetworkX图中
关键逻辑:
1. 识别 Rvalue右值和 Lvalue左值
2. 递归收集子节点的信号
3. 建立边:右值信号 → 左值信号(控制关系)
"""
```
### 2. 图构建入口 (graph_builder.py:89-99)
```python
def generate_top_logic_graph(filelist: list[str]):
# 1. PyVerilog解析Verilog代码
ast, directives = parse(filelist, preprocess_include=[], preprocess_define=[])
# 2. 遍历AST构建信号依赖图
return create_graph_from_ast(ast, display=False, display_signal_only=False)
```
### 3. BFS回溯 (debug_graph_analyzer.py:20-66)
```python
def get_k_control_signals(self, target_signals: list[str], k: int, signal_only: bool = False):
"""
从出错信号出发BFS回溯K层找到所有控制信号
输入target_signals = ['out'] # 出错的信号
输出control_signals = {'out': (10,10), 'state': (5,8), 'clk': (1,1)}
signal_level_tracer = [['clk->state', 'reset->state'], ['state->out']]
"""
```
---
## 依赖库
```
pyverilog # Verilog解析生成AST
networkx # 图数据结构
pandas # 波形数据处理(可选)
```
---
## 如果要用JavaScript重写
需要重写的核心逻辑:
1. **Verilog解析器** → 用 ANTLR4 + Verilog.g4 或 tree-sitter-verilog
2. **AST遍历建图** → 约100行参考 ast_node.py:32-137
3. **BFS回溯** → 约70行参考 debug_graph_analyzer.py
总计约 **200行核心逻辑**(不含解析器)

View File

@ -0,0 +1,455 @@
# AST波形调试工具 - TypeScript重写规范
## 一、项目背景
将Python实现的Verilog AST波形调试工具重写为TypeScript用于VSCode插件。
**已完成部分**
- ✅ Verilog AST解析生成JSON格式的信号依赖图
- ✅ 图结构定义
**待重写部分**
- ⚠️ BFS信号回溯
- ⚠️ VCD波形解析
- ⚠️ 仿真输出解析
- ⚠️ 工具整合封装
---
## 二、数据结构定义
### 2.1 AST图结构已完成
```typescript
interface ASTNode {
id: string;
attributes: {
lines: [number, number]; // [起始行, 结束行]
type: string; // Input/Output/Reg/Wire/Always/Assign等
};
}
interface ASTEdge {
from: string; // 控制信号
to: string; // 被控制信号
attributes: {
lines: [number, number];
type: string; // Always/Assign/IfStatement等
};
}
interface ASTGraph {
metadata: {
moduleName: string;
nodeCount: number;
edgeCount: number;
generatedAt: string;
};
nodes: ASTNode[];
edges: ASTEdge[];
}
```
### 2.2 追踪结果结构
```typescript
interface TraceResult {
controlSignals: Map<string, [number, number]>; // 信号名 -> 代码行号
signalLevelTracer: string[][]; // 每层的控制关系链
}
```
### 2.3 波形数据结构
```typescript
interface WaveformData {
time: number; // 时间点(ns)
signals: {
[signalName: string]: string; // 信号名 -> 值(十六进制)
};
}
interface MismatchInfo {
signals: string[]; // 出错的信号列表
firstMismatchTime: number; // 第一次出错的时间
}
```
---
## 三、需要重写的模块
### 3.1 BFS信号回溯模块
**源文件**: `debug_graph_analyzer.py`
**代码行数**: ~70行
**第三方依赖**: 无
#### 功能描述
从出错信号出发BFS反向遍历图找到所有控制该信号的上游信号。
#### 输入输出
```typescript
// 输入
graph: ASTGraph // AST图JSON格式
targetSignals: string[] // 出错的信号列表,如 ['count', 'overflow']
k: number // 回溯层数
signalOnly: boolean // 是否只返回信号节点过滤Always/Assign等
// 输出
TraceResult {
controlSignals: Map<string, [number, number]>,
signalLevelTracer: string[][]
}
```
#### 核心算法(伪代码)
```
1. 构建前驱映射(反向边)
for each edge in graph.edges:
predecessorMap[edge.to].push(edge.from)
2. 初始化BFS队列
for each signal in targetSignals:
queue.push([signal, signal])
controlSignals.set(signal, node.lines)
3. BFS遍历K层
for level = 0 to k:
while queue not empty:
[curSignal, controlledSignal] = queue.pop()
记录关系: curSignal -> controlledSignal
for each predecessor of curSignal:
if not visited and not filtered:
queue.push([predecessor, curSignal])
记录本层关系到 signalLevelTracer
4. 返回结果
```
#### 过滤规则
```typescript
// 需要过滤的节点类型
const FILTERED_TYPES = ['Parameter', 'Localparam'];
// signalOnly=true时还需要过滤以下前缀
const FILTERED_PREFIXES = ['Always', 'Assign', 'Module', 'IntConst'];
```
---
### 3.2 仿真输出解析模块
**源文件**: `vcd_waveform_analyzer.py` 中的 `parse_mismatch()`
**代码行数**: ~20行
**第三方依赖**: 无
#### 功能描述
解析仿真工具的输出文本,提取出错信号名和出错时间。
#### 输入输出
```typescript
// 输入
testOutput: string // 仿真工具的输出文本
// 输出
MismatchInfo {
signals: string[], // 出错信号列表
firstMismatchTime: number // 第一次出错时间(ns)
}
```
#### 解析规则
```typescript
// 需要匹配的格式
// "First mismatch occurred at time 100. Output 'count' ..."
const pattern = /First mismatch occurred at time (\d+).*Output '(\w+)'/g;
// 提取所有匹配
// 返回信号列表和最小时间戳
```
#### 示例
```
输入:
"First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0"
输出:
{
signals: ['count', 'overflow'],
firstMismatchTime: 100
}
```
---
### 3.3 VCD波形解析模块
**源文件**: `vcd_waveform_analyzer.py` 中的 `get_tabular()``tabular_via_dataframe()`
**代码行数**: ~150行
**第三方依赖**: Python版用了 `vcdvcd`, `pandas`, `numpy`
#### 功能描述
读取VCDValue Change Dump波形文件提取指定信号的波形值生成表格。
#### VCD文件格式简介
```vcd
$timescale 1ns $end
$scope module tb $end
$var wire 1 ! clk $end
$var wire 8 " count [7:0] $end
$upscope $end
$enddefinitions $end
#0
b0 "
1!
#5
0!
#10
1!
b00000001 "
...
```
#### 输入输出
```typescript
// 输入
vcdPath: string // VCD文件路径
signalsToTrace: string[] // 需要提取的信号列表
offset: number // 时间偏移(从哪个时间点开始)
windowSize: number // 窗口大小(提取多少个时间点)
// 输出
string // 格式化的波形表格字符串
```
#### 输出格式示例
```
### First mismatched signals time(ns) Trace ###
time(ns) clk reset count_ref count_dut
0 1 1 00 00
5 0 1 00 00
10 1 0 00 00
15 0 0 00 00
20 1 0 01 00 <- mismatch
### First mismatched signals time(ns) End ###
```
#### TS实现建议
1. **方案A**: 找现有的JS VCD解析库
- 搜索: `npm vcd parser`, `vcd-stream`, `wavedrom`
2. **方案B**: 自己实现简单的VCD解析器
- VCD格式相对简单核心是解析变量定义和时间变化
- 约100-150行代码
#### VCD解析核心逻辑
```typescript
class VCDParser {
signals: Map<string, Signal>; // 信号定义
timeValues: Map<number, Map<string, string>>; // 时间 -> 信号值
parse(vcdContent: string): void {
// 1. 解析头部($var定义
// 2. 解析数据部分(#时间 和 值变化)
}
getSignalValues(signalName: string, startTime: number, endTime: number): WaveformData[] {
// 提取指定信号在时间范围内的值
}
}
```
---
### 3.4 工具整合封装模块
**源文件**: `waveform_trace_tool.py`
**代码行数**: ~150行
**第三方依赖**: 依赖上面所有模块
#### 功能描述
整合所有模块,提供统一的调试接口。
#### 输入输出
```typescript
// 输入
verilogFilePath: string // Verilog文件路径
vcdFilePath: string // VCD波形文件路径
simulationOutput: string // 仿真输出文本
traceLevel: number // 回溯层数
// 输出
string // 完整的调试报告
```
#### 调试报告格式
```
[Signal Traces] Backtrace control signal relations.
clk->count
reset->count
-count->state
--state->out (*last output port level)
[Signal Waveform]:
<signal>_ref 是期望值golden
<signal>_dut 是实际输出
[Traced Signals]: out, state, count, clk, reset
[Table Waveform in hexadecimal format]
time(ns) clk reset count_ref count_dut
...
[Verilog of DUT]:
```verilog
module counter(...);
...
endmodule
```
[Hint] ...
```
---
## 四、调用流程图
```
┌─────────────────────────────────────────────────────────────────┐
│ waveform_trace_tool() │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 检查文件是否存在 │
│ ├── verilogFilePath │
│ └── vcdFilePath │
│ │
│ 2. 加载AST图已有JSON
│ └── graph = loadASTGraph(verilogFilePath) │
│ │
│ 3. 解析仿真输出,获取出错信号 │
│ └── mismatchInfo = parseMismatch(simulationOutput) │
│ ├── signals: ['count', 'overflow'] │
│ └── firstMismatchTime: 100 │
│ │
│ 4. BFS回溯找到控制信号链 │
│ └── traceResult = getKControlSignals(graph, signals, k) │
│ ├── controlSignals: Map<信号名, 行号>
│ └── signalLevelTracer: [['clk->count'], ...] │
│ │
│ 5. 读取VCD波形提取相关信号的值 │
│ └── waveformTable = getTabular(vcdPath, signals, offset) │
│ │
│ 6. 读取Verilog源码 │
│ └── verilogCode = readFile(verilogFilePath) │
│ │
│ 7. 组装调试报告 │
│ └── return formatReport(traceResult, waveformTable, code) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 五、参考实现
### 5.1 Python源文件位置
```
ast_debug_core/
├── ast_node.py # AST节点定义参考32-137行
├── graph_builder.py # 图构建入口
├── debug_graph_analyzer.py # BFS回溯完整文件约70行
├── vcd_waveform_analyzer.py # VCD解析参考89-285行
└── waveform_trace_tool.py # 工具封装完整文件约180行
```
### 5.2 关键函数对照表
| Python函数 | 位置 | TS函数名建议 |
|------------|------|--------------|
| `get_k_control_signals()` | debug_graph_analyzer.py:20 | `getKControlSignals()` |
| `parse_mismatch()` | vcd_waveform_analyzer.py:244 | `parseMismatch()` |
| `get_tabular()` | vcd_waveform_analyzer.py:264 | `getTabular()` |
| `tabular_via_dataframe()` | vcd_waveform_analyzer.py:95 | `generateWaveformTable()` |
| `waveform_trace_tool()` | waveform_trace_tool.py:63 | `waveformTraceTool()` |
---
## 六、测试用例
### 6.1 BFS回溯测试
```typescript
// 输入
const graph: ASTGraph = /* 加载 counter_ast_graph.json */;
const targetSignals = ['count'];
const k = 2;
// 期望输出
const expected = {
controlSignals: new Map([
['count', [6, 6]],
['next_count', [10, 10]],
['reset', [4, 4]],
['clk', [3, 3]],
['enable', [5, 5]]
]),
signalLevelTracer: [
['count->count'],
['next_count->count', 'reset->count', 'clk->count'],
['enable->next_count', 'count->next_count']
]
};
```
### 6.2 仿真输出解析测试
```typescript
// 输入
const testOutput = `
Mismatches: 2
First mismatch occurred at time 100. Output 'count' expected 0001, got 0000
First mismatch occurred at time 150. Output 'overflow' expected 1, got 0
`;
// 期望输出
const expected = {
signals: ['count', 'overflow'],
firstMismatchTime: 100
};
```
---
## 七、注意事项
1. **无第三方依赖要求**
- BFS回溯和仿真解析完全可以用原生TS实现
- VCD解析可以自己实现或找现有库
2. **性能考虑**
- 图遍历使用Map而非Object提高查找效率
- VCD文件可能很大考虑流式解析
3. **错误处理**
- 文件不存在时返回友好错误信息
- 信号不在图中时跳过而非报错
4. **兼容性**
- 信号名可能包含方括号,如 `count[7:0]`
- 时间单位统一为ns
---
## 八、交付物
1. `debugGraphAnalyzer.ts` - BFS回溯模块
2. `simulationParser.ts` - 仿真输出解析模块
3. `vcdParser.ts` - VCD波形解析模块
4. `waveformTraceTool.ts` - 工具整合封装
5. `types.ts` - 类型定义
6. 单元测试文件

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,70 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Author : Chia-Tung (Mark) Ho, NVIDIA
#
import copy
import re
from collections import deque
from graph_builder import generate_top_logic_graph
# use class
class DebugGraph:
def __init__(self, verilog_filelist: list[str]):
self.filelist = verilog_filelist
self.graph = generate_top_logic_graph(verilog_filelist)
# print(list(self.graph.nodes(data=True)))
def get_k_control_signals(self, target_signals: list[str], k:int, signal_only: bool=False) -> list[str]:
control_signals = {}
signal_level_tracer = []
# queue
q = deque()
tmp_q = deque()
for signal in target_signals:
# store (predecessors, controlled signal)
q.append((signal, signal))
control_signals[signal] = self.graph.nodes[signal]['lines']
# BFS
for l in range (k + 1):
# traverse l layers
tmp_q.clear()
level_signal_control_rels = []
while len(q) > 0:
cur_signal = q.popleft()
level_signal_control_rels.append(cur_signal[0] + "->" + cur_signal[1])
if cur_signal[0] not in control_signals:
if self.graph.has_edge(cur_signal[0], cur_signal[1]):
# must be the control signals through the edge
control_signals[cur_signal[0]] = self.graph[cur_signal[0]][cur_signal[1]]['lines']
else:
print("[Error] Edge not found! - ", cur_signal)
# find the predecessors
controls = self.graph.predecessors(cur_signal[0])
for c in controls:
if c in control_signals:
continue
# exclude the parameter
if 'type' in self.graph.nodes[c] and self.graph.nodes[c]['type'] in ["Parameter", "Localparam"]:
continue
if signal_only and (re.match('^Always', c) or re.match('^Assign', c) or re.match('^Module', c) or re.match('^IntConst', c)):
continue
# store (predecessors, controlled signal)
tmp_q.append((c, cur_signal[0]))
# swap the q
assert(len(q) == 0)
print(tmp_q)
q = copy.deepcopy(tmp_q)
# record the signal relations
signal_level_tracer.append(level_signal_control_rels)
return control_signals, signal_level_tracer
if __name__ == '__main__':
debug_graph_tracer = DebugGraph(["/home/scratch.chiatungh_nvresearch/hardware-agent-marco/hardware_agent/examples/verilog_testcases/fsm_serialdata.v"])
print(debug_graph_tracer.get_k_control_signals(['out_byte', 'done'], k=3, signal_only=True))

View File

@ -0,0 +1,144 @@
#
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Author : Chia-Tung (Mark) Ho, NVIDIA
#
from __future__ import print_function
import sys
import os
from optparse import OptionParser
# 优先使用本地修改过的 pyverilog包含 toplogic_tree_traverse 方法)
_local_path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _local_path)
from pyverilog.vparser.parser import parse
from io import StringIO
import networkx as nx
# importing matplotlib.pyplot
import matplotlib.pyplot as plt
import re
# create graph from ast str
# directed graph from networkX
def create_graph_from_ast(ast, display=False, display_signal_only=False):
graph = nx.DiGraph()
ast.toplogic_tree_traverse(network_G=graph, rvalue=False, lvalue=False)
if not display and not display_signal_only:
return graph
# Print out nodes with attributes
nodes_to_display = []
edges_to_display = []
print("Nodes:")
for node, attrs in graph.nodes(data=True):
if display_signal_only and (not re.match("^Assign", node) and not re.match("^Always", node) and not re.match("^Module", node)):
nodes_to_display.append(node)
print(f"Node {node}: {attrs}")
# Print out edges with attributes
print("\nEdges:")
for src, dst, attrs in graph.edges(data=True):
if display_signal_only and src in nodes_to_display and dst in nodes_to_display:
edges_to_display.append((src, dst))
print(f"Edge {src} to {dst}: {attrs}")
# displaying graphs
plt.figure(figsize=(18, 16)) # Set the figure size
pos = nx.spring_layout(graph, k=1.0)
if display_signal_only:
subgraph = graph.subgraph(nodes_to_display)
# subgraph.add_edges_from(edges_to_display)
else:
subgraph = graph
nx.draw_networkx(subgraph, pos, with_labels=True) # Draw the graph without labels
# Add node labels
# node_labels = nx.get_node_attributes(graph, 'label')
# nx.draw_networkx_labels(graph, pos, labels=node_labels)
# edge labels
edge_labels = nx.get_edge_attributes(subgraph, 'lines')
nx.draw_networkx_edge_labels(
subgraph, pos,
edge_labels=edge_labels,
font_color='blue'
)
# plt.axis('off')
plt.show()
return graph
def get_ast_structure_str(ast):
normal_stdout = sys.stdout
# put the string output to a string buffer
result = StringIO()
sys.stdout = result
# traverse the ast
ast.show(buf=sys.stdout)
# Redirect std output to the normal mode
sys.stdout = normal_stdout
# Get the result out
ast_str = result.getvalue()
# print('ast str = ', ast_str, '\n ast end')
return ast_str
def generate_top_logic_graph(filelist: list[str]):
for f in filelist:
if not os.path.exists(f):
raise IOError("file not found: " + f)
ast, directives = parse(filelist,
preprocess_include=[],
preprocess_define=[])
# ast_str = get_ast_structure_str(ast)
return create_graph_from_ast(ast, display=False, display_signal_only=False)
def main():
INFO = "Verilog code parser"
VERSION = pyverilog.__version__
USAGE = "Usage: python example_parser.py file ..."
def showVersion():
print(INFO)
print(VERSION)
print(USAGE)
sys.exit()
optparser = OptionParser()
optparser.add_option("-v", "--version", action="store_true", dest="showversion",
default=False, help="Show the version")
optparser.add_option("-I", "--include", dest="include", action="append",
default=[], help="Include path")
optparser.add_option("-D", dest="define", action="append",
default=[], help="Macro Definition")
(options, args) = optparser.parse_args()
filelist = args
# print(filelist)
if options.showversion:
showVersion()
for f in filelist:
if not os.path.exists(f):
raise IOError("file not found: " + f)
if len(filelist) == 0:
showVersion()
ast, directives = parse(filelist,
preprocess_include=options.include,
preprocess_define=options.define)
# ast_str = get_ast_structure_str(ast)
create_graph_from_ast(ast, display_signal_only=True, display=True)
ast.show(attrnames=True)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
.PHONY: clean
clean:
make clean -C ./utils
make clean -C ./vparser
make clean -C ./dataflow
make clean -C ./controlflow
make clean -C ./ast_code_generator
rm -rf *.pyc __pycache__ *.out parsetab.py *.html

View File

@ -0,0 +1 @@
1.3.0

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import
from __future__ import print_function
import os
with open(os.path.join(os.path.dirname(__file__), "VERSION")) as f:
__version__ = f.read().splitlines()[0]

View File

@ -0,0 +1,3 @@
.PHONY: clean
clean:
rm -rf *.pyc __pycache__ parsetab.py *.out

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
Source
Description
ModuleDef
Paramlist
Portlist
Port
Width
Length
Dimensions
Identifier
Value
Constant
IntConst
FloatConst
StringConst
Variable
Input
Output
Inout
Tri
Wire
Reg
Integer
Real
Genvar
Ioport
Parameter
Localparam
Decl
Concat
LConcat
Repeat
Partselect
Pointer
Lvalue
Rvalue
Operator
UnaryOperator
Uminus
Ulnot
Unot
Uand
Unand
Uor
Unor
Uxor
Uxnor
Power
Times
Divide
Mod
Plus
Minus
Sll
Srl
Sra
LessThan
GreaterThan
LessEq
GreaterEq
Eq
NotEq
Eql
NotEql
And
Xor
Xnor
Or
Land
Lor
Cond
Assign
Always
SensList
Sens
Substitution
BlockingSubstitution
NonblockingSubstitution
IfStatement
ForStatement
WhileStatement
CaseStatement
Case
Block
Initial
WaitStatement
ForeverStatement
DelayStatement
InstanceList
Instance
ParamArg
PortArg
Function
FunctionCall
Task
GenerateStatement
SystemCall
IdentifierScopeLabel
IdentifierScope
Pragma
PragmaEntry
Disable
ParallelBlock
SingleStatement

View File

@ -0,0 +1,3 @@
always @({{ sens_list }}) {{ statement }}

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

View File

@ -0,0 +1 @@
assign {{ left }} = {{ right }};

View File

@ -0,0 +1,5 @@
begin{% if scope != '' %} : {{ scope }}{% endif %}
{%- for statement in statements %}
{{ statement }}
{%- endfor %}
end

View File

@ -0,0 +1 @@
{% if ldelay != '' %}{{ ldelay }} {% endif %}{{ left }} = {% if rdelay != '' %}{{ rdelay }} {% endif %}{{ right }};

View File

@ -0,0 +1 @@
{{ cond }}: {{ statement }}

View File

@ -0,0 +1,5 @@
case({{ comp }})
{%- for case in caselist %}
{{ case }}
{%- endfor %}
endcase

View File

@ -0,0 +1,5 @@
casex({{ comp }})
{%- for case in caselist %}
{{ case }}
{%- endfor %}
endcase

View File

@ -0,0 +1 @@
{ {% for item in items %}{{ item }}{% if loop.index < len_items %}, {% endif %}{% endfor %} }

View File

@ -0,0 +1 @@
(({{ cond }})? {{ true_value }} : {{ false_value }})

View File

@ -0,0 +1 @@
{{ value }}

View File

@ -0,0 +1,2 @@
{%- for item in items %}{{ item }}
{%- endfor %}

View File

@ -0,0 +1,3 @@
{% for definition in definitions %}
{{ definition }}
{% endfor %}

View File

@ -0,0 +1 @@
diable {{ name }}

View File

@ -0,0 +1 @@
({{ left }} {{ op }} {{ right }})

Some files were not shown because too many files have changed in this diff Show More