48 Commits

Author SHA1 Message Date
f9c9fa1840 feat(auth): 添加登录状态检查,
- 未登录时不会自动打开面板命令打开也会显示需要登录
- 登录之后就回自动打开对话面板
2025-12-29 18:52:56 +08:00
53e91fc5a0 feat: 集成 VSCode Authentication API 实现用户登录
- 新增 Authentication Provider,登录信息显示在左下角
- 支持浏览器登录并自动回调
- 登录/登出后自动刷新窗口
- 侧边栏根据登录状态显示不同按钮
2025-12-29 18:25:21 +08:00
4288607ee2 feat(icon): 更新面板和视图中的图标路径为统一的 icon.png 2025-12-29 16:32:18 +08:00
d4d86df7de fix(PUBLISH.md): 修正打包命令,添加 --no-dependencies 选项以避免依赖问题 2025-12-29 15:59:41 +08:00
4b8d255207 feat(media): 添加主页和侧边栏图标,并更新 package.json 中的图标路径 2025-12-29 15:52:22 +08:00
a5dba25a8e fix(package): 修正 package.json 中的名称字段为小写 2025-12-29 15:38:00 +08:00
719d1396b0 feat(license): 添加 LICENSE 文件并更新 package.json 中的许可证信息 2025-12-29 15:35:57 +08:00
5b6ac43e13 feat(sendButton): 添加发送和暂停按钮图标及其状态管理功能 2025-12-29 14:41:15 +08:00
f7c2d86a46 feat(optimizeButton): 添加一键优化按钮组件及其集成到输入区域 2025-12-29 12:04:38 +08:00
83db55c790 feat(planToggle): 添加 Plan 开关组件及其集成到输入区域 2025-12-29 12:00:43 +08:00
94d41c3da9 feat(modeSelector): 添加模式选择器组件并集成到输入区域 2025-12-29 11:53:59 +08:00
83f9e2f005 feat(contextCompress): 添加上下文压缩组件及其集成到输入区域 2025-12-29 11:32:39 +08:00
318d3964bd feat(contextButton): 添加上下文按钮组件及相关集成
- 新增上下文按钮组件及其 HTML 内容、样式和脚本代码
- 在输入区域中集成上下文按钮组件,替换原内联代码
- 将上下文按钮样式从输入区域样式中移除并统一管理
- 将上下文按钮事件处理函数移入独立脚本并集成至输入区域脚本
- 保持输入区域功能完整性,简化代码结构,提高可维护性
2025-12-29 11:26:15 +08:00
770da72ce3 feat: 添加模型选择器组件,整合模型选择功能到输入区域 2025-12-29 11:17:10 +08:00
c050f0e167 feat: 添加上下文按钮和自定义下拉框,优化输入区域样式 2025-12-28 19:39:49 +08:00
3daa66ea01 feat:修复多面板任务管理和历史会话加载问题
主要改进:
1. 修复面板ID唯一性问题,为每个面板生成唯一ID
2. 修改任务创建时机,改为首次发送消息时创建
3. 修复面板任务映射,同时存储taskId和projectPath
4. 修复历史会话加载后继续对话的保存问题
5. 移除ensureCurrentTask的自动创建逻辑,避免创建多余任务

技术细节:
- 为面板添加__uniqueId属性,确保多窗口独立性
- 修改panelTaskMap数据结构,存储完整任务信息
- 在selectConversation中更新面板任务映射
- 优化任务创建流程,避免空任务目录
2025-12-28 11:31:28 +08:00
9bdaf34471 feat:实现任务历史加载功能 - 完整还原对话样式
主要改进:
1. 实现selectConversation功能,支持点击任务历史列表加载会话
2. 优化会话存储格式,保存完整的segments信息(包括工具调用)
3. 添加旧格式到新格式的自动转换,兼容历史数据
4. 改进错误处理,自动清理无效的空任务目录
5. 优化路径编码逻辑,确保跨平台一致性
6. 前端支持clearChat、addUserMessage、addAiMessage命令

技术细节:
- 扩展AiMessage数据结构,添加segments字段
- 修改messageHandler保存逻辑,将完整segments保存到一条消息
- 实现loadTaskSession方法,加载指定任务的完整会话
- 添加自动清理机制,删除无效的空任务目录
2025-12-28 10:38:54 +08:00
25a8ea5aa4 feat:记录会话,按顺序记录AI和用户的会话
- 包含工具调用等
2025-12-25 15:13:15 +08:00
ef83016b7f feat:解决强制滚动的问题 2025-12-25 11:00:23 +08:00
2e6812d00d feat:vscode的版本要求从1.8改为1.07 2025-12-25 10:31:35 +08:00
b676846b2f feat:实现plan的开关的前端展示效果 2025-12-25 09:23:53 +08:00
9c787627a9 feat:实现已完成仿真之后直接调用波形预览组件展示波形预览图 2025-12-24 12:00:03 +08:00
463eedf1dd feat:实现了工具调用的icon替换
- 还将icon抽取出来成为独立的组件,方便统一管理
2025-12-24 11:26:25 +08:00
fb1156d24f feat:将英文的工具名称映射为中文展示 2025-12-24 10:43:55 +08:00
0b4ec2ca6e feat:修改了工具调用的样式 + 实现工具调用内容太长可以折叠的功能 2025-12-24 10:25:12 +08:00
10f0877a5e fix: 修复AI询问时选项点击后选中状态丢失的问题
- 添加 answeredQuestions Map 存储已回答问题的状态
- 在重新渲染时恢复选中状态和 answered 类
- 已回答的问题自动隐藏输入框并禁用点击事件
- 确保用户选择在页面更新时保持显示
2025-12-24 10:01:53 +08:00
5c2ea0f15c Merge branch 'feat/plugin-initialization' into feat/back-to-front 2025-12-17 10:07:08 +08:00
b0e1995897 feat:优化小细节输入框的样式 2025-12-17 10:03:43 +08:00
6c5d470bad fex:尝试修复流式显示工具调用不穿插显示的问题 2025-12-17 10:03:40 +08:00
f18ca4fb9c feat:新增会话历史列表样式 + 新开会话功能
- 还优化了页面的样式,删除了读取文件的以及没用的文本
2025-12-17 09:48:45 +08:00
c21ad95963 feat: 实现状态栏显示功能
- 在消息区域下方添加状态栏 UI(HTML、CSS、JS)
- 支持"思考中..."状态显示(蓝色脉冲动画)
- 支持"生成中..."状态显示(橙色脉冲动画)
- 支持工具执行时显示"正在执行 xxx..."
- 在 messageHandler 中添加状态栏消息发送逻辑
2025-12-16 19:20:14 +08:00
7c1f1fae07 feat: 集成后端通信和前端交互功能
- 重构消息处理器(src/utils/messageHandler.ts)
  - 集成 DialogService 实现后端对话管理
  - 添加流式消息处理和 SSE 事件监听
  - 实现工具执行状态的实时更新
  - 支持用户问题的交互处理
  - 添加对话中止和错误处理机制

- 更新 ICHelperPanel(src/panels/ICHelperPanel.ts)
  - 添加 submitAnswer 消息处理,支持用户答案提交
  - 添加 abortDialog 消息处理,支持对话中止
  - 与后端服务进行双向通信

- 更新 ICViewProvider(src/views/ICViewProvider.ts)
  - 同步更新消息处理逻辑
  - 添加 extensionPath 参数传递
  - 支持新的消息类型和事件处理

完成前后端通信的完整集成,实现:
- AI 对话的流式响应
- 工具调用的实时反馈
- 用户交互的双向通信
- 错误处理和状态管理
2025-12-16 19:09:46 +08:00
c61e29a41f feat: 实现 WebView 流式消息显示和状态管理
- 添加流式消息分段显示功能
  - 支持 AI 消息的实时流式渲染
  - 实现消息块(MessageChunk)的增量更新
  - 使用 marked 库进行 Markdown 渲染

- 新增加载状态指示器
  - 显示 AI 思考中的动画效果
  - 支持加载状态的显示和隐藏

- 实现工具执行状态展示
  - 显示工具调用的实时状态(执行中/成功/失败)
  - 展示工具名称、参数和执行结果
  - 提供折叠/展开功能查看详细信息

- 添加用户问题交互 UI
  - 支持 AI 向用户提问的界面展示
  - 显示问题内容和等待用户响应的提示
  - 集成答案提交和对话中止功能

- 优化消息渲染性能
  - 使用 DocumentFragment 批量更新 DOM
  - 避免频繁的页面重排和重绘
2025-12-16 19:09:35 +08:00
703912bb5f chore: 添加后端通信相关依赖
- 添加 eventsource-parser 依赖用于 SSE 事件解析
- 新增后端配置项(iccoder.backend.baseUrl 和 timeout)
- 更新 pnpm-lock.yaml 锁定依赖版本
2025-12-16 19:09:23 +08:00
8ad6a48e8f feat: 实现核心服务层
- 新增对话服务(src/services/dialogService.ts)
  - 封装完整的对话生命周期管理
  - 集成 SSE 流式响应处理
  - 支持对话创建、消息发送、对话中止
  - 提供统一的事件回调接口

- 新增工具执行器(src/services/toolExecutor.ts)
  - 实现前端工具调用框架
  - 支持 readFile、writeFile、listFiles、executeCommand 等工具
  - 提供工具执行结果的标准化返回
  - 集成 VSCode API 进行文件和终端操作

- 新增用户交互处理(src/services/userInteraction.ts)
  - 实现 AI 向用户提问功能(AskUser)
  - 支持 input、confirm、quickPick 等交互类型
  - 使用 VSCode 原生 UI 组件展示问题
  - 提供答案收集和提交机制
2025-12-16 19:09:16 +08:00
ba75541dd6 feat: 实现后端通信层
- 新增 HTTP 客户端(src/services/apiClient.ts)
  - 实现对话创建、消息发送、对话中止等 API 调用
  - 支持用户答案提交和对话历史查询
  - 统一的错误处理和超时控制

- 新增 SSE 事件处理器(src/services/sseHandler.ts)
  - 实现 Server-Sent Events 流式数据解析
  - 支持 MessageChunk、ToolExecution、AskUser、Error 等事件类型
  - 使用 eventsource-parser 库处理 SSE 数据流
  - 提供事件回调机制,支持实时 UI 更新
2025-12-16 19:09:04 +08:00
f87adab7be feat: 添加后端通信基础设施
- 新增 API 类型定义(src/types/api.ts)
  - 定义对话请求/响应接口
  - 定义 SSE 事件类型(MessageChunk、ToolExecution、AskUser 等)
  - 定义工具执行和用户交互相关类型

- 新增配置管理模块(src/config/settings.ts)
  - 实现后端服务器配置读取
  - 支持从 VSCode 配置中获取 baseUrl 和 timeout
  - 提供统一的配置访问接口
2025-12-16 19:08:54 +08:00
f2382a8eed feat:实现波形预览的功能 2025-12-16 16:58:35 +08:00
4918399325 feat:新增复制点赞点踩功能
- 优化输入框占据不满整个大框的问题
- 优化点赞点踩复制的tooltip显示被遮挡的问题
2025-12-16 16:22:07 +08:00
92f6639741 feat:对话界面将输入框固定在底部 2025-12-16 15:19:33 +08:00
07bb2d46b0 feat:实现一键优化的前端效果 2025-12-16 14:53:46 +08:00
e42514fe95 feat:实现添加上下文的样式 2025-12-16 09:17:07 +08:00
4e4e33d136 feat:模型切换的样式定义 2025-12-15 18:21:17 +08:00
e541b85005 feat:将调用node.js的fs模块改为vscode官方的API
- 这样可以避免用户本地没有node环境导致插件无法运行的原因
2025-12-15 17:29:15 +08:00
c77187eec1 feat:订阅之后开启vscode就自动打开聊天面板 2025-12-15 17:14:36 +08:00
a1a526bb98 feat:搭建本地存储会话历史的框架
- 将会话历史存储在C:\Users\admin\.iccoder文件下
- 在里面又会创建多个文件夹进行存储
2025-12-15 15:19:36 +08:00
ab6d257df2 feat:接入波形查看器的工具
- 生成VCD文件后,就自动打开波形查看的工具显示波形
2025-12-15 11:51:35 +08:00
22b9a0ed13 feat:接入iverilog工具
- 将iverilog可以随着插件的下载而下载
- 用户输入自然语言就可以控制生成对应的VCD文件
2025-12-15 11:09:03 +08:00
113 changed files with 14789 additions and 479 deletions

View File

@ -9,5 +9,7 @@
"dist": true // set this to false to include "dist" folder in search results "dist": true // set this to false to include "dist" folder in search results
}, },
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off" "typescript.tsc.autoDetect": "off",
// IC Coder 后端服务地址
"icCoder.backendUrl": "http://192.168.1.108:2233"
} }

View File

@ -1,14 +0,0 @@
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
webpack.config.js
vsc-extension-quickstart.md
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*

284
IVERILOG_INTEGRATION.md Normal file
View File

@ -0,0 +1,284 @@
# Iverilog 集成完成报告
## 概述
已成功将 Icarus Verilog (iverilog) 工具集成到 IC Coder 插件中,用户可以通过简单的命令生成 VCD 波形文件。
## 完成的功能
### 1. 核心功能模块
**文件**: `src/utils/iverilogRunner.ts`
实现了以下功能:
- ✅ Verilog 项目文件完整性检查
- ✅ 自动查找顶层模块和 testbench 文件
- ✅ iverilog 编译功能
- ✅ vvp 仿真运行
- ✅ VCD 文件生成
- ✅ 路径空格问题处理(使用 spawn 代替 exec
- ✅ 环境变量配置
### 2. 用户界面集成
**文件**: `src/utils/messageHandler.ts`
- ✅ 命令解析:支持多种 VCD 生成命令
- "生成 VCD"
- "创建 VCD"
- "运行仿真"
- "执行仿真"
- "iverilog"
- "生成波形"
- "仿真生成"
- ✅ 实时反馈:
- 项目检查进度
- 编译状态
- 仿真输出
- 错误信息
- ✅ 用户交互:
- 成功后提示打开 VCD 文件
- 详细的错误提示和解决建议
### 3. Iverilog 工具打包
**目录**: `tools/iverilog/`
已成功复制以下文件到插件包:
**bin/ 目录** (10 个文件,约 4 MB):
- `iverilog.exe` - Verilog 编译器
- `vvp.exe` - Verilog 仿真器
- 所有必需的 DLL 文件
**lib/ 目录** (44 个文件,约 21 MB):
- 所有 `.vpi` 库文件
- 所有 `.tgt` 目标文件
- 所有 `.conf` 配置文件
- `include/` 头文件目录
**总大小**: 约 25 MB
### 4. 文档和示例
创建了完整的文档:
1. **README.md** - 工具说明和使用指南
2. **INSTALL.md** - 安装和配置说明
3. **DOWNLOAD_INSTRUCTIONS.md** - 下载和部署指南
4. **examples/** - 测试示例项目
- `counter.v` - 4 位计数器模块
- `counter_tb.v` - 测试平台
- `README.md` - 示例说明
### 5. 自动化脚本
-`copy-iverilog.ps1` - PowerShell 自动复制脚本
-`copy-iverilog.bat` - Windows 批处理启动器
## 技术实现细节
### 路径空格处理
使用 `child_process.spawn` 代替 `child_process.exec`,完美解决了路径中包含空格的问题:
```typescript
function execCommand(
command: string,
args: string[],
options: { cwd: string; env?: any }
): Promise<{ stdout: string; stderr: string }>
```
### 环境变量配置
设置 `IVERILOG_ROOT` 环境变量,确保 iverilog 能找到库文件:
```typescript
const env = {
...process.env,
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
};
```
### 文件检查逻辑
智能识别项目文件:
- 自动查找所有 `.v``.sv` 文件
- 识别 testbench 文件(文件名包含 `tb``test`,或包含 `$dumpfile`
- 识别顶层模块(非 testbench 的 module 定义)
## 使用方法
### 1. 准备项目
确保项目包含:
- 至少一个 Verilog 模块文件(`.v``.sv`
- 一个 testbench 文件,包含:
```verilog
initial begin
$dumpfile("output.vcd");
$dumpvars(0, module_name);
// ... 测试代码 ...
$finish;
end
```
### 2. 生成 VCD
在 IC Coder 插件中输入任一命令:
- `生成 VCD`
- `运行仿真`
- `生成波形`
### 3. 查看结果
- VCD 文件保存在项目根目录:`output.vcd`
- 可以使用 GTKWave 等工具查看波形
## 测试示例
提供了完整的测试示例:
**位置**: `tools/iverilog/examples/`
**运行测试**:
1. 在 VS Code 中打开 `examples` 目录
2. 打开 IC Coder 插件
3. 输入 "生成 VCD"
4. 查看生成的 `output.vcd` 文件
## 文件清单
### 新增文件
```
src/utils/iverilogRunner.ts # 核心功能模块
tools/iverilog/
├── bin/ # 可执行文件 (10 个文件)
│ ├── iverilog.exe
│ ├── vvp.exe
│ └── *.dll
├── lib/ # 库文件 (44 个文件)
│ ├── ivl/
│ │ ├── *.vpi
│ │ ├── *.tgt
│ │ └── *.conf
│ └── include/
├── examples/ # 测试示例
│ ├── counter.v
│ ├── counter_tb.v
│ └── README.md
├── README.md # 使用说明
├── INSTALL.md # 安装指南
├── DOWNLOAD_INSTRUCTIONS.md # 下载说明
├── copy-iverilog.ps1 # 自动复制脚本
└── copy-iverilog.bat # 批处理启动器
```
### 修改文件
```
src/utils/messageHandler.ts # 添加 VCD 生成命令处理
src/panels/ICHelperPanel.ts # 传递 extensionPath 参数
package.json # 添加 tools 目录到打包列表
```
## 版本信息
- **Icarus Verilog**: v12.0 (devel) (s20150603-1539-g2693dd32b)
- **平台**: Windows x64
- **许可证**: GPL v2+
## 已知问题和限制
### 1. 路径空格问题 ✅ 已解决
- 使用 `spawn` 代替 `exec` 完美解决
### 2. 平台支持
- 当前仅包含 Windows x64 版本的 iverilog
- macOS 和 Linux 用户需要自行安装 iverilog
### 3. 文件大小
- 插件包增加约 25 MB
- 建议在发布时说明文件大小
## 后续优化建议
### 1. 多平台支持
- 为 macOS 和 Linux 提供对应的 iverilog 二进制文件
- 根据平台自动选择对应的可执行文件
### 2. 配置选项
- 允许用户配置 VCD 文件输出路径
- 允许用户配置仿真参数
### 3. 高级功能
- 支持 SystemVerilog
- 支持多个 testbench 选择
- 集成波形查看器
### 4. 错误处理
- 更详细的编译错误提示
- 语法错误定位
- 常见问题自动修复建议
## 测试清单
- ✅ 编译成功(无 TypeScript 错误)
- ✅ iverilog 工具已打包25 MB
- ✅ 路径空格问题已解决
- ✅ 环境变量配置正确
- ✅ 文档完整
- ✅ 示例项目可用
- ⏳ 实际运行测试(需要在 VS Code 中测试)
## 部署步骤
1. **确认文件完整**
```bash
ls -lh "D:/IC Coder Plugin/ic-coder/tools/iverilog/bin"
ls -lh "D:/IC Coder Plugin/ic-coder/tools/iverilog/lib"
```
2. **编译插件**
```bash
cd "D:/IC Coder Plugin/ic-coder"
pnpm run compile
```
3. **打包插件**
```bash
pnpm run package
```
4. **测试插件**
- 在 VS Code 中按 F5 启动调试
- 打开 `tools/iverilog/examples` 目录
- 测试 VCD 生成功能
5. **发布插件**
- 确保 `package.json` 中的 `files` 字段包含 `tools`
- 使用 `vsce package` 打包
- 发布到 VS Code Marketplace
## 总结
**所有功能已完成并集成**
- Iverilog 工具已成功打包到插件中25 MB
- 用户下载插件后即可直接使用,无需额外安装
- 支持多种命令触发 VCD 生成
- 提供完整的文档和示例
- 解决了路径空格等技术问题
- 代码编译成功,无错误
**下一步**: 在 VS Code 中实际测试插件功能,验证 VCD 生成流程。
---
**创建时间**: 2025-12-15
**版本**: 1.0
**状态**: ✅ 完成

12
LICENSE Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2025 IC Coder Team. All rights reserved.
本软件及其相关文档文件(以下简称"软件")的版权归 IC Coder 所有。
未经版权所有者事先书面许可,不得以任何形式或方式(电子、机械、复印、录制或其他方式)
复制、分发、传播、展示、修改或创建本软件的衍生作品。
本软件按"原样"提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性
和非侵权性的保证。在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,
无论是在合同诉讼、侵权行为还是其他方面。
如需商业使用或获取许可,请联系:[pyjtkj@pyjtkj.com]

356
PUBLISH.md Normal file
View File

@ -0,0 +1,356 @@
# IC Coder 插件发布流程文档
本文档详细说明如何将 IC Coder 插件发布到 VS Code 插件市场进行测试和正式发布。
## 目录
- [前置准备](#前置准备)
- [账号配置](#账号配置)
- [插件信息完善](#插件信息完善)
- [打包与发布](#打包与发布)
- [版本更新](#版本更新)
- [常见问题](#常见问题)
---
## 前置准备
### 环境要求
- Node.js 和 pnpm 已安装
- VS Code 1.80.0 或更高版本
- 已安装 `@vscode/vsce` 工具(项目已包含)
### 检查清单
在发布前,请确保以下文件和配置已准备就绪:
- [x] `package.json` - 插件配置文件
- [x] `README.md` - 插件说明文档
- [x] `dist/` - 编译后的代码
- [x] `media/` - 图标和资源文件
- [ ] `CHANGELOG.md` - 版本更新日志(建议添加)
- [x] `LICENSE` - 开源许可证(建议添加)
---
## 账号配置
### 1. 创建 Azure DevOps 账号
1. 访问 [Azure DevOps](https://dev.azure.com)
2. 使用 Microsoft 账号注册或登录
3. 创建一个组织(如果还没有)
### 2. 生成 Personal Access Token (PAT)
这是发布插件的关键凭证,请妥善保管。
**步骤:**
1. 登录 Azure DevOps
2. 点击右上角用户图标 → **User settings****Personal access tokens**
3. 点击 **New Token** 按钮
4. 配置 Token 信息:
- **Name**: `vscode-publisher`(或其他易识别的名称)
- **Organization**: 选择 **All accessible organizations**
- **Expiration**: 建议选择较长期限(如 90 天或自定义)
- **Scopes**: 选择 **Custom defined**
- 展开 **Marketplace**
- 勾选 **Manage**(包含发布和管理权限)
5. 点击 **Create** 生成 Token
6. **重要**: 立即复制并保存 Token页面关闭后将无法再次查看
**Token 示例格式:**
```
CO03l8nmFBBTNPDg7lN9a9fYwDdgsRIDVDwTrx6Esggi6HnzmrMTJQQJ99BLACAAAAAAAAAAAAAGAZDOVVyT
```
### 3. 创建发布者账号
发布者账号是你在 VS Code 市场的身份标识。
**步骤:**
1. 访问 [VS Code Marketplace 管理页面](https://marketplace.visualstudio.com/manage)
2. 使用 Azure DevOps 账号登录
3. 点击 **Create publisher** 按钮
4. 填写发布者信息:
- **ID**: `ICCoder`(必须与 package.json 中的 `publisher` 字段一致)
- **Name**: `IC Coder`(显示名称,可自定义)
- **Email**: 你的联系邮箱
5. 点击 **Create** 完成创建
**注意事项:**
- Publisher ID 一旦创建无法修改
- Publisher ID 必须全局唯一
- 建议使用有意义且专业的 ID
---
## 插件信息完善
### 1. 完善 package.json
建议在 `package.json` 中添加以下字段以提升插件质量:
```json
{
"repository": {
"type": "git",
"url": "https://github.com/your-org/ic-coder.git"
},
"homepage": "https://github.com/your-org/ic-coder#readme",
"bugs": {
"url": "https://github.com/your-org/ic-coder/issues"
},
"license": "MIT"
}
```
### 2. 创建 CHANGELOG.md
版本更新日志帮助用户了解每个版本的变化。
**示例内容:**
```markdown
# 更新日志
## [0.0.2] - 2025-12-29
### 新增
- 添加发送和暂停按钮功能
- 添加一键优化按钮组件
- 添加 Plan 开关组件
- 添加模式选择器组件
- 添加上下文压缩功能
### 改进
- 优化用户界面交互体验
## [0.0.1] - 2025-12-XX
### 新增
- 初始版本发布
- Verilog 代码智能生成
- 集成 iverilog 仿真工具
- VCD 波形文件查看器
```
### 3. 创建 LICENSE 文件
如果使用 MIT 许可证,创建 `LICENSE` 文件:
```
MIT License
Copyright (c) 2025 IC Coder Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction...
```
### 4. 优化 README.md
确保 README 包含:
- 清晰的功能介绍
- 使用截图或 GIF 演示
- 详细的使用说明
- 系统要求
- 常见问题解答
---
## 打包与发布
### 方式一:命令行发布(推荐)
这是最便捷的发布方式,适合频繁更新。
**步骤:**
1. **登录发布者账号**
```bash
pnpm vsce login ic-coder-team
```
系统会提示输入 Personal Access Token粘贴之前创建的 PAT。
2. **打包插件**
```bash
# 执行生产环境构建
pnpm run package
# 打包成 .vsix 文件
pnpm vsce package
```
这会生成 `ic-coder-plugin-0.0.2.vsix` 文件。
3. **发布到市场**
```bash
pnpm vsce publish
```
发布成功后会显示插件的市场链接。
**一键发布(跳过打包步骤):**
```bash
# 直接发布当前版本
pnpm vsce publish
```
### 方式二:手动上传
适合首次发布或网络环境受限的情况。
**步骤:**
1. 本地打包插件:
```bash
pnpm run package
pnpm vsce package[pnpm vsce package --no-dependencies]
```
2. 访问 [发布者管理页面](https://marketplace.visualstudio.com/manage/publishers/ic-coder-team)
3. 点击 **New extension****Visual Studio Code**
4. 上传 `ic-coder-plugin-0.0.2.vsix` 文件
5. 填写插件信息(如果需要)并提交
6. 等待审核通过
---
## 版本更新
### 自动更新版本号
使用 `vsce publish` 命令可以自动更新版本号并发布:
```bash
# 补丁版本更新0.0.2 → 0.0.3
pnpm vsce publish patch
# 次版本更新0.0.2 → 0.1.0
pnpm vsce publish minor
# 主版本更新0.0.2 → 1.0.0
pnpm vsce publish major
```
### 手动指定版本
```bash
# 发布指定版本
pnpm vsce publish 0.0.3
```
### 更新流程建议
1. 修改代码并测试
2. 更新 `CHANGELOG.md` 记录变更
3. 提交代码到 Git
4. 执行发布命令
5. 验证市场上的插件是否正常
---
## 常见问题
### 1. 发布失败Authentication failed
**原因:** PAT Token 无效或过期
**解决方案:**
- 重新生成 PAT Token
- 重新登录:`pnpm vsce login ic-coder-team`
### 2. 发布失败Publisher not found
**原因:** Publisher ID 不存在或不匹配
**解决方案:**
- 检查 `package.json` 中的 `publisher` 字段
- 确认已在市场创建对应的 Publisher
### 3. 打包失败Missing files
**原因:** 必需文件缺失
**解决方案:**
- 确保 `dist/` 目录存在且包含编译后的代码
- 运行 `pnpm run package` 重新构建
### 4. 插件审核被拒
**常见原因:**
- 插件名称或描述违反市场规则
- 图标不符合要求(建议 128x128 PNG
- README 内容不完整
**解决方案:**
- 查看审核反馈邮件
- 修改相关内容后重新发布
### 5. 如何撤回已发布的版本?
```bash
# 取消发布指定版本
pnpm vsce unpublish ic-coder-team.ic-coder-plugin@0.0.2
# 取消发布整个插件(慎用)
pnpm vsce unpublish ic-coder-team.ic-coder-plugin
```
### 6. 如何本地测试 .vsix 文件?
```bash
# 在 VS Code 中安装本地 .vsix 文件
code --install-extension ic-coder-plugin-0.0.2.vsix
```
或者在 VS Code 中:
1. 打开扩展面板
2. 点击 `...` 菜单
3. 选择 **Install from VSIX...**
4. 选择 `.vsix` 文件
---
## 发布检查清单
在正式发布前,请确认以下事项:
- [ ] 代码已充分测试,无明显 Bug
- [ ] `package.json` 版本号已更新
- [ ] `CHANGELOG.md` 已记录本次更新内容
- [ ] README.md 内容完整且准确
- [ ] 图标和资源文件正常显示
- [ ] 已在本地安装测试 .vsix 文件
- [ ] 已创建 Azure DevOps PAT Token
- [ ] 已创建 VS Code Marketplace Publisher
- [ ] 已执行 `pnpm run package` 构建生产版本
---
## 参考资源
- [VS Code 插件发布官方文档](https://code.visualstudio.com/api/working-with-extensions/publishing-extension)
- [vsce 工具文档](https://github.com/microsoft/vscode-vsce)
- [Azure DevOps 文档](https://docs.microsoft.com/en-us/azure/devops/)
- [VS Code 插件市场](https://marketplace.visualstudio.com/)
---
**文档维护:** IC Coder Team
**最后更新:** 2025-12-29

View File

@ -1,71 +1,24 @@
# ic-coder README # IC Coder Plugin
This is the README for your extension "ic-coder". After writing up a brief description, we recommend including the following sections. IC Coder 是一个面向 Verilog/FPGA 开发的智能辅助插件。
## Features ## 功能特性
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - Verilog 代码智能生成
- 文件操作支持(创建、读取、修改、删除)
- 集成 iverilog 仿真工具
- VCD 波形文件生成
- 智能对话助手
For example if there is an image subfolder under your extension project workspace: ## 使用说明
\!\[feature X\]\(images/feature-x.png\) 安装插件后,点击侧边栏的 IC Coder 图标即可开始使用。
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. ## 系统要求
## Requirements - VS Code 1.107.0 或更高版本
- 插件已内置 iverilog 工具(Windows 平台)
If you have any requirements or dependencies, add a section describing those and how to install and configure them. ## 许可证
## Extension Settings MIT
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
---
## Following extension guidelines
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
## Working with Markdown
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux).
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux).
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets.
## For more information
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
**Enjoy!**

View File

@ -0,0 +1,573 @@
# IC Coder 认证系统实现文档
## 概述
本文档详细说明了 IC Coder 插件如何集成 VSCode Authentication API实现用户登录功能并在 VSCode 左下角账户区域显示登录状态。
## 架构设计
### 核心组件
1. **ICCoderAuthenticationProvider** - 认证提供者
2. **VSCode Authentication API** - VSCode 官方认证接口
3. **本地 HTTP 服务器** - 处理登录回调
4. **ICViewProvider** - 侧边栏视图(根据登录状态显示不同按钮)
### 工作流程
```
用户点击登录
调用 vscode.authentication.getSession()
ICCoderAuthenticationProvider.createSession()
启动本地 HTTP 服务器(动态端口)
打开浏览器访问登录页面
用户在网站完成登录
网站重定向到 http://localhost:{port}/callback?token=xxx
本地服务器接收 token
创建 AuthenticationSession
VSCode 左下角显示账户信息
```
## 详细实现
### 1. Authentication Provider 实现
文件:`src/services/icCoderAuthProvider.ts`
#### 1.1 类定义
```typescript
export class ICCoderAuthenticationProvider
implements vscode.AuthenticationProvider
{
private _onDidChangeSessions =
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
public readonly onDidChangeSessions = this._onDidChangeSessions.event;
private _sessions: vscode.AuthenticationSession[] = [];
}
```
**关键点:**
- 实现 `vscode.AuthenticationProvider` 接口
- 使用 `EventEmitter` 通知会话变化
- 在内存中维护会话列表
#### 1.2 核心方法
##### getSessions() - 获取会话列表
```typescript
async getSessions(scopes?: readonly string[]): Promise<readonly vscode.AuthenticationSession[]> {
return this._sessions;
}
```
##### createSession() - 创建会话(登录)
```typescript
async createSession(scopes: readonly string[]): Promise<vscode.AuthenticationSession> {
const token = await this.login();
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: token,
account: {
id: "iccoder-user",
label: "IC Coder 用户",
},
scopes: [...scopes],
};
this._sessions.push(session);
await this.saveSessions();
this._onDidChangeSessions.fire({
added: [session],
removed: [],
changed: [],
});
return session;
}
```
**关键点:**
- 调用 `login()` 方法获取 token
- 创建 `AuthenticationSession` 对象
- 保存到 `globalState`
- 触发 `onDidChangeSessions` 事件通知 VSCode
##### removeSession() - 删除会话(登出)
```typescript
async removeSession(sessionId: string): Promise<void> {
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
if (sessionIndex > -1) {
const session = this._sessions[sessionIndex];
this._sessions.splice(sessionIndex, 1);
await this.saveSessions();
this._onDidChangeSessions.fire({
added: [],
removed: [session],
changed: [],
});
}
}
```
### 2. 本地 HTTP 服务器实现
#### 2.1 动态端口分配
```typescript
server.listen(0, () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 3000;
resolve({ server, port });
});
```
**关键点:**
- 使用端口 `0` 让系统自动分配可用端口
- 避免端口冲突问题
- 支持多个用户同时使用
#### 2.2 回调处理
```typescript
if (url.pathname === "/callback") {
const token = url.searchParams.get("token");
if (token) {
// 返回成功页面
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(this.getSuccessPage(iconBase64));
// 关闭服务器
server.close();
// 返回 token
if ((server as any)._loginResolve) {
(server as any)._loginResolve(token);
}
}
}
```
### 3. package.json 配置
#### 3.1 注册 Authentication Provider
```json
{
"contributes": {
"authentication": [
{
"id": "iccoder",
"label": "IC Coder"
}
]
}
}
```
**关键点:**
- `id` 必须与代码中使用的 ID 一致
- `label` 会显示在 VSCode 账户菜单中
#### 3.2 注册命令
```json
{
"contributes": {
"commands": [
{
"command": "ic-coder.login",
"title": "IC Coder: 登录账户",
"category": "IC Coder"
},
{
"command": "ic-coder.logout",
"title": "IC Coder: 退出登录",
"category": "IC Coder"
}
]
}
}
```
### 4. extension.ts 注册
#### 4.1 注册 Authentication Provider
```typescript
export function activate(context: vscode.ExtensionContext) {
// 注册 Authentication Provider
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(
"iccoder",
"IC Coder",
authProvider
)
);
}
```
#### 4.2 登录命令
```typescript
const loginCommand = vscode.commands.registerCommand(
"ic-coder.login",
async () => {
try {
await vscode.authentication.getSession("iccoder", [], {
createIfNone: true,
});
} catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`);
}
}
);
```
**关键点:**
- `createIfNone: true` 会在没有会话时自动调用 `createSession()`
- VSCode 会自动处理 UI 交互
#### 4.3 登出命令
```typescript
const logoutCommand = vscode.commands.registerCommand(
"ic-coder.logout",
async () => {
try {
const session = await vscode.authentication.getSession("iccoder", [], {
createIfNone: false,
});
if (session) {
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
forceNewSession: true,
});
vscode.window.showInformationMessage("已退出登录");
}
} catch (error) {
vscode.window.showInformationMessage("当前未登录");
}
}
);
```
### 5. ICViewProvider 集成
#### 5.1 检查登录状态
```typescript
private async checkLoginStatus(): Promise<boolean> {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
return !!session;
} catch (error) {
return false;
}
}
```
#### 5.2 根据登录状态显示不同按钮
```typescript
resolveWebviewView(webviewView: vscode.WebviewView) {
this.checkLoginStatus().then((isLoggedIn) => {
webviewView.webview.html = this.getWebviewContent(
webviewView.webview,
isLoggedIn
);
});
}
```
```typescript
${isLoggedIn
? '<button class="btn" onclick="openChat()">开始创作</button>'
: '<button class="btn" onclick="login()">登录账户</button>'
}
```
### 6. 网站前端配置
#### 6.1 检测插件登录请求
```javascript
// 在登录页面检测 redirect_uri 参数
const urlParams = new URLSearchParams(window.location.search);
const redirectUri = urlParams.get("redirect_uri");
if (redirectUri) {
// 保存回调地址
localStorage.setItem("plugin_redirect_uri", redirectUri);
}
```
#### 6.2 登录成功后重定向
```javascript
// 用户登录成功,拿到 token
const token = response.data.token;
// 检查是否需要重定向回插件
const redirectUri = localStorage.getItem("plugin_redirect_uri");
if (redirectUri) {
// 重定向回插件,带上 token
window.location.href = `${redirectUri}?token=${token}`;
localStorage.removeItem("plugin_redirect_uri");
} else {
// 正常登录流程
router.push("/dashboard");
}
```
## 关键技术点
### 1. 动态端口分配
**问题:** 固定端口可能被占用,导致登录失败
**解决方案:** 使用端口 `0` 让系统自动分配可用端口
```typescript
server.listen(0, () => {
const address = server.address();
const port = typeof address === "object" && address ? address.port : 3000;
});
```
### 2. Promise 异步等待
**问题:** 需要等待浏览器登录完成后才能继续
**解决方案:** 使用 Promise 包装回调逻辑
```typescript
return new Promise((resolve, reject) => {
(server as any)._loginResolve = resolve;
(server as any)._loginReject = reject;
});
```
### 3. 会话持久化
**问题:** 重启 VSCode 后需要重新登录
**解决方案:** 使用 `globalState` 保存会话
```typescript
await this.context.globalState.update("icCoderSessions", this._sessions);
```
### 4. 事件通知机制
**问题:** VSCode 需要知道会话状态变化
**解决方案:** 使用 `EventEmitter` 触发事件
```typescript
this._onDidChangeSessions.fire({
added: [session],
removed: [],
changed: [],
});
```
## 用户体验
### 登录流程
1. 用户点击侧边栏"登录账户"按钮
2. 浏览器自动打开登录页面
3. 用户在网站完成登录
4. 浏览器自动跳转到成功页面
5. VSCode 左下角显示"IC Coder 用户"
6. 侧边栏按钮变为"开始创作"
### 登出流程
1. 点击 VSCode 左下角账户图标
2. 选择"IC Coder"账户
3. 点击"退出"按钮
4. 或使用命令 `IC Coder: 退出登录`
## 常见问题
### Q1: 为什么不直接使用 globalState 存储 token
**A:** 使用 VSCode Authentication API 的优势:
- ✅ 统一的用户体验(左下角账户区域)
- ✅ VSCode 自动管理会话生命周期
- ✅ 支持多账户切换
- ✅ 更好的安全性VSCode 负责加密存储)
### Q2: 如何处理 token 过期?
**A:** 可以在 API 请求失败时:
1. 检测 401 错误
2. 调用 `removeSession()` 清除过期会话
3. 提示用户重新登录
### Q3: 如何支持多个账户?
**A:** 修改 `account` 对象:
```typescript
account: {
id: userInfo.id,
label: userInfo.username,
}
```
### Q4: 登录页面如何获取用户信息?
**A:** 可以在登录成功后,通过 API 获取用户信息:
```typescript
const userInfo = await fetch("https://api.iccoder.com/user/info", {
headers: { Authorization: `Bearer ${token}` },
});
const session: vscode.AuthenticationSession = {
account: {
id: userInfo.id,
label: userInfo.username,
},
// ...
};
```
## 安全考虑
### 1. Token 存储
- ✅ 使用 VSCode `globalState` 加密存储
- ✅ 不在代码中硬编码敏感信息
- ✅ Token 仅在内存和加密存储中传递
### 2. 本地服务器
- ✅ 仅监听 `localhost`,不暴露到外网
- ✅ 使用动态端口,避免固定端口被劫持
- ✅ 接收到 token 后立即关闭服务器
- ✅ 设置 5 分钟超时,防止服务器长期运行
### 3. HTTPS 考虑
**当前实现:** 使用 HTTP 本地回调
**生产环境建议:**
- 网站使用 HTTPS
- 本地回调使用 HTTPlocalhost 不受浏览器限制)
- 或使用 `vscode://` 协议(需要网站支持)
## 测试指南
### 1. 本地测试
```bash
# 启动调试模式
按 F5
# 测试登录
1. 打开侧边栏
2. 点击"登录账户"
3. 在浏览器完成登录
4. 检查左下角是否显示账户
# 测试登出
1. 点击左下角账户
2. 选择"IC Coder"
3. 点击"退出"
```
### 2. 调试技巧
```typescript
// 在 ICCoderAuthenticationProvider 中添加日志
console.log("🔐 创建会话:", session);
console.log("🔑 Token:", token);
// 在 ICViewProvider 中添加日志
console.log("🔍 登录状态:", isLoggedIn);
```
### 3. 常见错误排查
| 错误 | 原因 | 解决方案 |
| ------------------------------- | --------------- | ---------------------------------- |
| `getSessions is not a function` | VSCode 版本过低 | 升级到 1.63.0+ |
| 端口被占用 | 固定端口冲突 | 使用动态端口(已实现) |
| 登录后未显示账户 | 未触发事件 | 检查 `_onDidChangeSessions.fire()` |
| 重启后需要重新登录 | 未保存会话 | 检查 `saveSessions()` 调用 |
## 文件结构
```
ic-coder/
├── src/
│ ├── services/
│ │ └── icCoderAuthProvider.ts # Authentication Provider 实现
│ ├── views/
│ │ └── ICViewProvider.ts # 侧边栏视图(集成登录状态)
│ └── extension.ts # 注册 Provider 和命令
├── package.json # 配置 authentication 和 commands
└── docs/
└── authentication-implementation.md # 本文档
```
## 参考资料
- [VSCode Authentication API](https://code.visualstudio.com/api/references/vscode-api#authentication)
- [Authentication Provider Sample](https://github.com/microsoft/vscode-extension-samples/tree/main/authentication-sample)
- [VSCode Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
## 总结
本实现通过以下步骤完成了 VSCode Authentication API 的集成:
1. ✅ 创建 `ICCoderAuthenticationProvider` 类实现认证逻辑
2. ✅ 在 `package.json` 中注册 authentication provider
3. ✅ 在 `extension.ts` 中注册 provider 和命令
4. ✅ 实现本地 HTTP 服务器处理登录回调
5. ✅ 使用动态端口避免冲突
6. ✅ 集成到侧边栏视图,根据登录状态显示不同按钮
7. ✅ 配置网站前端支持插件登录重定向
**最终效果:**
- 用户登录后VSCode 左下角显示"IC Coder 用户"
- 侧边栏根据登录状态显示"登录账户"或"开始创作"按钮
- 支持通过账户菜单或命令进行登录/登出操作
---
**文档版本:** 1.0
**最后更新:** 2025-12-29
**作者:** Roe-xin

View File

@ -0,0 +1,279 @@
# IC Coder 插件 - 会话历史管理功能实现文档
## 概述
本次更新为 IC Coder 插件添加了完整的会话历史管理功能实现了基于任务Task的对话历史持久化存储采用 LangChain4j 兼容的消息格式。
## 核心功能
### 1. 会话历史管理系统
#### 存储架构
- **存储位置**: `~/.iccoder/projects/{项目路径编码}/{taskId}/`
- **项目路径编码规则**:
- 移除冒号 `:`
- 将斜杠 `/` 和反斜杠 `\` 替换为 `--`
- 示例: `C:\Users\admin\Documents\Project``C--Users-admin-Documents-Project`
#### 文件结构
每个任务目录包含三个文件:
```
~/.iccoder/projects/{编码后的项目路径}/{taskId}/
├── meta.json # 任务元数据
├── conversation.json # 对话历史
└── conversation_meta.jsonl # 对话轮次元数据JSONL格式
```
### 2. 新增类型定义 (`src/types/chatHistory.ts`)
#### 消息类型
- **MessageType 枚举**: `SYSTEM`, `USER`, `AI`, `TOOL_EXECUTION_RESULT`
- **消息接口**:
- `SystemMessage`: 系统消息
- `UserMessage`: 用户消息(支持多内容块)
- `AiMessage`: AI 回复(支持工具调用和思考过程)
- `ToolExecutionResultMessage`: 工具执行结果
#### 元数据结构
- **TaskMeta**: 任务元数据
- 任务 ID、名称、项目路径
- 创建/更新时间
- Token 使用统计
- **ConversationMeta**: 对话轮次元数据
- 轮次 ID、时间戳
- Token 使用量
- 模型信息、响应耗时
### 3. ChatHistoryManager 单例类 (`src/utils/chatHistoryManager.ts`)
#### 核心方法
**任务管理**:
- `createTask(projectPath, taskName)`: 创建新任务
- `switchTask(projectPath, taskId)`: 切换到指定任务
- `listProjectTasks(projectPath)`: 列出项目所有任务
- `getCurrentTaskSession()`: 获取当前任务完整会话
**消息记录**:
- `addUserMessage(text)`: 添加用户消息
- `addAiMessage(text, toolRequests?)`: 添加 AI 消息
- `addSystemMessage(text)`: 添加系统消息
- `recordTurnMeta(turnId, usage, model, duration)`: 记录对话轮次元数据
**自动化功能**:
- 自动创建存储目录
- 首次使用时自动创建默认任务
- 自动更新任务时间戳和 Token 统计
### 4. 集成到消息处理流程 (`src/utils/messageHandler.ts`)
#### 修改点
-`handleUserMessage()` 中自动记录用户消息
- 在所有 AI 回复点自动记录 AI 消息
- 支持文件操作、VCD 生成等场景的历史记录
#### 记录场景
- 普通对话消息
- 文件操作(创建、删除、读取、更新、重命名、替换)
- VCD 文件生成流程
- 错误消息
### 5. 插件初始化 (`src/extension.ts`)
- 导入 `ChatHistoryManager`
- 插件激活时自动初始化历史管理器
- 为后续命令实现预留接口(已在 `package.json` 中定义)
## 预留命令(待实现)
`package.json` 中已定义以下命令,但在 `extension.ts` 中暂时注释:
- `ic-coder.viewHistory`: 查看会话历史
- `ic-coder.newSession`: 新建会话
- `ic-coder.exportSession`: 导出当前会话
- `ic-coder.deleteSession`: 删除会话
- `ic-coder.clearHistory`: 清空会话历史
- `ic-coder.searchSession`: 搜索会话
## 技术特点
1. **单例模式**: 确保全局唯一的历史管理器实例
2. **自动初始化**: 首次使用时自动创建必要的目录和文件
3. **LangChain4j 兼容**: 消息格式与 LangChain4j 保持一致
4. **跨平台支持**: 使用 VSCode API 处理文件系统,支持 Windows/Linux/macOS
5. **错误处理**: 完善的异常捕获和错误提示
6. **增量更新**: 使用 JSONL 格式追加对话元数据,避免重写整个文件
## 使用示例
```typescript
// 获取历史管理器实例
const historyManager = ChatHistoryManager.getInstance();
// 创建新任务
await historyManager.createTask('/path/to/project', '实现计数器模块');
// 记录对话
await historyManager.addUserMessage('帮我创建一个计数器');
await historyManager.addAiMessage('好的,我来帮你创建计数器模块...');
// 记录轮次元数据
await historyManager.recordTurnMeta(1, {
inputTokens: 100,
outputTokens: 200,
totalTokens: 300
}, 'gpt-4', 2.5);
// 获取当前会话
const session = await historyManager.getCurrentTaskSession();
```
## 数据格式示例
### meta.json
```json
{
"taskId": "task_20231215_abc123",
"taskName": "实现计数器模块",
"projectPath": "D:\\ICCoderPlugin\\ic-coder",
"createdAt": "2023-12-15T10:30:00.000Z",
"updatedAt": "2023-12-15T11:45:00.000Z",
"stats": {
"credits": 0,
"totalTokens": 5000,
"inputTokens": 2000,
"outputTokens": 3000
}
}
```
### conversation.json
```json
[
{
"type": "USER",
"contents": [
{
"type": "TEXT",
"text": "帮我创建一个计数器"
}
]
},
{
"type": "AI",
"text": "好的,我来帮你创建计数器模块..."
}
]
```
### conversation_meta.jsonl
```jsonl
{"turnId":1,"timestamp":"2023-12-15T10:30:00.000Z","usage":{"inputTokens":100,"outputTokens":200,"totalTokens":300},"model":"gpt-4","duration":2.5}
{"turnId":2,"timestamp":"2023-12-15T10:32:00.000Z","usage":{"inputTokens":150,"outputTokens":250,"totalTokens":400},"model":"gpt-4","duration":3.2}
```
## 文件修改清单
### 新增文件
1. `src/types/chatHistory.ts` - 类型定义文件
2. `src/utils/chatHistoryManager.ts` - 会话历史管理器
### 修改文件
1. `src/extension.ts` - 导入 ChatHistoryManager
2. `src/utils/messageHandler.ts` - 集成历史记录功能
3. `package.json` - 定义会话管理相关命令
## 关键代码位置
### 用户消息记录
- 文件: `src/utils/messageHandler.ts:29-30`
- 代码: `await historyManager.addUserMessage(text);`
### AI 消息记录
- 文件: `src/utils/messageHandler.ts:54`
- 代码: `await historyManager.addAiMessage(reply);`
### 文件操作历史记录
- 文件: `src/utils/messageHandler.ts:194, 207, 217, 227, 243, 263`
- 各种文件操作后都会记录 AI 消息
### 任务自动创建
- 文件: `src/utils/chatHistoryManager.ts:269-279`
- 方法: `ensureCurrentTask()`
## 后续开发建议
1. **实现预留的会话管理命令**
- 查看历史:展示任务列表和对话内容
- 新建会话:创建新任务并切换
- 导出会话:支持 Markdown、JSON 等格式
- 删除会话:删除指定任务目录
- 清空历史:清空所有会话数据
- 搜索会话:按关键词搜索对话内容
2. **增强功能**
- 添加会话统计和可视化面板
- 支持多任务并行管理
- 实现会话备份和恢复功能
- 添加会话标签和分类
- 支持会话合并和拆分
3. **性能优化**
- 实现会话数据的懒加载
- 添加缓存机制减少文件读写
- 优化大型会话的加载速度
4. **用户体验**
- 添加会话切换的快捷键
- 在状态栏显示当前任务信息
- 提供会话导入功能
- 支持会话模板
## 测试建议
1. **单元测试**
- 测试项目路径编码/解码
- 测试任务创建和切换
- 测试消息添加和读取
- 测试元数据更新
2. **集成测试**
- 测试完整的对话流程
- 测试文件操作的历史记录
- 测试跨会话切换
- 测试异常情况处理
3. **性能测试**
- 测试大量消息的读写性能
- 测试多任务并发访问
- 测试长时间运行的稳定性
## 注意事项
1. **数据安全**
- 会话数据存储在用户主目录,确保权限正确
- 敏感信息不应记录到历史中
- 定期清理过期会话数据
2. **兼容性**
- 确保跨平台路径处理正确
- 注意文件编码问题(统一使用 UTF-8
- 保持与 LangChain4j 格式的兼容性
3. **错误处理**
- 文件系统操作都应有异常捕获
- 提供友好的错误提示
- 记录详细的错误日志
## 版本信息
- **功能版本**: v1.0.0
- **实现日期**: 2025-12-15
- **兼容版本**: VSCode ^1.107.0
- **依赖**: 无额外依赖,仅使用 VSCode API
## 参考资料
- [LangChain4j 消息格式](https://docs.langchain4j.dev/)
- [VSCode 文件系统 API](https://code.visualstudio.com/api/references/vscode-api#FileSystem)
- [JSONL 格式规范](http://jsonlines.org/)

View File

@ -0,0 +1,751 @@
# IC Coder 会话存储技术文档
## 1. 概述
IC Coder 的会话存储系统负责持久化保存用户与 AI 的对话历史,支持多项目、多任务的会话管理。系统采用文件系统存储方案,将会话数据按项目和任务组织,便于管理和检索。
### 1.1 核心特性
- **多项目支持**:不同项目的会话数据独立存储
- **任务级管理**:每个会话作为独立任务进行管理
- **分页加载**:支持历史会话的分页查询,提升性能
- **实时更新**:会话数据实时保存,防止数据丢失
- **统计信息**:记录 Token 使用量、对话轮次等统计数据
### 1.2 技术栈
- **存储方式**文件系统JSON/JSONL 格式)
- **存储位置**`~/.iccoder/projects/{项目路径编码}/{taskId}/`
- **数据格式**
- `meta.json`:任务元数据
- `conversation.json`:完整对话历史
- `conversation_meta.jsonl`对话轮次元数据JSONL 格式)
---
## 2. 架构设计
### 2.1 目录结构
```
~/.iccoder/
└── projects/
└── {项目路径编码}/
└── {taskId}/
├── meta.json # 任务元数据
├── conversation.json # 对话历史
└── conversation_meta.jsonl # 对话元数据
```
**项目路径编码规则**
- 移除冒号 `:`
- 将斜杠 `/` 和反斜杠 `\` 替换为 `--`
- 示例:`C:\Users\admin\Documents\Project``C--Users--admin--Documents--Project`
**任务 ID 格式**
- 格式:`task_{date}_{sequence}`
- 示例:`task_20231226_a3f9k2`
- `date`8 位日期YYYYMMDD
- `sequence`6 位随机字符串
### 2.2 核心类ChatHistoryManager
`ChatHistoryManager` 是会话存储的核心管理类,采用单例模式设计。
**主要职责**
1. 管理会话存储目录
2. 创建和切换任务
3. 保存和加载对话历史
4. 记录统计信息
5. 提供会话历史查询接口
**关键属性**
```typescript
private static instance: ChatHistoryManager;
private baseDir: string; // ~/.iccoder
private currentTaskId: string | null; // 当前任务 ID
private currentProjectPath: string | null; // 当前项目路径
```
---
## 3. 数据模型
### 3.1 TaskMeta任务元数据
存储在 `meta.json` 文件中,记录任务的基本信息和统计数据。
```typescript
interface TaskMeta {
taskId: string; // 任务 ID
taskName: string; // 任务名称
projectPath: string; // 项目路径
createdAt: string; // 创建时间ISO 8601
updatedAt: string; // 更新时间ISO 8601
stats: {
credits: number; // 消耗的积分
totalTokens: number; // 总 Token 数
inputTokens: number; // 输入 Token 数
outputTokens: number; // 输出 Token 数
};
}
```
**示例**
```json
{
"taskId": "task_20231226_a3f9k2",
"taskName": "实现计数器功能",
"projectPath": "C:\\Users\\admin\\Documents\\Project",
"createdAt": "2023-12-26T10:30:00.000Z",
"updatedAt": "2023-12-26T11:45:00.000Z",
"stats": {
"credits": 0,
"totalTokens": 15420,
"inputTokens": 8200,
"outputTokens": 7220
}
}
```
### 3.2 ChatMessage对话消息
存储在 `conversation.json` 文件中,记录完整的对话历史。
**消息类型枚举**
```typescript
enum MessageType {
USER = "USER", // 用户消息
AI = "AI", // AI 消息
SYSTEM = "SYSTEM", // 系统消息
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT" // 工具执行结果
}
```
**用户消息**
```typescript
interface UserMessage {
type: MessageType.USER;
contents: Array<{
type: "TEXT";
text: string;
}>;
}
```
**AI 消息**
```typescript
interface AiMessage {
type: MessageType.AI;
text: string;
toolExecutionRequests?: Array<{
id: string;
toolName: string;
parameters: any;
}>;
}
```
**系统消息**
```typescript
interface SystemMessage {
type: MessageType.SYSTEM;
text: string;
}
```
**工具执行结果消息**
```typescript
interface ToolExecutionResultMessage {
type: MessageType.TOOL_EXECUTION_RESULT;
id: string;
toolName: string;
text: string;
}
```
### 3.3 ConversationMeta对话轮次元数据
存储在 `conversation_meta.jsonl` 文件中每行一条记录JSONL 格式)。
```typescript
interface ConversationMeta {
turnId: number; // 对话轮次 ID
timestamp: string; // 时间戳ISO 8601
usage?: {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
};
model?: string; // 使用的模型
duration?: number; // 耗时(毫秒)
}
```
**示例**
```jsonl
{"turnId":1,"timestamp":"2023-12-26T10:30:15.000Z","usage":{"inputTokens":120,"outputTokens":350,"totalTokens":470},"model":"gpt-4","duration":2500}
{"turnId":2,"timestamp":"2023-12-26T10:32:30.000Z","usage":{"inputTokens":200,"outputTokens":450,"totalTokens":650},"model":"gpt-4","duration":3200}
```
---
## 4. 核心功能实现
### 4.1 任务创建
**方法**`createTask(projectPath: string, taskName: string): Promise<TaskMeta>`
**流程**
1. 生成唯一的任务 ID
2. 创建任务元数据对象
3. 创建任务目录
4. 保存 `meta.json`
5. 初始化空的 `conversation.json`
6. 设置为当前任务
**代码位置**`chatHistoryManager.ts:114-146`
```typescript
public async createTask(projectPath: string, taskName: string): Promise<TaskMeta> {
const taskId = this.generateTaskId();
const now = new Date().toISOString();
const meta: TaskMeta = {
taskId,
taskName,
projectPath,
createdAt: now,
updatedAt: now,
stats: {
credits: 0,
totalTokens: 0,
inputTokens: 0,
outputTokens: 0
}
};
this.currentTaskId = taskId;
this.currentProjectPath = projectPath;
// 创建任务目录
const taskDir = this.getTaskDir(projectPath, taskId);
await this.ensureTaskDir(taskDir);
// 保存 meta.json
await this.saveTaskMeta(meta);
// 初始化空的 conversation.json
await this.saveConversation([]);
return meta;
}
```
### 4.2 消息保存
系统提供了四种消息保存方法:
#### 4.2.1 添加用户消息
**方法**`addUserMessage(text: string): Promise<void>`
**代码位置**`chatHistoryManager.ts:285-299`
```typescript
public async addUserMessage(text: string): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const userMessage: UserMessage = {
type: MessageType.USER,
contents: [{ type: "TEXT", text }]
};
messages.push(userMessage);
await this.saveConversation(messages);
// 更新任务元数据
await this.updateTaskTimestamp();
}
```
#### 4.2.2 添加 AI 消息
**方法**`addAiMessage(text: string, toolRequests?: any[]): Promise<void>`
**代码位置**`chatHistoryManager.ts:304-319`
#### 4.2.3 添加系统消息
**方法**`addSystemMessage(text: string): Promise<void>`
**代码位置**`chatHistoryManager.ts:324-335`
#### 4.2.4 添加工具执行结果
**方法**`addToolExecutionResult(id: string, toolName: string, result: string): Promise<void>`
**代码位置**`chatHistoryManager.ts:340-353`
### 4.3 对话元数据记录
**方法**`recordTurnMeta(turnId, usage?, model?, duration?): Promise<void>`
**功能**:记录每轮对话的元数据,包括 Token 使用量、模型信息、耗时等。
**代码位置**`chatHistoryManager.ts:358-378`
```typescript
public async recordTurnMeta(
turnId: number,
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number },
model?: string,
duration?: number
): Promise<void> {
const meta: ConversationMeta = {
turnId,
timestamp: new Date().toISOString(),
usage,
model,
duration
};
await this.appendConversationMeta(meta);
// 更新任务统计
if (usage) {
await this.updateTaskStats(usage);
}
}
```
### 4.4 会话历史查询
**方法**`getConversationHistoryList(projectPath, offset, limit): Promise<{items, total, hasMore}>`
**功能**:分页查询项目的会话历史列表。
**参数**
- `projectPath`:项目路径
- `offset`:偏移量(从第几条开始,默认 0
- `limit`:每页数量(默认 10
**返回值**
```typescript
{
items: Array<{
id: string; // 任务 ID
title: string; // 会话标题(第一句用户消息)
timestamp: string; // 创建时间
}>;
total: number; // 总数
hasMore: boolean; // 是否还有更多
}
```
**代码位置**`chatHistoryManager.ts:525-590`
**实现逻辑**
1. 获取项目的所有任务列表(按更新时间倒序)
2. 根据 offset 和 limit 进行分页
3. 读取每个任务的 `conversation.json`
4. 提取第一条用户消息作为标题(截取前 50 个字符)
5. 返回分页结果
---
## 5. 前端集成
### 5.1 会话历史栏组件
**文件**`conversationHistoryBar.ts`
**组件结构**
- 下拉按钮:显示 "Past Conversations"
- 下拉菜单:显示会话历史列表
- 新建按钮:创建新会话
**关键功能**
#### 5.1.1 加载会话历史
```javascript
function loadMoreHistory() {
if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
return;
}
// 检查是否已达到最大数量100 条)
if (currentOffset >= MAX_HISTORY_ITEMS) {
return;
}
isLoadingHistory = true;
vscode.postMessage({
command: 'loadConversationHistory',
offset: currentOffset,
limit: HISTORY_PAGE_SIZE
});
}
```
#### 5.1.2 渲染会话列表
```javascript
function renderConversationHistory(data) {
isLoadingHistory = false;
// 追加新数据
conversationHistory = conversationHistory.concat(data.items);
totalHistory = data.total;
hasMoreHistory = data.hasMore;
currentOffset += data.items.length;
// 渲染所有历史记录
historyList.innerHTML = conversationHistory.map(item => `
<div class="history-item" onclick="selectConversation('${item.id}')">
<div class="history-item-title">${item.title || '未命名会话'}</div>
<div class="history-item-time">${formatTime(item.timestamp)}</div>
</div>
`).join('');
// 如果还有更多数据,添加"加载更多"提示
if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
historyList.innerHTML += `
<div class="history-load-more">
<span>滚动加载更多...</span>
</div>
`;
}
}
```
#### 5.1.3 滚动加载
```javascript
historyDropdownMenu.addEventListener('scroll', () => {
const menu = historyDropdownMenu;
const scrollTop = menu.scrollTop;
const scrollHeight = menu.scrollHeight;
const clientHeight = menu.clientHeight;
// 当滚动到距离底部 50px 时,加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
loadMoreHistory();
}
});
```
#### 5.1.4 时间格式化
```javascript
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
// 超过7天显示具体日期
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
```
### 5.2 后端消息处理
**文件**`ICHelperPanel.ts`
**消息处理流程**
```typescript
case "loadConversationHistory":
// 加载会话历史(支持分页)
loadConversationHistory(panel, message.offset || 0, message.limit || 10);
break;
case "selectConversation":
// 选择会话(暂未实现)
break;
case "createNewConversation":
// 创建新会话 - 在当前编辑器组中打开新标签页
showICHelperPanel(context, panel.viewColumn);
break;
```
**加载会话历史实现**
```typescript
async function loadConversationHistory(
panel: vscode.WebviewPanel,
offset: number = 0,
limit: number = 10
) {
try {
const historyManager = ChatHistoryManager.getInstance();
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspacePath) {
// 没有打开的工作区,返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
return;
}
// 获取会话历史列表(支持分页)
const result = await historyManager.getConversationHistoryList(
workspacePath,
offset,
limit
);
// 发送会话历史到前端
panel.webview.postMessage({
command: "conversationHistory",
items: result.items,
total: result.total,
hasMore: result.hasMore,
});
} catch (error) {
console.error("加载会话历史失败:", error);
// 发生错误时返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
}
}
```
---
## 6. 使用示例
### 6.1 创建新任务并保存对话
```typescript
const historyManager = ChatHistoryManager.getInstance();
// 创建新任务
const task = await historyManager.createTask(
'C:\\Users\\admin\\Documents\\Project',
'实现计数器功能'
);
// 添加用户消息
await historyManager.addUserMessage('请帮我生成一个4位计数器');
// 添加 AI 消息
await historyManager.addAiMessage(
'好的我来帮你生成一个4位计数器...',
[{ id: '1', toolName: 'generateCode', parameters: {} }]
);
// 添加工具执行结果
await historyManager.addToolExecutionResult(
'1',
'generateCode',
'代码生成成功'
);
// 记录对话元数据
await historyManager.recordTurnMeta(
1,
{ inputTokens: 120, outputTokens: 350, totalTokens: 470 },
'gpt-4',
2500
);
```
### 6.2 查询会话历史
```typescript
const historyManager = ChatHistoryManager.getInstance();
// 获取第一页前10条
const page1 = await historyManager.getConversationHistoryList(
'C:\\Users\\admin\\Documents\\Project',
0,
10
);
console.log('总数:', page1.total);
console.log('是否还有更多:', page1.hasMore);
console.log('会话列表:', page1.items);
// 获取第二页第11-20条
const page2 = await historyManager.getConversationHistoryList(
'C:\\Users\\admin\\Documents\\Project',
10,
10
);
```
### 6.3 切换任务
```typescript
const historyManager = ChatHistoryManager.getInstance();
// 切换到指定任务
const success = await historyManager.switchTask(
'C:\\Users\\admin\\Documents\\Project',
'task_20231226_a3f9k2'
);
if (success) {
// 获取当前任务会话
const session = await historyManager.getCurrentTaskSession();
console.log('任务元数据:', session.meta);
console.log('对话历史:', session.messages);
console.log('对话元数据:', session.conversationMeta);
}
```
---
## 7. 性能优化
### 7.1 分页加载
- 前端默认每页加载 10 条记录
- 最多显示 100 条历史记录
- 滚动到底部时自动加载下一页
### 7.2 懒加载
- 只在打开下拉菜单时才加载会话历史
- 避免不必要的文件读取操作
### 7.3 缓存机制
- 前端缓存已加载的会话列表
- 避免重复请求相同数据
### 7.4 文件格式优化
- 使用 JSONL 格式存储对话元数据,支持追加写入
- 避免频繁读写整个文件
---
## 8. 错误处理
### 8.1 目录不存在
系统会自动创建不存在的目录:
```typescript
private async ensureTaskDir(taskDir: string): Promise<void> {
try {
const uri = vscode.Uri.file(taskDir);
try {
await vscode.workspace.fs.stat(uri);
} catch {
// 目录不存在,创建它
await vscode.workspace.fs.createDirectory(uri);
console.log(`创建任务目录: ${taskDir}`);
}
} catch (error) {
console.error("创建任务目录失败:", error);
throw error;
}
}
```
### 8.2 文件读取失败
读取失败时返回默认值:
```typescript
private async loadConversation(): Promise<ChatMessage[]> {
try {
const uri = vscode.Uri.file(conversationPath);
const content = await vscode.workspace.fs.readFile(uri);
const data = Buffer.from(content).toString('utf-8');
return JSON.parse(data);
} catch (error) {
// 文件不存在或读取失败,返回空数组
return [];
}
}
```
### 8.3 无工作区处理
没有打开工作区时,自动创建默认任务:
```typescript
private async ensureCurrentTask(): Promise<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (workspacePath) {
await this.createTask(workspacePath, "默认任务");
} else {
throw new Error("没有打开的工作区,无法创建任务");
}
}
}
```
---
## 9. 未来扩展
### 9.1 会话切换功能
目前 `selectConversation` 功能暂未实现,未来可以支持:
- 点击历史会话,加载该会话的完整对话历史
- 在新标签页中打开历史会话
- 继续历史会话的对话
### 9.2 会话搜索
- 支持按关键词搜索会话
- 支持按时间范围筛选
- 支持按 Token 使用量排序
### 9.3 会话导出
- 导出为 Markdown 格式
- 导出为 JSON 格式
- 导出为 PDF 格式
### 9.4 会话统计
- 显示总对话轮次
- 显示总 Token 使用量
- 显示平均响应时间
### 9.5 云端同步
- 支持将会话数据同步到云端
- 支持多设备访问
- 支持团队协作
---
## 10. 总结
IC Coder 的会话存储系统采用文件系统存储方案,具有以下优势:
1. **简单可靠**:无需额外的数据库依赖
2. **易于备份**:直接复制文件即可备份
3. **跨平台**:支持 Windows、macOS、Linux
4. **可扩展**:易于添加新的数据字段
5. **高性能**:分页加载,避免一次性加载大量数据
系统已经实现了核心的会话管理功能,包括任务创建、消息保存、历史查询等,为用户提供了完整的会话历史管理体验。

View File

@ -0,0 +1,444 @@
# 波形预览功能技术文档
## 功能概述
在对话界面中显示 VCD 波形文件的预览卡片,用户可以查看前几个信号的真实波形,并通过"展开查看"按钮打开完整的波形查看器。
## 功能流程
```
用户输入"生成VCD"命令
系统执行 iverilog 编译和仿真
生成 VCD 文件
在对话界面显示波形预览卡片
├─ 显示真实的波形图前3个信号
├─ 显示信号名称和波形
└─ "展开查看"按钮
点击"展开查看"按钮
打开完整的 VCDViewerPanel 波形查看器
```
## 文件结构
### 1. `src/views/waveformPreviewContent.ts`
**功能:** 波形预览组件的独立模块
**导出函数:**
- `getWaveformPreviewContent()` - 返回波形预览组件的 CSS 样式
- `getWaveformPreviewScript()` - 返回波形预览组件的 JavaScript 代码
**主要功能:**
- 创建波形预览卡片的 HTML 结构
- 从 VCD 文件中提取真实信号数据
- 使用 SVG 绘制波形图
- 单比特信号:绘制数字波形(高/低电平)
- 多比特信号:绘制总线波形(梯形)
- 处理"展开查看"按钮点击事件
**关键函数:**
```javascript
createWaveformPreview(vcdFilePath, fileName)
- 创建波形预览卡片的 DOM 结构
- 包含头部标题 + 展开按钮和内容区域
loadMiniWaveform(containerId, vcdFilePath, loadingDiv)
- 请求后端获取 VCD 文件信息
renderWaveformInfo(containerId, vcdInfo)
- 接收 VCD 信息并渲染波形
drawRealWaveform(signals)
- 根据真实信号数据绘制 SVG 波形图
- 支持单比特和多比特信号
- 使用不同颜色区分信号
openFullWaveform(vcdFilePath)
- 发送消息打开完整波形查看器
addWaveformPreviewToMessage(messageDiv, vcdFilePath, fileName)
- 将波形预览组件添加到消息中
```
---
### 2. `src/views/webviewContent.ts`
**功能:** 主 WebView 页面,集成波形预览组件
**修改内容:**
- 导入波形预览组件的样式和脚本
-`<style>` 标签中插入 `getWaveformPreviewContent()`
-`<script>` 标签中插入 `getWaveformPreviewScript()`
- 添加 `vcdGenerated` 消息处理逻辑
- 添加 `vcdInfo` 消息处理逻辑
**消息处理:**
```javascript
case 'vcdGenerated':
// VCD 文件生成成功,显示带波形预览的消息
- 创建消息 div
- 添加成功消息文本
- 调用 addWaveformPreviewToMessage() 添加波形预览卡片
case 'vcdInfo':
// 接收到 VCD 文件信息,渲染波形预览
- 调用 renderWaveformInfo() 渲染波形
```
---
### 3. `src/utils/messageHandler.ts`
**功能:** 处理用户消息和 VCD 生成请求
**修改内容:**
- 导入 `path` 模块
- 修改 VCD 生成成功后的消息发送逻辑
**关键代码:**
```typescript
if (result.success) {
if (result.vcdFilePath) {
const fileName = path.basename(result.vcdFilePath);
panel.webview.postMessage({
command: "vcdGenerated", // 发送 vcdGenerated 消息
text: successMsg,
vcdFilePath: result.vcdFilePath,
fileName: fileName,
});
}
}
```
---
### 4. `src/panels/ICHelperPanel.ts`
**功能:** IC 助手面板,处理 WebView 消息
**修改内容:**
- 导入 `VCDViewerPanel`
- 添加 `openWaveformViewer` 命令处理
- 添加 `getVCDInfo` 命令处理
- 新增 `getVCDFileInfo()` 函数
- 新增 `parseVCDSignals()` 函数
**新增函数:**
#### `getVCDFileInfo(panel, vcdFilePath, containerId)`
**功能:** 获取 VCD 文件信息并解析信号数据
**处理流程:**
1. 检查文件是否存在
2. 获取文件大小
3. 读取 VCD 文件内容
4. 解析信号数量
5. 解析时间范围
6. 调用 `parseVCDSignals()` 解析前 3 个信号的数据
7. 发送 `vcdInfo` 消息回前端
**返回数据结构:**
```typescript
{
command: "vcdInfo",
containerId: string,
vcdInfo: {
signalCount: string,
timeRange: string,
fileSize: string,
signals: Array<{
name: string,
identifier: string,
width: number,
values: Array<{ time: number, value: string }>
}>
}
}
```
#### `parseVCDSignals(content, maxSignals)`
**功能:** 解析 VCD 文件中的信号数据
**处理流程:**
1. 使用正则表达式解析信号定义部分(`$var ... $end`
2. 提取信号名称、标识符、位宽
3. 找到数据变化部分(`$dumpvars` 之后)
4. 解析每个信号的值变化
- 单比特信号:格式 `0!``1!`
- 多比特信号:格式 `b1010 !`
5. 限制最多 50 个采样点(避免数据过多)
6. 返回信号数据数组
**VCD 文件格式示例:**
```vcd
$var wire 1 ! clk $end
$var wire 8 " data $end
$dumpvars
0!
b00000000 "
#10
1!
#20
0!
b00000001 "
```
**消息处理:**
```typescript
case "openWaveformViewer":
// 打开完整波形查看器
VCDViewerPanel.createOrShow(context.extensionUri, message.vcdFilePath);
case "getVCDInfo":
// 获取 VCD 文件信息
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
```
---
### 5. `src/panels/VCDViewerPanel.ts`
**功能:** 完整的 VCD 波形查看器面板(已存在)
**作用:** 当用户点击"展开查看"按钮时,打开此面板显示完整的波形
---
## 数据流
### 1. VCD 生成流程
```
用户输入 → messageHandler.handleUserMessage()
检测到 VCD 生成命令
messageHandler.handleVCDGeneration()
iverilogRunner.generateVCD()
生成 VCD 文件
发送 vcdGenerated 消息到前端
webviewContent 接收消息
创建波形预览卡片
```
### 2. 波形预览加载流程
```
createWaveformPreview() 创建卡片
loadMiniWaveform() 请求 VCD 信息
发送 getVCDInfo 消息到后端
ICHelperPanel 接收消息
getVCDFileInfo() 读取并解析 VCD 文件
parseVCDSignals() 解析信号数据
发送 vcdInfo 消息到前端
renderWaveformInfo() 渲染波形
drawRealWaveform() 绘制 SVG 波形图
```
### 3. 展开查看流程
```
用户点击"展开查看"按钮
openFullWaveform() 发送消息
发送 openWaveformViewer 消息到后端
ICHelperPanel 接收消息
VCDViewerPanel.createOrShow()
打开完整波形查看器窗口
```
---
## 样式说明
### CSS 类名
- `.waveform-preview` - 波形预览卡片容器
- `.waveform-preview-header` - 卡片头部
- `.waveform-preview-title` - 标题区域
- `.waveform-expand-btn` - 展开按钮
- `.waveform-preview-content` - 内容区域
- `.waveform-mini-viewer` - 波形显示容器
- `.waveform-loading` - 加载提示
### 波形绘制
- **单比特信号**:使用 SVG `<path>` 绘制数字波形
- 高电平y = 顶部
- 低电平y = 底部
- 垂直跳变表示信号变化
- **多比特信号**:使用 SVG `<path>` 绘制总线波形
- 梯形表示数据变化
- 中间线表示稳定状态
- **颜色方案**
- 第1个信号蓝色 (`var(--vscode-charts-blue)`)
- 第2个信号绿色 (`var(--vscode-charts-green)`)
- 第3个信号橙色 (`var(--vscode-charts-orange)`)
---
## 配置参数
### 可调整参数
**在 `ICHelperPanel.ts` 中:**
```typescript
const signals = parseVCDSignals(content, 3); // 解析前3个信号
```
- 修改数字可以改变显示的信号数量
**在 `parseVCDSignals()` 中:**
```typescript
if (values.length >= 50) {
break; // 限制最多50个采样点
}
```
- 修改数字可以改变采样点数量
**在 `drawRealWaveform()` 中:**
```typescript
const signalHeight = 20; // 信号高度
const signalSpacing = 30; // 信号间距
const leftMargin = 80; // 左边距(信号名称区域)
const rightMargin = 20; // 右边距
```
- 修改这些参数可以调整波形显示样式
---
## 性能优化
1. **限制信号数量**:只解析前 3 个信号
2. **限制采样点**:每个信号最多 50 个采样点
3. **轻量级渲染**:使用 SVG 而不是 Canvas
4. **按需加载**:只在需要时读取和解析 VCD 文件
---
## 错误处理
### 文件不存在
```typescript
if (!fs.existsSync(vcdFilePath)) {
// 返回错误信息
vcdInfo: {
signalCount: 'N/A',
timeRange: 'N/A',
fileSize: 'N/A',
error: '文件不存在'
}
}
```
### 解析失败
```typescript
try {
// 解析逻辑
} catch (error) {
console.error('解析 VCD 信号数据失败:', error);
return []; // 返回空数组
}
```
### 无信号数据
```typescript
if (!signals || signals.length === 0) {
return `<svg>无波形数据</svg>`;
}
```
---
## 未来扩展
### 可能的改进方向
1. **交互功能**
- 鼠标悬停显示信号值
- 点击信号高亮显示
- 缩放和平移功能
2. **显示优化**
- 自动选择最有代表性的信号
- 智能采样(保留关键变化点)
- 支持更多信号类型(模拟信号等)
3. **性能优化**
- 使用 Web Worker 解析大文件
- 虚拟滚动显示大量信号
- 缓存解析结果
4. **功能增强**
- 支持信号搜索和过滤
- 导出波形图片
- 比较多个 VCD 文件
---
## 调试技巧
### 查看消息流
在浏览器开发者工具中查看 `vscode.postMessage()` 的调用:
```javascript
console.log('发送消息:', message);
vscode.postMessage(message);
```
### 查看解析结果
`parseVCDSignals()` 中添加日志:
```typescript
console.log('解析到的信号:', signals);
```
### 查看 SVG 输出
`drawRealWaveform()` 中添加日志:
```javascript
console.log('SVG 内容:', svgContent);
```
---
## 相关文件
- `src/utils/iverilogRunner.ts` - VCD 文件生成
- `src/panels/VCDViewerPanel.ts` - 完整波形查看器
- `media/vcdrom/` - VCDrom 波形查看库
---
## 版本历史
### v1.0 (当前版本)
- ✅ 创建独立的波形预览组件
- ✅ 解析 VCD 文件中的真实信号数据
- ✅ 绘制单比特和多比特信号波形
- ✅ 支持展开查看完整波形
- ✅ 轻量级预览,快速加载
---
## 总结
波形预览功能通过以下文件协同工作:
1. **`waveformPreviewContent.ts`** - 组件核心逻辑
2. **`webviewContent.ts`** - 集成到主页面
3. **`messageHandler.ts`** - 处理 VCD 生成
4. **`ICHelperPanel.ts`** - 解析 VCD 数据和消息处理
整个功能采用模块化设计,易于维护和扩展。

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 889 KiB

After

Width:  |  Height:  |  Size: 889 KiB

View File

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
media/vcdrom/vcd.wasm Normal file

Binary file not shown.

13
media/vcdrom/vcdrom.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,13 @@
{ {
"name": "ic-coder-plugin", "name": "iccoder",
"displayName": "IC Coder plugin", "displayName": "IC Coder",
"description": "Agentic Verilog Coding Platform for Real-World FPGAs", "description": "Agentic Verilog Coding Platform for Real-World FPGAs",
"version": "0.0.2", "version": "0.0.2",
"publisher": "ICCoder",
"engines": { "engines": {
"vscode": "^1.106.3" "vscode": "^1.80.0"
}, },
"icon": "media/图案(方底).png", "icon": "media/icon.png",
"categories": [ "categories": [
"Other" "Other"
], ],
@ -18,6 +19,7 @@
"eda", "eda",
"assistant" "assistant"
], ],
"license": "SEE LICENSE IN LICENSE",
"activationEvents": [ "activationEvents": [
"onCommand:ic-coder.openPanel", "onCommand:ic-coder.openPanel",
"onView:ic-coder-sidebar", "onView:ic-coder-sidebar",
@ -37,6 +39,11 @@
"command": "ic-coder.openChat", "command": "ic-coder.openChat",
"title": "IC Coder: 打开聊天", "title": "IC Coder: 打开聊天",
"category": "IC Coder" "category": "IC Coder"
},
{
"command": "ic-coder.openVCDViewer",
"title": "打开 VCD 波形查看器",
"category": "IC Coder"
} }
], ],
"viewsContainers": { "viewsContainers": {
@ -44,7 +51,7 @@
{ {
"id": "ic-coder-sidebar", "id": "ic-coder-sidebar",
"title": "IC Coder", "title": "IC Coder",
"icon": "media/侧边栏logo.png" "icon": "media/sidebar-icon.png"
} }
] ]
}, },
@ -56,6 +63,32 @@
"type": "webview" "type": "webview"
} }
] ]
},
"authentication": [
{
"id": "iccoder",
"label": "IC Coder"
}
],
"configuration": {
"title": "IC Coder",
"properties": {
"icCoder.backendUrl": {
"type": "string",
"default": "http://192.168.1.108:2233",
"description": "后端服务地址"
},
"icCoder.timeout": {
"type": "number",
"default": 60000,
"description": "请求超时时间(毫秒)"
},
"icCoder.userId": {
"type": "string",
"default": "default-user",
"description": "用户ID临时配置"
}
}
} }
}, },
"scripts": { "scripts": {
@ -71,16 +104,32 @@
"build": "pnpm run compile" "build": "pnpm run compile"
}, },
"devDependencies": { "devDependencies": {
"@types/vscode": "^1.107.0",
"@types/mocha": "^10.0.10", "@types/mocha": "^10.0.10",
"@types/node": "22.x", "@types/node": "22.x",
"typescript-eslint": "^8.48.1", "@types/vscode": "^1.80.0",
"eslint": "^9.39.1",
"typescript": "^5.9.3",
"ts-loader": "^9.5.4",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1",
"@vscode/test-cli": "^0.0.12", "@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2" "@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.7.1",
"eslint": "^9.39.1",
"ts-loader": "^9.5.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.48.1",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1"
},
"files": [
"dist",
"media",
"tools"
],
"dependencies": {
"@wavedrom/doppler": "^1.14.0",
"eventsource-parser": "^3.0.6",
"iconv-lite": "^0.7.1",
"onml": "^2.1.0",
"style-mod": "^4.1.3",
"vcd-stream": "^1.5.0",
"vcdrom": "^1.6.0",
"waveql": "^1.9.0"
} }
} }

2198
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

48
src/config/settings.ts Normal file
View File

@ -0,0 +1,48 @@
/**
* 配置管理
* 从 VSCode 设置读取配置项
*/
import * as vscode from "vscode";
/** 配置项接口 */
export interface IccoderConfig {
/** 后端服务地址 */
backendUrl: string;
/** 请求超时时间(毫秒) */
timeout: number;
/** 用户ID临时使用后续对接认证 */
userId: string;
}
/** 默认配置 */
const DEFAULT_CONFIG: IccoderConfig = {
backendUrl: "http://localhost:8080",
timeout: 60000,
userId: "default-user",
};
/**
* 获取配置项
*/
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),
};
}
/**
* 获取后端 API 地址
*/
export function getApiUrl(path: string): string {
const { backendUrl } = getConfig();
// 确保 URL 格式正确
const baseUrl = backendUrl.endsWith("/")
? backendUrl.slice(0, -1)
: backendUrl;
const apiPath = path.startsWith("/") ? path : `/${path}`;
return `${baseUrl}${apiPath}`;
}

View File

@ -0,0 +1,72 @@
/**
* 工具图标定义
* 包含各种工具的 SVG 图标
*/
/**
* 折叠图标 SVG用于可折叠的工具结果
*/
export const collapseIconSvg = `
<span class="tool-collapse-icon">
<svg class="icon-collapsed" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M355.05845325 160.07583932c-19.63862503 19.63862503-19.63862503 51.53175211 0 71.17037712L618.05891976 494.24668297c9.74075802 9.74075802 9.74075802 25.76587604 0 35.50663406L355.05845325 792.75378356c-19.63862503 19.63862503-19.63862503 51.53175211 0 71.17037712s51.53175211 19.63862503 71.17037716 0L706.98261396 583.17037714c39.27725009-39.27725009 39.27725009-102.90639522 0-142.18364526L426.22883041 160.07583932c-19.63862503-19.63862503-51.53175211-19.63862503-71.17037716 0z" fill="#8a8a8a"/>
</svg>
<svg class="icon-expanded" style="display:none;" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M899.70688 272.92672l-382.19776 373.53472-393.45664-384.512a43.52 43.52 0 0 0-60.52352 0 41.14944 41.14944 0 0 0 0 59.14624l423.72096 414.11584a43.35616 43.35616 0 0 0 60.56448 0l412.4672-403.11296a41.20064 41.20064 0 0 0 11.06432-40.41728 42.3424 42.3424 0 0 0-30.2848-29.58336 43.52 43.52 0 0 0-41.35424 10.84416z m0 0" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 文件写入完成图标 SVG
*/
export const fileWriteIconSvg = `
<span class="tool-file-write-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M866.304 852.096H161.728a31.36 31.36 0 0 1-30.528-30.592 31.36 31.36 0 0 1 30.528-30.528h704.64a31.36 31.36 0 0 1 30.528 30.528c0 16.32-12.224 30.592-30.592 30.592z m-65.152-134.4h-392.96a31.36 31.36 0 0 1-30.592-30.592 31.36 31.36 0 0 1 30.528-30.528h391.04a31.36 31.36 0 0 1 30.528 30.528c0 16.32-12.224 30.592-28.544 30.592z m-596.672-179.2l91.648 93.632-40.704 40.768-91.648-91.648 40.704-42.752zM552.704 188.16l91.648 93.696-42.752 40.704-91.648-91.648 42.752-42.752z" fill="#8a8a8a"/>
<path d="M176 733.952a72.96 72.96 0 0 1-50.88-22.4c-14.272-14.272-22.4-36.672-22.4-56.96l8.128-99.84 423.552-425.6a104.576 104.576 0 0 1 75.328-30.528c32.64 0 63.168 14.272 85.568 36.672 24.384 24.384 36.608 56.96 36.608 89.6 0 28.48-12.16 54.976-32.576 73.28l-419.456 427.648-99.84 8.128H176z m-4.096-152.704l-6.08 77.376c0 4.096 2.048 8.128 4.096 8.128h2.048l77.376-6.08 417.344-425.6c8.128-8.128 12.224-18.304 12.224-28.48 0-14.272-6.08-28.48-16.32-38.72-10.24-10.24-22.4-16.32-36.672-16.32-12.16 0-22.4 4.096-30.528 12.224L171.904 581.248z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 语法检查图标 SVG
*/
export const syntaxCheckIconSvg = `
<span class="tool-syntax-check-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M143.36 241.8688h638.976a33.28 33.28 0 0 0 0-66.56H143.36a33.28 33.28 0 0 0 0 66.56zM143.36 421.2736h423.5264a33.28 33.28 0 0 0 0-66.56H143.36a33.28 33.28 0 0 0 0 66.56zM419.0208 532.8384H143.36a33.28 33.28 0 0 0 0 66.56h275.6608a33.28 33.28 0 0 0 0-66.56zM365.5168 709.5296H129.0752a33.28 33.28 0 0 0 0 66.56h236.4416a33.28 33.28 0 1 0 0-66.56zM918.4256 791.8592l-82.5856-82.432a178.8928 178.8928 0 1 0-47.0528 47.0528l82.5856 82.4832a33.28 33.28 0 1 0 47.0528-47.104z m-342.3232-182.9376a112.128 112.128 0 1 1 112.128 112.0768 112.2816 112.2816 0 0 1-112.128-112.0768z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 已检索代码图标 SVG
*/
export const SearchCode = `
<span class="tool-search-code-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M916.33 859.76L678.51 621.94A318.92 318.92 0 0 0 768 400c0-176.73-143.27-320-320-320S128 223.27 128 400s143.27 320 320 320a318.48 318.48 0 0 0 167.88-47.55l243.88 243.88a40 40 0 1 0 56.57-56.57zM192 400c0-141.38 114.62-256 256-256s256 114.62 256 256-114.62 256-256 256-256-114.62-256-256z" fill="#8a8a8a"/>
</svg>
</span>
`;
/**
* 发送按钮图标 SVG向上箭头
*/
export const sendIconSvg = `
<svg class="send-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M507.904 882.688c-18.432 0-33.28-14.848-33.28-33.28v-655.36c0-18.432 14.848-33.28 33.28-33.28s33.28 14.848 33.28 33.28v654.848c0 18.432-14.848 33.792-33.28 33.792z" fill="currentColor"></path>
<path d="M787.968 502.784c-8.704 0-16.896-3.072-23.552-9.728L507.904 236.544 251.392 493.056c-12.8 12.8-34.304 12.8-47.104 0-12.8-12.8-12.8-34.304 0-47.104l280.064-280.064c6.144-6.144 14.848-9.728 23.552-9.728s17.408 3.584 23.552 9.728l280.064 280.064c12.8 12.8 12.8 34.304 0 47.104-6.656 6.656-15.36 9.728-23.552 9.728z" fill="currentColor"></path>
</svg>
`;
/**
* 暂停按钮图标 SVG圆形边框内的方块
*/
export const stopIconSvg = `
<svg class="stop-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M512 936a424.1 424.1 0 0 1-165.05-814.66 424.1 424.1 0 0 1 330.1 781.33A421.38 421.38 0 0 1 512 936z m0-768c-189.68 0-344 154.32-344 344s154.32 344 344 344 344-154.32 344-344-154.32-344-344-344z" fill="currentColor"></path>
<path d="M349.75 349.75m57.15 0l210.2 0q57.15 0 57.15 57.15l0 210.2q0 57.15-57.15 57.15l-210.2 0q-57.15 0-57.15-57.15l0-210.2q0-57.15 57.15-57.15Z" fill="currentColor"></path>
</svg>
`;

View File

@ -1,28 +1,160 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { ICViewProvider } from "./views/ICViewProvider"; import { ICViewProvider } from "./views/ICViewProvider";
import { showICHelperPanel } from "./panels/ICHelperPanel"; import { showICHelperPanel } from "./panels/ICHelperPanel";
import { VCDViewerPanel } from "./panels/VCDViewerPanel";
import { ChatHistoryManager } from "./utils/chatHistoryManager";
import { ICCoderAuthenticationProvider } from "./services/icCoderAuthProvider";
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log("🎉 IC Coder 插件已激活!"); console.log("🎉 IC Coder 插件已激活!");
// 注册 Authentication Provider
const authProvider = new ICCoderAuthenticationProvider(context);
context.subscriptions.push(
vscode.authentication.registerAuthenticationProvider(
"iccoder",
"IC Coder",
authProvider
)
);
// 检查登录状态,如果已登录则自动打开聊天面板
vscode.authentication.getSession("iccoder", [], { createIfNone: false })
.then((session) => {
if (session) {
vscode.commands.executeCommand("ic-coder.openChat");
}
}, () => {
// 未登录,不做任何操作
});
// 注册命令:打开助手面板 // 注册命令:打开助手面板
const openPanelCommand = vscode.commands.registerCommand( const openPanelCommand = vscode.commands.registerCommand(
"ic-coder.openPanel", "ic-coder.openPanel",
() => { async () => {
showICHelperPanel(context); await showICHelperPanel(context);
} }
); );
// 注册命令:打开聊天(用于侧边栏) // 注册命令:打开聊天(用于侧边栏)
const openChatCommand = vscode.commands.registerCommand( const openChatCommand = vscode.commands.registerCommand(
"ic-coder.openChat", "ic-coder.openChat",
() => { async () => {
showICHelperPanel(context); await showICHelperPanel(context);
} }
); );
// 注册命令:打开 VCD 波形查看器
const openVCDViewerCommand = vscode.commands.registerCommand(
"ic-coder.openVCDViewer",
async (vcdFilePath?: string) => {
if (!vcdFilePath) {
// 如果没有提供文件路径,让用户选择 VCD 文件
const fileUri = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
filters: {
"VCD 文件": ["vcd"],
"所有文件": ["*"],
},
title: "选择 VCD 文件",
});
if (fileUri && fileUri[0]) {
vcdFilePath = fileUri[0].fsPath;
} else {
return;
}
}
VCDViewerPanel.createOrShow(context.extensionUri, vcdFilePath);
}
);
// 注册命令:用户登录
const loginCommand = vscode.commands.registerCommand(
"ic-coder.login",
async () => {
try {
await vscode.authentication.getSession("iccoder", [], { createIfNone: true });
} catch (error) {
vscode.window.showErrorMessage(`登录失败: ${error}`);
}
}
);
// 注册命令:用户登出
const logoutCommand = vscode.commands.registerCommand(
"ic-coder.logout",
async () => {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
if (session) {
// 通过创建新会话并清除偏好来实现登出
await vscode.authentication.getSession("iccoder", [], {
clearSessionPreference: true,
forceNewSession: true
});
vscode.window.showInformationMessage("已退出登录");
} else {
vscode.window.showInformationMessage("当前未登录");
}
} catch (error) {
vscode.window.showInformationMessage("当前未登录");
}
}
);
// 注册命令:查看会话历史
// TODO: 这些命令需要根据新的任务架构重新实现
// 暂时注释掉,等待重新实现
/*
const viewHistoryCommand = vscode.commands.registerCommand(
"ic-coder.viewHistory",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const newSessionCommand = vscode.commands.registerCommand(
"ic-coder.newSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const exportSessionCommand = vscode.commands.registerCommand(
"ic-coder.exportSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const deleteSessionCommand = vscode.commands.registerCommand(
"ic-coder.deleteSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const clearHistoryCommand = vscode.commands.registerCommand(
"ic-coder.clearHistory",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
const searchSessionCommand = vscode.commands.registerCommand(
"ic-coder.searchSession",
() => {
vscode.window.showInformationMessage("此功能正在重构中,敬请期待");
}
);
*/
// 注册侧边栏视图 // 注册侧边栏视图
const viewProvider = new ICViewProvider(context.extensionUri); const viewProvider = new ICViewProvider(context.extensionUri, context);
const viewRegistration = vscode.window.registerWebviewViewProvider( const viewRegistration = vscode.window.registerWebviewViewProvider(
"ic-coder.mainView", "ic-coder.mainView",
viewProvider viewProvider
@ -32,6 +164,16 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push( context.subscriptions.push(
openPanelCommand, openPanelCommand,
openChatCommand, openChatCommand,
openVCDViewerCommand,
loginCommand,
logoutCommand,
// TODO: 等待重新实现这些命令
// viewHistoryCommand,
// newSessionCommand,
// exportSessionCommand,
// deleteSessionCommand,
// clearHistoryCommand,
// searchSessionCommand,
viewRegistration viewRegistration
); );
} }

View File

@ -6,18 +6,46 @@ import {
handleReadFile, handleReadFile,
handleUpdateFile, handleUpdateFile,
handleRenameFile, handleRenameFile,
handleReplaceInFile handleReplaceInFile,
handleUserAnswer,
abortCurrentDialog,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
import { VCDViewerPanel } from "./VCDViewerPanel";
import { ChatHistoryManager } from "../utils/chatHistoryManager";
import { MessageType } from "../types/chatHistory";
/** /**
* 创建并显示 IC 助手面板 * 创建并显示 IC 助手面板
*/ */
export function showICHelperPanel(context: vscode.ExtensionContext) { export async function showICHelperPanel(
context: vscode.ExtensionContext,
viewColumn?: vscode.ViewColumn
) {
// 检查用户是否已登录
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
if (!session) {
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
});
return;
}
} catch (error) {
vscode.window.showWarningMessage("请先登录后再使用 IC Coder", "立即登录").then((selection) => {
if (selection === "立即登录") {
vscode.commands.executeCommand("ic-coder.login");
}
});
return;
}
// 创建WebView面板 // 创建WebView面板
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"icCoder", // 面板ID "icCoder", // 面板ID
"IC Coder", // 面板标题 "IC Coder", // 面板标题
vscode.ViewColumn.Beside, // 显示在旁边 viewColumn || vscode.ViewColumn.Beside, // 默认显示在旁边,但可以指定
{ {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
@ -25,12 +53,22 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
} }
); );
// 为面板生成唯一ID
const panelId = `panel_${Date.now()}_${Math.random()
.toString(36)
.substr(2, 9)}`;
(panel as any).__uniqueId = panelId;
// 设置标签页图标 // 设置标签页图标
panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "图案(方底).png"); panel.iconPath = vscode.Uri.joinPath(
context.extensionUri,
"media",
"icon.png"
);
// 获取页面内图标URI // 获取页面内图标URI
const iconUri = panel.webview.asWebviewUri( const iconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(context.extensionUri, "media", "图案(方底).png") vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
); );
// 设置HTML内容 // 设置HTML内容
@ -38,10 +76,38 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
// 处理消息 // 处理消息
panel.webview.onDidReceiveMessage( panel.webview.onDidReceiveMessage(
(message) => { async (message) => {
const historyManager = ChatHistoryManager.getInstance();
const panelId = (panel as any).__uniqueId;
switch (message.command) { switch (message.command) {
case "sendMessage": case "sendMessage":
handleUserMessage(panel, message.text); // 仅在用户发送消息时,确保面板有任务上下文
// 如果没有,则创建新任务(仅在首次发送消息时)
if (!historyManager.getPanelTask(panelId)) {
const workspacePath =
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (workspacePath) {
try {
const taskMeta = await historyManager.createTask(
workspacePath,
"新对话"
);
historyManager.setPanelTask(
panelId,
taskMeta.taskId,
workspacePath
);
} catch (error) {
console.error("创建任务失败:", error);
}
}
}
// 切换到当前面板的任务上下文
historyManager.switchToPanelTask(panelId);
handleUserMessage(panel, message.text, context.extensionPath);
break; break;
case "readFile": case "readFile":
handleReadFile(panel, message.filePath); handleReadFile(panel, message.filePath);
@ -53,7 +119,12 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
handleRenameFile(panel, message.oldPath, message.newPath); handleRenameFile(panel, message.oldPath, message.newPath);
break; break;
case "replaceInFile": case "replaceInFile":
handleReplaceInFile(panel, message.filePath, message.searchText, message.replaceText); handleReplaceInFile(
panel,
message.filePath,
message.searchText,
message.replaceText
);
break; break;
case "insertCode": case "insertCode":
insertCodeToEditor(message.code); insertCodeToEditor(message.code);
@ -61,9 +132,488 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
case "showInfo": case "showInfo":
vscode.window.showInformationMessage(message.text); vscode.window.showInformationMessage(message.text);
break; break;
case "openWaveformViewer":
// 打开波形查看器
if (message.vcdFilePath) {
VCDViewerPanel.createOrShow(
context.extensionUri,
message.vcdFilePath
);
}
break;
case "getVCDInfo":
// 获取 VCD 文件信息
if (message.vcdFilePath && message.containerId) {
getVCDFileInfo(panel, message.vcdFilePath, message.containerId);
}
break;
case "createNewConversation":
// 创建新会话 - 在当前编辑器组中打开新标签页
showICHelperPanel(context, panel.viewColumn);
break;
case "loadConversationHistory":
// 加载会话历史(支持分页)
loadConversationHistory(
panel,
message.offset || 0,
message.limit || 10
);
break;
case "selectConversation":
// 选择会话
if (message.conversationId) {
selectConversation(
panel,
message.conversationId,
context.extensionPath
);
}
break;
// 新增:处理用户回答
case "submitAnswer":
handleUserAnswer(
message.askId,
message.selected,
message.customInput
);
break;
// 新增:中止对话
case "abortDialog":
abortCurrentDialog();
break;
} }
}, },
undefined, undefined,
context.subscriptions context.subscriptions
); );
// 面板关闭时清理任务映射
panel.onDidDispose(
() => {
const historyManager = ChatHistoryManager.getInstance();
const panelId = (panel as any).__uniqueId;
historyManager.removePanelTask(panelId);
},
undefined,
context.subscriptions
);
}
/**
* 获取 VCD 文件信息
*/
async function getVCDFileInfo(
panel: vscode.WebviewPanel,
vcdFilePath: string,
containerId: string
) {
try {
const fs = require("fs");
const path = require("path");
// 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) {
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: "N/A",
timeRange: "N/A",
fileSize: "N/A",
error: "文件不存在",
},
});
return;
}
// 获取文件大小
const stats = fs.statSync(vcdFilePath);
const fileSizeKB = stats.size / 1024;
const fileSize =
fileSizeKB < 1024
? `${fileSizeKB.toFixed(2)} KB`
: `${(fileSizeKB / 1024).toFixed(2)} MB`;
// 读取 VCD 文件内容
const content = fs.readFileSync(vcdFilePath, "utf-8");
// 解析信号数量
const varMatches = content.match(/\$var/g);
const signalCount = varMatches ? varMatches.length : 0;
// 解析时间范围
let timeRange = "N/A";
const timeMatch = content.match(/#(\d+)/g);
if (timeMatch && timeMatch.length > 0) {
const times = timeMatch.map((t: string) => parseInt(t.substring(1)));
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
timeRange = `${minTime} - ${maxTime}`;
}
// 解析前几个信号的真实数据
const signals = parseVCDSignals(content, 3); // 只解析前3个信号
// 发送信息回前端
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: signalCount.toString(),
timeRange: timeRange,
fileSize: fileSize,
signals: signals, // 添加真实信号数据
},
});
} catch (error) {
console.error("获取 VCD 文件信息失败:", error);
panel.webview.postMessage({
command: "vcdInfo",
containerId: containerId,
vcdInfo: {
signalCount: "N/A",
timeRange: "N/A",
fileSize: "N/A",
error: error instanceof Error ? error.message : "未知错误",
},
});
}
}
/**
* 解析 VCD 文件中的信号数据
*/
function parseVCDSignals(content: string, maxSignals: number = 3) {
const signals: Array<{
name: string;
identifier: string;
width: number;
values: Array<{ time: number; value: string }>;
}> = [];
try {
// 1. 解析信号定义部分
const varRegex = /\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+([^\$]+?)\s+\$end/g;
let match;
const signalDefs: Array<{
name: string;
identifier: string;
width: number;
}> = [];
while (
(match = varRegex.exec(content)) !== null &&
signalDefs.length < maxSignals
) {
const width = parseInt(match[2]);
const identifier = match[3];
const name = match[4].trim();
signalDefs.push({ name, identifier, width });
}
// 2. 找到数据变化部分的起始位置
const dumpvarsIndex = content.indexOf("$dumpvars");
if (dumpvarsIndex === -1) {
return signals;
}
const dataSection = content.substring(dumpvarsIndex);
// 3. 解析每个信号的值变化
for (const signalDef of signalDefs) {
const values: Array<{ time: number; value: string }> = [];
let currentTime = 0;
// 分行处理数据
const lines = dataSection.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
// 解析时间戳
if (trimmedLine.startsWith("#")) {
currentTime = parseInt(trimmedLine.substring(1));
continue;
}
// 解析信号值变化
// 格式1: 单比特信号 "0!" 或 "1!"
// 格式2: 多比特信号 "b1010 !"
if (signalDef.width === 1) {
// 单比特信号
const singleBitMatch = trimmedLine.match(
new RegExp(`^([01xz])${signalDef.identifier}$`)
);
if (singleBitMatch) {
values.push({ time: currentTime, value: singleBitMatch[1] });
}
} else {
// 多比特信号
const multiBitMatch = trimmedLine.match(
new RegExp(`^b([01xz]+)\\s+${signalDef.identifier}$`)
);
if (multiBitMatch) {
values.push({ time: currentTime, value: multiBitMatch[1] });
}
}
// 限制采样点数量,避免数据过多
if (values.length >= 50) {
break;
}
}
signals.push({
name: signalDef.name,
identifier: signalDef.identifier,
width: signalDef.width,
values: values,
});
}
} catch (error) {
console.error("解析 VCD 信号数据失败:", error);
}
return signals;
}
/**
* 加载会话历史(支持分页)
*/
async function loadConversationHistory(
panel: vscode.WebviewPanel,
offset: number = 0,
limit: number = 10
) {
try {
const historyManager = ChatHistoryManager.getInstance();
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspacePath) {
// 没有打开的工作区,返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
return;
}
// 获取会话历史列表(支持分页)
const result = await historyManager.getConversationHistoryList(
workspacePath,
offset,
limit
);
// 发送会话历史到前端
panel.webview.postMessage({
command: "conversationHistory",
items: result.items,
total: result.total,
hasMore: result.hasMore,
});
} catch (error) {
console.error("加载会话历史失败:", error);
// 发生错误时返回空历史
panel.webview.postMessage({
command: "conversationHistory",
items: [],
total: 0,
hasMore: false,
});
}
}
/**
* 选择并加载指定的会话
*/
async function selectConversation(
panel: vscode.WebviewPanel,
taskId: string,
extensionPath: string
) {
try {
const historyManager = ChatHistoryManager.getInstance();
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspacePath) {
vscode.window.showErrorMessage("没有打开的工作区");
return;
}
// 加载任务会话
const taskSession = await historyManager.loadTaskSession(
workspacePath,
taskId
);
if (!taskSession) {
vscode.window.showErrorMessage(
`加载任务 ${taskId} 失败: 任务不存在或数据损坏`
);
return;
}
// 切换到该任务
const switched = await historyManager.switchTask(workspacePath, taskId);
if (!switched) {
vscode.window.showErrorMessage(`切换到任务 ${taskId} 失败`);
return;
}
// 更新面板的任务映射,确保后续对话保存到正确的任务中
const panelId = (panel as any).__uniqueId;
historyManager.setPanelTask(panelId, taskId, workspacePath);
// 清空当前聊天界面
panel.webview.postMessage({
command: "clearChat",
});
// 将会话历史消息转换为 segments 格式并发送到前端显示
const segments: any[] = [];
let i = 0;
while (i < taskSession.messages.length) {
const message = taskSession.messages[i];
if (message.type === MessageType.USER) {
// 用户消息 - 如果有累积的 segments先发送
if (segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: [...segments],
});
segments.length = 0;
}
// 发送用户消息
const textContent = message.contents?.find((c) => c.type === "TEXT");
if (textContent && "text" in textContent) {
panel.webview.postMessage({
command: "addUserMessage",
text: textContent.text,
});
}
i++;
} else if (message.type === MessageType.AI) {
// AI消息 - 如果有 segments直接使用
if (message.segments && message.segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: message.segments,
});
i++;
} else {
// 旧格式:需要转换为 segments
// 收集连续的 AI 消息、工具调用和工具结果
if (message.text) {
segments.push({
type: "text",
content: message.text,
});
}
// 检查是否有工具调用
if (
message.toolExecutionRequests &&
message.toolExecutionRequests.length > 0
) {
for (const toolReq of message.toolExecutionRequests) {
// 查找对应的工具执行结果
let toolResult = "";
if (i + 1 < taskSession.messages.length) {
const nextMsg = taskSession.messages[i + 1];
if (
nextMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
nextMsg.id === toolReq.id
) {
toolResult = nextMsg.text;
i++; // 跳过工具结果消息
}
}
segments.push({
type: "tool",
toolName: toolReq.name,
askId: toolReq.id,
toolResult: toolResult,
});
}
}
i++;
// 继续收集后续的 AI 消息,直到遇到用户消息或有 segments 的 AI 消息
while (i < taskSession.messages.length) {
const nextMsg = taskSession.messages[i];
if (nextMsg.type === MessageType.USER) {
break;
}
if (nextMsg.type === MessageType.AI) {
if (nextMsg.segments && nextMsg.segments.length > 0) {
break;
}
if (nextMsg.text) {
segments.push({
type: "text",
content: nextMsg.text,
});
}
if (
nextMsg.toolExecutionRequests &&
nextMsg.toolExecutionRequests.length > 0
) {
for (const toolReq of nextMsg.toolExecutionRequests) {
let toolResult = "";
if (i + 1 < taskSession.messages.length) {
const resultMsg = taskSession.messages[i + 1];
if (
resultMsg.type === MessageType.TOOL_EXECUTION_RESULT &&
resultMsg.id === toolReq.id
) {
toolResult = resultMsg.text;
i++; // 跳过工具结果消息
}
}
segments.push({
type: "tool",
toolName: toolReq.name,
askId: toolReq.id,
toolResult: toolResult,
});
}
}
i++;
} else if (nextMsg.type === MessageType.TOOL_EXECUTION_RESULT) {
// 独立的工具结果(没有被上面处理的)
i++;
} else {
i++;
}
}
}
} else {
i++;
}
}
// 发送剩余的 segments
if (segments.length > 0) {
panel.webview.postMessage({
command: "receiveSegments",
segments: segments,
});
}
vscode.window.showInformationMessage(
`已加载会话: ${taskSession.meta.taskName}`
);
} catch (error) {
console.error("选择会话失败:", error);
vscode.window.showErrorMessage(`加载会话失败: ${error}`);
}
} }

View File

@ -0,0 +1,352 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
/**
* VCD 波形查看器面板
*/
export class VCDViewerPanel {
public static currentPanel: VCDViewerPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri;
private _disposables: vscode.Disposable[] = [];
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._extensionUri = extensionUri;
// 设置初始 HTML 内容
this._panel.webview.html = this._getLoadingHtml();
// 监听面板关闭事件
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
// 监听来自 webview 的消息
this._panel.webview.onDidReceiveMessage(
(message) => {
switch (message.command) {
case "loadVCD":
if (message.filePath) {
this.loadVCDFile(message.filePath);
}
break;
}
},
null,
this._disposables
);
}
/**
* 创建或显示 VCD 查看器面板
*/
public static createOrShow(extensionUri: vscode.Uri, vcdFilePath?: string) {
const column = vscode.ViewColumn.One;
// 如果已经有面板打开,则显示它
if (VCDViewerPanel.currentPanel) {
VCDViewerPanel.currentPanel._panel.reveal(column);
if (vcdFilePath) {
VCDViewerPanel.currentPanel.loadVCDFile(vcdFilePath);
}
return;
}
// 创建新面板
const panel = vscode.window.createWebviewPanel(
"vcdViewer",
"VCD 波形查看器",
column,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [extensionUri],
}
);
VCDViewerPanel.currentPanel = new VCDViewerPanel(panel, extensionUri);
// 如果提供了 VCD 文件路径,加载它
if (vcdFilePath) {
VCDViewerPanel.currentPanel.loadVCDFile(vcdFilePath);
}
}
/**
* 加载 VCD 文件
*/
public loadVCDFile(vcdFilePath: string) {
try {
// 检查文件是否存在
if (!fs.existsSync(vcdFilePath)) {
vscode.window.showErrorMessage(`VCD 文件不存在: ${vcdFilePath}`);
return;
}
// 更新面板标题
const fileName = path.basename(vcdFilePath);
this._panel.title = `VCD 波形查看器 - ${fileName}`;
// 设置 HTML 内容
this._panel.webview.html = this._getWebviewContent(vcdFilePath);
} catch (error) {
vscode.window.showErrorMessage(
`加载 VCD 文件失败: ${error instanceof Error ? error.message : "未知错误"}`
);
}
}
/**
* 清理资源
*/
public dispose() {
VCDViewerPanel.currentPanel = undefined;
this._panel.dispose();
while (this._disposables.length) {
const disposable = this._disposables.pop();
if (disposable) {
disposable.dispose();
}
}
}
/**
* 获取加载中的 HTML
*/
private _getLoadingHtml(): string {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VCD 波形查看器</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
background-color: var(--vscode-editor-background);
}
.loading {
text-align: center;
}
.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: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="loading">
<div class="spinner"></div>
<p>正在加载 VCD 波形查看器...</p>
</div>
</body>
</html>`;
}
/**
* 获取 Webview 的 HTML 内容
*/
private _getWebviewContent(vcdFilePath: string): string {
// 获取资源 URI
const vcdromJsUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcdrom.js")
);
const vcdWasmUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "vcd.wasm")
);
const fontRegularUri = this._panel.webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, "media", "vcdrom", "IosevkaDrom-Regular.woff2")
);
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">
<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>
<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);
overflow: hidden;
}
#waveform-container {
width: 100vw;
height: 100vh;
overflow: auto;
}
#waveform1 {
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;
color: var(--vscode-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder);
border-radius: 4px;
margin: 20px;
}
</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>
</div>
<script>
(async function() {
try {
// 设置 WASM 文件路径
window.wasmBinaryFile = '${vcdWasmUri}';
// 解码 base64 VCD 内容
const vcdBase64 = '${vcdBase64}';
const vcdContent = atob(vcdBase64);
// 隐藏加载提示
document.querySelector('.loading').style.display = 'none';
// 创建一个函数来提供 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);
} else {
throw new Error('VCDrom 未正确加载');
}
} 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>';
}
})();
</script>
</body>
</html>`;
}
}

154
src/services/apiClient.ts Normal file
View File

@ -0,0 +1,154 @@
/**
* API 客户端
* 封装与后端的 HTTP 通信
*/
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';
/**
* HTTP 请求选项
*/
interface RequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: unknown;
timeout?: number;
}
/**
* 发送 HTTP 请求
*/
async function request<T>(path: string, options: RequestOptions): Promise<T> {
const url = new URL(getApiUrl(path));
const { timeout } = getConfig();
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const requestOptions: http.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: options.method,
headers: {
'Content-Type': 'application/json',
...options.headers
},
timeout: options.timeout || timeout
};
return new Promise((resolve, reject) => {
const req = httpModule.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
resolve(json as T);
} else {
reject(new Error(json.error || json.message || `HTTP ${res.statusCode}`));
}
} catch (e) {
reject(new Error(`解析响应失败: ${data}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('请求超时'));
});
if (options.body) {
req.write(JSON.stringify(options.body));
}
req.end();
});
}
/**
* 提交工具执行结果
* POST /api/tool/result
*/
export async function submitToolResult(result: ToolCallResult): Promise<ToolResultResponse> {
console.log(`[API] 提交工具结果: callId=${result.id}`);
return request<ToolResultResponse>('/api/tool/result', {
method: 'POST',
body: result
});
}
/**
* 提交用户回答
* POST /api/task/answer
*/
export async function submitAnswer(answer: AnswerRequest): Promise<AnswerResponse> {
console.log(`[API] 提交用户回答: askId=${answer.askId}`);
return request<AnswerResponse>('/api/task/answer', {
method: 'POST',
body: answer
});
}
/**
* 健康检查
* GET /api/dialog/health
*/
export async function healthCheck(): Promise<{ status: string }> {
return request<{ status: string }>('/api/dialog/health', {
method: 'GET',
timeout: 5000
});
}
/**
* 创建成功的工具结果
*/
export function createSuccessResult(id: number, text: string): ToolCallResult {
return {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text }],
isError: false
}
};
}
/**
* 创建业务错误的工具结果(如编译失败)
*/
export function createBusinessErrorResult(id: number, errorMessage: string): ToolCallResult {
return {
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: errorMessage }],
isError: true
}
};
}
/**
* 创建系统错误的工具结果
*/
export function createSystemErrorResult(id: number, code: number, message: string): ToolCallResult {
return {
jsonrpc: '2.0',
id,
error: { code, message }
};
}

View File

@ -0,0 +1,337 @@
/**
* 对话服务
* 整合 SSE 通信、工具执行、用户交互
*/
import * as vscode from 'vscode';
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';
/**
* 消息段落类型
*/
export interface MessageSegment {
type: 'text' | 'tool' | 'question';
content?: string;
toolName?: string;
toolStatus?: 'running' | 'success' | 'error';
toolResult?: string;
askId?: string;
question?: string;
options?: string[];
}
/**
* 对话回调接口
*/
export interface DialogCallbacks {
/** 收到文本(可能多次调用,流式) */
onText?: (text: string, isStreaming: boolean) => void;
/** 工具开始执行 */
onToolStart?: (toolName: string) => void;
/** 工具执行完成 */
onToolComplete?: (toolName: string, result: string) => void;
/** 工具执行错误 */
onToolError?: (toolName: string, error: string) => void;
/** 显示问题ask_user */
onQuestion?: (askId: string, question: string, options: string[]) => void;
/** 实时更新段落(流式过程中) */
onSegmentUpdate?: (segments: MessageSegment[]) => void;
/** 对话完成,返回所有段落 */
onComplete?: (segments: MessageSegment[]) => void;
/** 错误 */
onError?: (message: string) => void;
/** 通知消息 */
onNotification?: (message: string) => void;
}
/**
* 对话会话
*/
export class DialogSession {
private taskId: string;
private sseController: SSEController | null = null;
private toolContext: ToolExecutorContext;
private accumulatedText = '';
private isActive = false;
private segments: MessageSegment[] = [];
private currentTextSegment: MessageSegment | null = null;
constructor(extensionPath: string) {
this.taskId = generateTaskId();
this.toolContext = createToolExecutorContext(extensionPath);
}
/**
* 添加文本到当前文本段落
*/
private appendText(text: string): void {
if (!this.currentTextSegment) {
this.currentTextSegment = { type: 'text', content: '' };
this.segments.push(this.currentTextSegment);
}
this.currentTextSegment.content = (this.currentTextSegment.content || '') + text;
}
/**
* 结束当前文本段落
*/
private finalizeTextSegment(): void {
this.currentTextSegment = null;
}
/**
* 添加工具段落
*/
private addToolSegment(toolName: string, status: 'running' | 'success' | 'error', result?: string): MessageSegment {
this.finalizeTextSegment();
const segment: MessageSegment = {
type: 'tool',
toolName,
toolStatus: status,
toolResult: result
};
this.segments.push(segment);
return segment;
}
/**
* 更新工具段落状态
*/
private updateToolSegment(toolName: string, status: 'success' | 'error', result?: string): void {
// 找到最后一个匹配的工具段落
for (let i = this.segments.length - 1; i >= 0; i--) {
const seg = this.segments[i];
if (seg.type === 'tool' && seg.toolName === toolName && seg.toolStatus === 'running') {
seg.toolStatus = status;
seg.toolResult = result;
break;
}
}
}
/**
* 获取任务ID
*/
getTaskId(): string {
return this.taskId;
}
/**
* 是否活跃
*/
get active(): boolean {
return this.isActive;
}
/**
* 发送消息并开始流式对话
*/
async sendMessage(
message: string,
callbacks: DialogCallbacks
): Promise<void> {
if (this.isActive) {
callbacks.onError?.('当前有对话正在进行中');
return;
}
this.isActive = true;
this.accumulatedText = '';
this.segments = [];
this.currentTextSegment = null;
const config = getConfig();
const request: DialogRequest = {
taskId: this.taskId,
message,
userId: config.userId,
toolMode: 'AGENT'
};
const sseCallbacks: SSECallbacks = {
onTextDelta: (data) => {
this.accumulatedText += data.text;
this.appendText(data.text);
console.log('[DialogSession] onTextDelta, 累积文本长度:', this.accumulatedText.length);
callbacks.onText?.(this.accumulatedText, true);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
},
onToolCall: async (data: ToolCallRequest) => {
const toolName = data.params.name;
console.log('[DialogSession] onToolCall:', toolName);
// 检查是否已经有相同的工具段落(可能由 onToolStart 添加)
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
if (lastToolSegment && lastToolSegment.toolName === toolName && lastToolSegment.toolStatus === 'running') {
console.log('[DialogSession] onToolCall: 跳过重复的工具段落:', toolName);
} else {
this.addToolSegment(toolName, 'running');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
// 注意:不在这里调用 callbacks.onToolStart避免与 onToolStart 事件重复
try {
await executeToolCall(data, this.toolContext);
this.updateToolSegment(toolName, 'success', '执行完成');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
// 也不调用 callbacks.onToolComplete避免重复
} catch (error) {
const errorMsg = error instanceof Error ? error.message : '未知错误';
this.updateToolSegment(toolName, 'error', errorMsg);
callbacks.onToolError?.(toolName, errorMsg);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
},
onToolStart: (data) => {
console.log('[DialogSession] onToolStart:', data.tool_name);
// 检查是否已经有相同的工具段落(可能由 onToolCall 添加)
const lastToolSegment = this.segments.filter(s => s.type === 'tool').pop();
if (lastToolSegment && lastToolSegment.toolName === data.tool_name && lastToolSegment.toolStatus === 'running') {
console.log('[DialogSession] 跳过重复的工具段落:', data.tool_name);
} else {
this.addToolSegment(data.tool_name, 'running');
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
}
console.log('[DialogSession] segments 数量:', this.segments.length);
callbacks.onToolStart?.(data.tool_name);
},
onToolComplete: (data) => {
this.updateToolSegment(data.tool_name, 'success', data.result);
callbacks.onToolComplete?.(data.tool_name, data.result);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
},
onToolError: (data) => {
this.updateToolSegment(data.tool_name, 'error', data.error);
callbacks.onToolError?.(data.tool_name, data.error);
// 实时发送段落更新
callbacks.onSegmentUpdate?.(this.segments);
},
onAskUser: async (data: AskUserEvent) => {
this.finalizeTextSegment();
this.segments.push({
type: 'question',
askId: data.askId,
question: data.question,
options: data.options
});
// 实时发送段落更新(包含问题)
callbacks.onSegmentUpdate?.(this.segments);
// 同时调用 onQuestion 用于更新状态栏等
callbacks.onQuestion?.(data.askId, data.question, data.options);
try {
await userInteractionManager.handleAskUser(data, this.taskId);
} catch (error) {
console.error('[DialogSession] 处理用户问题失败:', error);
}
},
onComplete: (data) => {
this.isActive = false;
this.finalizeTextSegment();
// 发送所有段落
callbacks.onComplete?.(this.segments);
},
onError: (data) => {
this.isActive = false;
callbacks.onError?.(data.message);
},
onWarning: (data) => {
callbacks.onNotification?.(`⚠️ ${data.message}`);
},
onNotification: (data) => {
callbacks.onNotification?.(data.message);
},
onOpen: () => {
console.log('[DialogSession] SSE 连接已建立');
},
onClose: () => {
console.log('[DialogSession] SSE 连接已关闭');
this.isActive = false;
}
};
try {
this.sseController = await startStreamDialog(request, sseCallbacks);
} catch (error) {
this.isActive = false;
const errorMsg = error instanceof Error ? error.message : '连接失败';
callbacks.onError?.(errorMsg);
throw error;
}
}
/**
* 中止当前对话
*/
abort(): void {
if (this.sseController) {
this.sseController.abort();
this.sseController = null;
}
this.isActive = false;
userInteractionManager.cancelAll();
}
/**
* 提交用户回答
*/
async submitAnswer(
askId: string,
selected?: string[],
customInput?: string
): Promise<void> {
await userInteractionManager.receiveAnswer(askId, selected, customInput);
}
}
/**
* 全局对话会话管理
*/
class DialogManager {
private currentSession: DialogSession | null = null;
/**
* 创建新会话
*/
createSession(extensionPath: string): DialogSession {
// 如果有活跃会话,先中止
if (this.currentSession?.active) {
this.currentSession.abort();
}
this.currentSession = new DialogSession(extensionPath);
return this.currentSession;
}
/**
* 获取当前会话
*/
getCurrentSession(): DialogSession | null {
return this.currentSession;
}
/**
* 中止当前会话
*/
abortCurrentSession(): void {
this.currentSession?.abort();
}
}
export const dialogManager = new DialogManager();

View File

@ -0,0 +1,437 @@
import * as vscode from "vscode";
import * as http from "http";
import * as path from "path";
import * as fs from "fs";
/**
* IC Coder Authentication Provider
* 集成到 VSCode 账户系统
*/
export class ICCoderAuthenticationProvider
implements vscode.AuthenticationProvider
{
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;
private _onDidChangeSessions =
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
public readonly onDidChangeSessions = this._onDidChangeSessions.event;
private _sessions: vscode.AuthenticationSession[] = [];
constructor(private readonly context: vscode.ExtensionContext) {
// 从存储中恢复会话
this.loadSessions();
}
/**
* 从存储中加载会话
*/
private async loadSessions(): Promise<void> {
const storedSessions = this.context.globalState.get<
vscode.AuthenticationSession[]
>("icCoderSessions", []);
this._sessions = storedSessions;
}
/**
* 保存会话到存储
*/
private async saveSessions(): Promise<void> {
await this.context.globalState.update("icCoderSessions", this._sessions);
}
/**
* 获取会话列表
*/
async getSessions(
scopes?: readonly string[]
): Promise<vscode.AuthenticationSession[]> {
return [...this._sessions];
}
/**
* 创建会话(登录)
*/
async createSession(
scopes: readonly string[]
): Promise<vscode.AuthenticationSession> {
try {
const token = await this.login();
// 创建会话
const session: vscode.AuthenticationSession = {
id: this.generateSessionId(),
accessToken: token,
account: {
id: "iccoder-user",
label: "IC Coder 用户",
},
scopes: [...scopes],
};
this._sessions.push(session);
await this.saveSessions();
// 触发会话变化事件
this._onDidChangeSessions.fire({
added: [session],
removed: [],
changed: [],
});
vscode.window.showInformationMessage("登录成功!窗口将自动刷新...");
// 延迟 1 秒后重新加载窗口,让用户看到成功消息
setTimeout(() => {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}, 1000);
return session;
} catch (error) {
vscode.window.showErrorMessage(
`登录失败: ${error instanceof Error ? error.message : String(error)}`
);
throw error;
}
}
/**
* 删除会话(登出)
*/
async removeSession(sessionId: string): Promise<void> {
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
if (sessionIndex > -1) {
const session = this._sessions[sessionIndex];
this._sessions.splice(sessionIndex, 1);
await this.saveSessions();
// 触发会话变化事件
this._onDidChangeSessions.fire({
added: [],
removed: [session],
changed: [],
});
vscode.window.showInformationMessage("已退出登录!窗口将自动刷新...");
// 延迟 1 秒后重新加载窗口,让用户看到成功消息
setTimeout(() => {
vscode.commands.executeCommand("workbench.action.reloadWindow");
}, 1000);
}
}
/**
* 生成会话 ID
*/
private generateSessionId(): string {
return `iccoder-${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
/**
* 登录逻辑(打开浏览器并等待回调)
*/
private async login(): Promise<string> {
// 如果已有服务器在运行,先关闭
if (ICCoderAuthenticationProvider.loginServer) {
ICCoderAuthenticationProvider.loginServer.close();
ICCoderAuthenticationProvider.loginServer = null;
}
// 创建本地服务器监听回调
const { server, port } = await this.createCallbackServer();
ICCoderAuthenticationProvider.loginServer = server;
ICCoderAuthenticationProvider.currentPort = port;
// 构建登录 URL
const callbackUrl = `http://localhost:${port}/callback`;
const loginUrl = `${
ICCoderAuthenticationProvider.LOGIN_URL
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
console.log("🔐 登录服务器已启动,监听端口:", port);
console.log("🌐 登录 URL:", loginUrl);
// 打开浏览器登录
await vscode.env.openExternal(vscode.Uri.parse(loginUrl));
vscode.window.showInformationMessage(
"请在浏览器中完成登录,登录成功后将自动返回..."
);
// 等待 token通过 Promise
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (ICCoderAuthenticationProvider.loginServer) {
ICCoderAuthenticationProvider.loginServer.close();
ICCoderAuthenticationProvider.loginServer = null;
reject(new Error("登录超时"));
}
}, 5 * 60 * 1000);
// 将 resolve 和 reject 保存到服务器上下文
(server as any)._loginResolve = resolve;
(server as any)._loginReject = reject;
(server as any)._loginTimeout = timeout;
});
}
/**
* 创建本地回调服务器
*/
private createCallbackServer(): Promise<{
server: http.Server;
port: number;
}> {
return new Promise((resolve, reject) => {
// 读取 icon.png 并转换为 Base64
const iconPath = path.join(
this.context.extensionPath,
"media",
"icon.png"
);
let iconBase64 = "";
try {
const iconBuffer = fs.readFileSync(iconPath);
iconBase64 = `data:image/png;base64,${iconBuffer.toString("base64")}`;
} catch (error) {
console.warn("无法读取 icon.png:", error);
}
const server = http.createServer(async (req, res) => {
try {
console.log("📥 收到回调请求:", req.url);
const url = new URL(
req.url!,
`http://localhost:${ICCoderAuthenticationProvider.currentPort}`
);
console.log("📍 路径:", url.pathname);
console.log("📋 所有参数:", Object.fromEntries(url.searchParams));
if (url.pathname === "/callback") {
const token = url.searchParams.get("token");
console.log("🔑 Token:", token ? "已获取" : "未找到");
if (token) {
// 返回成功页面
res.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
});
res.end(this.getSuccessPage(iconBase64));
// 关闭服务器
server.close();
ICCoderAuthenticationProvider.loginServer = null;
// 清除超时
if ((server as any)._loginTimeout) {
clearTimeout((server as any)._loginTimeout);
}
// 返回 token
if ((server as any)._loginResolve) {
(server as any)._loginResolve(token);
}
} else {
res.writeHead(400, {
"Content-Type": "text/html; charset=utf-8",
});
res.end(`
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>登录失败</title></head>
<body><h1>❌ 登录失败</h1><p>未获取到有效的 Token</p></body>
</html>
`);
if ((server as any)._loginReject) {
(server as any)._loginReject(new Error("未获取到有效的 Token"));
}
}
} else {
res.writeHead(404);
res.end("Not Found");
}
} catch (error) {
res.writeHead(500);
res.end("Internal Server Error");
if ((server as any)._loginReject) {
(server as any)._loginReject(error);
}
}
});
// 监听端口(使用 0 表示自动分配可用端口)
server.listen(0, () => {
const address = server.address();
const port =
typeof address === "object" && address ? address.port : 3000;
resolve({ server, port });
});
// 处理错误
server.on("error", (error: NodeJS.ErrnoException) => {
reject(error);
});
});
}
/**
* 获取登录成功页面 HTML
*/
private getSuccessPage(iconBase64: string): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录成功 - IC Coder</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #dbeafe 0%, #93c5fd 100%);
overflow: hidden;
position: relative;
}
.bg-circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 20s infinite ease-in-out;
}
.bg-circle:nth-child(1) { width: 300px; height: 300px; top: -150px; left: -150px; }
.bg-circle:nth-child(2) { width: 200px; height: 200px; bottom: -100px; right: -100px; animation-delay: 5s; }
.bg-circle:nth-child(3) { width: 150px; height: 150px; top: 50%; right: 10%; animation-delay: 10s; }
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -30px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
}
.container {
position: relative;
z-index: 10;
text-align: center;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 60px 50px;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
animation: slideUp 0.6s ease-out;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.success-icon {
width: 80px;
height: 80px;
margin: 0 auto 30px;
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: scaleIn 0.5s ease-out 0.2s both;
}
@keyframes scaleIn {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.checkmark {
width: 40px;
height: 40px;
border: 4px solid white;
border-radius: 50%;
position: relative;
}
.checkmark::after {
content: '';
position: absolute;
left: 8px;
top: 3px;
width: 12px;
height: 20px;
border: solid white;
border-width: 0 4px 4px 0;
transform: rotate(45deg);
}
h1 {
color: #2d3748;
font-size: 32px;
font-weight: 700;
margin-bottom: 16px;
animation: fadeIn 0.6s ease-out 0.3s both;
}
p {
color: #718096;
font-size: 18px;
line-height: 1.6;
margin-bottom: 30px;
animation: fadeIn 0.6s ease-out 0.4s both;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
animation: fadeIn 0.6s ease-out 0.5s both;
}
.brand-logo {
width: 32px;
height: 32px;
border-radius: 6px;
overflow: hidden;
}
.brand-logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.brand-text {
color: #4a5568;
font-size: 16px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="bg-circle"></div>
<div class="bg-circle"></div>
<div class="bg-circle"></div>
<div class="container">
<div class="success-icon">
<div class="checkmark"></div>
</div>
<h1>登录成功!</h1>
<p>您已成功登录 IC Coder<br>现在可以返回 VSCode 继续使用</p>
<div class="brand">
<div class="brand-logo">
<img src="${iconBase64}" alt="IC Coder" />
</div>
<span class="brand-text">IC Coder</span>
</div>
</div>
</body>
</html>
`;
}
}

296
src/services/sseHandler.ts Normal file
View File

@ -0,0 +1,296 @@
/**
* SSE 事件处理器
* 处理与后端的流式通信
* 使用 eventsource-parser + Node.js 原生 http 模块
*/
import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';
import { createParser, type EventSourceParser } from 'eventsource-parser';
import { getApiUrl, getConfig } from '../config/settings';
import type {
DialogRequest,
SSEEventType,
TextDeltaEvent,
ToolCallRequest,
AskUserEvent,
CompleteEvent,
ErrorEvent,
ToolStartEvent,
ToolCompleteEvent,
ToolErrorEvent,
WarningEvent,
NotificationEvent,
DepthUpdateEvent
} from '../types/api';
/**
* SSE 事件回调接口
*/
export interface SSECallbacks {
/** 收到文本增量 */
onTextDelta?: (data: TextDeltaEvent) => void;
/** 收到工具调用请求 */
onToolCall?: (data: ToolCallRequest) => void;
/** 工具开始执行 */
onToolStart?: (data: ToolStartEvent) => void;
/** 工具执行完成 */
onToolComplete?: (data: ToolCompleteEvent) => void;
/** 工具执行错误 */
onToolError?: (data: ToolErrorEvent) => void;
/** 收到用户提问 */
onAskUser?: (data: AskUserEvent) => void;
/** 对话完成 */
onComplete?: (data: CompleteEvent) => void;
/** 错误 */
onError?: (data: ErrorEvent) => void;
/** 警告 */
onWarning?: (data: WarningEvent) => void;
/** 通知 */
onNotification?: (data: NotificationEvent) => void;
/** 深度更新 */
onDepthUpdate?: (data: DepthUpdateEvent) => void;
/** 连接打开 */
onOpen?: () => void;
/** 连接关闭 */
onClose?: () => void;
}
/**
* SSE 会话控制器
*/
export class SSEController {
private request: http.ClientRequest | null = null;
private isConnected = false;
private isAborted = false;
/**
* 是否已连接
*/
get connected(): boolean {
return this.isConnected;
}
/**
* 设置请求对象
*/
setRequest(req: http.ClientRequest): void {
this.request = req;
}
/**
* 设置连接状态
*/
setConnected(connected: boolean): void {
this.isConnected = connected;
}
/**
* 是否已中止
*/
get aborted(): boolean {
return this.isAborted;
}
/**
* 中止当前连接
*/
abort(): void {
if (this.request && !this.isAborted) {
this.isAborted = true;
this.request.destroy();
this.request = null;
this.isConnected = false;
}
}
}
/**
* 发起流式对话
* @param request 对话请求
* @param callbacks 事件回调
* @returns SSE 控制器(用于中止连接)
*/
export async function startStreamDialog(
request: DialogRequest,
callbacks: SSECallbacks
): Promise<SSEController> {
const controller = new SSEController();
const urlString = getApiUrl('/api/dialog/stream');
const url = new URL(urlString);
const isHttps = url.protocol === 'https:';
const httpModule = isHttps ? https : http;
const body = JSON.stringify(request);
console.log(`[SSE] 开始流式对话: taskId=${request.taskId}, url=${urlString}`);
return new Promise((resolve, reject) => {
const options: http.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache',
'Content-Length': Buffer.byteLength(body)
}
};
const req = httpModule.request(options, (res) => {
// 检查响应状态
if (res.statusCode !== 200) {
let errorBody = '';
res.on('data', chunk => errorBody += chunk);
res.on('end', () => {
const error = new Error(`SSE 连接失败: ${res.statusCode} ${errorBody}`);
callbacks.onError?.({ message: error.message });
reject(error);
});
return;
}
// 连接成功
console.log('[SSE] 连接已建立');
controller.setConnected(true);
callbacks.onOpen?.();
resolve(controller);
// 创建 SSE 解析器
const parser = createParser({
onEvent: (event) => {
const eventType = event.event as SSEEventType;
const eventData = event.data;
if (!eventData) {
console.log(`[SSE] 收到空事件: ${eventType}`);
return;
}
try {
const data = JSON.parse(eventData);
console.log(`[SSE] 收到事件: ${eventType}`, data);
// 分发事件到对应回调
dispatchEvent(eventType, data, callbacks);
} catch (e) {
console.error(`[SSE] 解析事件数据失败: ${eventData}`, e);
}
}
});
// 设置编码
res.setEncoding('utf8');
// 处理数据流
res.on('data', (chunk: string) => {
if (!controller.aborted) {
console.log('[SSE] 收到原始数据块:', chunk.substring(0, 200));
parser.feed(chunk);
}
});
// 处理连接关闭
res.on('end', () => {
console.log('[SSE] 连接已关闭');
controller.setConnected(false);
callbacks.onClose?.();
});
// 处理错误
res.on('error', (err) => {
if (!controller.aborted) {
console.error('[SSE] 响应错误:', err);
controller.setConnected(false);
callbacks.onError?.({ message: err.message });
}
});
});
// 保存请求引用用于中止
controller.setRequest(req);
// 处理请求错误
req.on('error', (err) => {
if (!controller.aborted) {
console.error('[SSE] 请求错误:', err);
controller.setConnected(false);
callbacks.onError?.({ message: err.message });
reject(err);
}
});
// 处理超时
const { timeout } = getConfig();
req.setTimeout(timeout, () => {
if (!controller.aborted) {
console.error('[SSE] 请求超时');
controller.abort();
const error = new Error('请求超时');
callbacks.onError?.({ message: error.message });
reject(error);
}
});
// 发送请求体
req.write(body);
req.end();
});
}
/**
* 分发 SSE 事件到对应回调
*/
function dispatchEvent(
eventType: SSEEventType,
data: unknown,
callbacks: SSECallbacks
): void {
switch (eventType) {
case 'text_delta':
callbacks.onTextDelta?.(data as TextDeltaEvent);
break;
case 'tool_call':
callbacks.onToolCall?.(data as ToolCallRequest);
break;
case 'tool_start':
callbacks.onToolStart?.(data as ToolStartEvent);
break;
case 'tool_complete':
callbacks.onToolComplete?.(data as ToolCompleteEvent);
break;
case 'tool_error':
callbacks.onToolError?.(data as ToolErrorEvent);
break;
case 'ask_user':
callbacks.onAskUser?.(data as AskUserEvent);
break;
case 'complete':
callbacks.onComplete?.(data as CompleteEvent);
break;
case 'error':
callbacks.onError?.(data as ErrorEvent);
break;
case 'warning':
callbacks.onWarning?.(data as WarningEvent);
break;
case 'notification':
callbacks.onNotification?.(data as NotificationEvent);
break;
case 'depth_update':
callbacks.onDepthUpdate?.(data as DepthUpdateEvent);
break;
default:
console.log(`[SSE] 未知事件类型: ${eventType}`, data);
}
}
/**
* 生成任务ID
*/
export function generateTaskId(): string {
return `task-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
}

View File

@ -0,0 +1,272 @@
/**
* 工具执行器
* 接收后端的 tool_call 事件,执行本地工具,返回结果
*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { readFileContent, readDirectory } from '../utils/readFiles';
import { createOrOverwriteFile } from '../utils/createFiles';
import { generateVCD, checkIverilogAvailable } from '../utils/iverilogRunner';
import {
submitToolResult,
createSuccessResult,
createBusinessErrorResult,
createSystemErrorResult
} from './apiClient';
import type {
ToolCallRequest,
ToolName,
FileReadArgs,
FileWriteArgs,
FileListArgs,
SyntaxCheckArgs,
SimulationArgs,
WaveformSummaryArgs
} from '../types/api';
/**
* 工具执行器上下文
*/
export interface ToolExecutorContext {
/** 扩展路径(用于 iverilog */
extensionPath: string;
/** 工作区路径 */
workspacePath: string;
}
/**
* 执行工具调用
* @param request 工具调用请求
* @param context 执行上下文
*/
export async function executeToolCall(
request: ToolCallRequest,
context: ToolExecutorContext
): Promise<void> {
const toolName = request.params.name as ToolName;
const args = request.params.arguments;
const callId = request.id;
console.log(`[ToolExecutor] 执行工具: ${toolName}, callId=${callId}`, args);
try {
let resultText: string;
switch (toolName) {
case 'file_read':
resultText = await executeFileRead(args as unknown as FileReadArgs);
break;
case 'file_write':
resultText = await executeFileWrite(args as unknown as FileWriteArgs);
break;
case 'file_list':
resultText = await executeFileList(args as unknown as FileListArgs);
break;
case 'syntax_check':
resultText = await executeSyntaxCheck(args as unknown as SyntaxCheckArgs, context);
break;
case 'simulation':
resultText = await executeSimulation(args as unknown as SimulationArgs, context);
break;
case 'waveform_summary':
resultText = await executeWaveformSummary(args as unknown as WaveformSummaryArgs);
break;
default:
throw new Error(`未知工具: ${toolName}`);
}
// 提交成功结果
const result = createSuccessResult(callId, resultText);
await submitToolResult(result);
console.log(`[ToolExecutor] 工具执行成功: ${toolName}, callId=${callId}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[ToolExecutor] 工具执行失败: ${toolName}, callId=${callId}`, error);
// 提交错误结果
const result = createBusinessErrorResult(callId, errorMessage);
await submitToolResult(result);
}
}
/**
* 执行 file_read 工具
*/
async function executeFileRead(args: FileReadArgs): Promise<string> {
const content = await readFileContent(args.path);
return content;
}
/**
* 执行 file_write 工具
*/
async function executeFileWrite(args: FileWriteArgs): Promise<string> {
await createOrOverwriteFile(args.path, args.content);
return `文件已写入: ${args.path}`;
}
/**
* 执行 file_list 工具
*/
async function executeFileList(args: FileListArgs): Promise<string> {
const dirPath = args.path || '.';
const extensions = args.extension ? [args.extension] : undefined;
const files = await readDirectory(dirPath, extensions);
const fileList = files.map(f => f.path).join('\n');
return fileList || '(目录为空)';
}
/**
* 执行 syntax_check 工具
* 将代码写入临时文件,调用 iverilog 检查语法
*/
async function executeSyntaxCheck(
args: SyntaxCheckArgs,
context: ToolExecutorContext
): Promise<string> {
// 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(context.extensionPath);
if (!iverilogCheck.available) {
throw new Error(`iverilog 不可用: ${iverilogCheck.message}`);
}
// 创建临时文件
const tempDir = os.tmpdir();
const tempFile = path.join(tempDir, `iccoder_syntax_${Date.now()}.v`);
try {
// 写入代码到临时文件
fs.writeFileSync(tempFile, args.code, 'utf-8');
// 调用 iverilog 进行语法检查
const { spawn } = require('child_process');
const iverilogPath = getIverilogPath(context.extensionPath);
return new Promise((resolve, reject) => {
const child = spawn(iverilogPath, ['-t', 'null', tempFile], {
cwd: tempDir,
env: {
...process.env,
IVERILOG_ROOT: path.join(context.extensionPath, 'tools', 'iverilog')
}
});
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) => {
// 清理临时文件
try {
fs.unlinkSync(tempFile);
} catch (e) {
// 忽略清理错误
}
if (code === 0) {
resolve('语法检查通过,无错误。');
} else {
resolve(`语法检查发现错误:\n${stderr || stdout}`);
}
});
child.on('error', (error: Error) => {
try {
fs.unlinkSync(tempFile);
} catch (e) {
// 忽略清理错误
}
reject(error);
});
});
} catch (error) {
// 确保清理临时文件
try {
fs.unlinkSync(tempFile);
} catch (e) {
// 忽略
}
throw error;
}
}
/**
* 执行 simulation 工具
*/
async function executeSimulation(
args: SimulationArgs,
context: ToolExecutorContext
): Promise<string> {
// 获取工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
throw new Error('请先打开一个工作区');
}
const projectPath = workspaceFolders[0].uri.fsPath;
// 调用现有的 generateVCD 函数
const result = await generateVCD(projectPath, context.extensionPath);
if (result.success) {
let message = result.message;
if (result.stdout) {
message += `\n\n仿真输出:\n${result.stdout}`;
}
return message;
} else {
let errorMessage = result.message;
if (result.stderr) {
errorMessage += `\n\n错误输出:\n${result.stderr}`;
}
throw new Error(errorMessage);
}
}
/**
* 执行 waveform_summary 工具
* TODO: 实现 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 || '无'}`;
}
/**
* 获取 iverilog 路径
*/
function getIverilogPath(extensionPath: string): string {
const platform = process.platform;
if (platform === 'win32') {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog.exe');
} else {
return path.join(extensionPath, 'tools', 'iverilog', 'bin', 'iverilog');
}
}
/**
* 创建工具执行器上下文
*/
export function createToolExecutorContext(extensionPath: string): ToolExecutorContext {
const workspaceFolders = vscode.workspace.workspaceFolders;
const workspacePath = workspaceFolders?.[0]?.uri.fsPath || '';
return {
extensionPath,
workspacePath
};
}

View File

@ -0,0 +1,147 @@
/**
* 用户交互处理器
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
*/
import * as vscode from 'vscode';
import { submitAnswer } from './apiClient';
import type { AskUserEvent, AnswerRequest } from '../types/api';
/**
* 待处理的用户问题
*/
interface PendingQuestion {
askId: string;
taskId: string;
question: string;
options: string[];
resolve: (answer: string) => void;
reject: (error: Error) => void;
}
/**
* 用户交互管理器
*/
export class UserInteractionManager {
private pendingQuestions = new Map<string, PendingQuestion>();
private webviewPanel: vscode.WebviewPanel | null = null;
/**
* 设置 WebView 面板(用于发送消息)
*/
setWebviewPanel(panel: vscode.WebviewPanel): void {
this.webviewPanel = panel;
}
/**
* 处理 ask_user 事件
* @param event ask_user 事件数据
* @param taskId 当前任务ID
*/
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
const { askId, question, options } = event;
console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`);
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
// 这里不再单独发送 showQuestion 命令,避免重复显示
// 创建 Promise 等待用户回答
return new Promise((resolve, reject) => {
this.pendingQuestions.set(askId, {
askId,
taskId,
question,
options,
resolve: (answer: string) => {
this.submitUserAnswer(askId, taskId, answer)
.then(() => resolve())
.catch(reject);
},
reject
});
// 设置超时5分钟
setTimeout(() => {
if (this.pendingQuestions.has(askId)) {
this.pendingQuestions.delete(askId);
reject(new Error('用户回答超时'));
}
}, 300000);
});
}
/**
* 处理用户提交的回答(从 WebView 调用)
* @param askId 问题ID
* @param selected 选中的选项
* @param customInput 自定义输入
*/
async receiveAnswer(
askId: string,
selected?: string[],
customInput?: string
): Promise<void> {
const pending = this.pendingQuestions.get(askId);
if (!pending) {
console.warn(`[UserInteraction] 问题不存在或已超时: askId=${askId}`);
return;
}
// 构建答案
const answer = customInput || selected?.join(', ') || '';
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
// 移除待处理问题
this.pendingQuestions.delete(askId);
// 触发 resolve
pending.resolve(answer);
}
/**
* 提交用户回答到后端
*/
private async submitUserAnswer(
askId: string,
taskId: string,
answer: string
): Promise<void> {
const request: AnswerRequest = {
askId,
taskId,
customInput: answer
};
try {
const response = await submitAnswer(request);
if (!response.success) {
throw new Error(response.error || '提交回答失败');
}
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
} catch (error) {
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
throw error;
}
}
/**
* 取消所有待处理的问题
*/
cancelAll(): void {
for (const [askId, pending] of this.pendingQuestions) {
pending.reject(new Error('用户交互已取消'));
}
this.pendingQuestions.clear();
}
/**
* 检查是否有待处理的问题
*/
hasPendingQuestions(): boolean {
return this.pendingQuestions.size > 0;
}
}
// 全局实例
export const userInteractionManager = new UserInteractionManager();

243
src/types/api.ts Normal file
View File

@ -0,0 +1,243 @@
/**
* 后端 API 类型定义
* 对应后端 IC Coder Backend 的接口格式
*/
// ============== 对话请求/响应 ==============
/**
* 对话请求
* POST /api/dialog/stream
*/
export interface DialogRequest {
/** 任务ID用于记忆隔离 */
taskId: string;
/** 用户消息 */
message: string;
/** 用户ID */
userId: string;
/** 工具模式 */
toolMode: 'ASK' | 'AGENT';
}
// ============== 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 事件数据 */
export interface TextDeltaEvent {
text: string;
}
/** tool_start 事件数据 */
export interface ToolStartEvent {
tool_name: string;
tool_input: unknown;
}
/** tool_complete 事件数据 */
export interface ToolCompleteEvent {
tool_name: string;
result: string;
}
/** tool_error 事件数据 */
export interface ToolErrorEvent {
tool_name: string;
error: string;
}
/** ask_user 事件数据 */
export interface AskUserEvent {
askId: string;
question: string;
options: string[];
}
/** complete 事件数据 */
export interface CompleteEvent {
status: string;
finish_reason: string;
}
/** error 事件数据 */
export interface ErrorEvent {
message: string;
}
/** warning 事件数据 */
export interface WarningEvent {
message: string;
}
/** notification 事件数据 */
export interface NotificationEvent {
message: string;
}
/** depth_update 事件数据 */
export interface DepthUpdateEvent {
depth: number;
}
// ============== 工具调用协议 (MCP 格式) ==============
/**
* 工具调用请求MCP格式
* 后端通过 SSE tool_call 事件推送
*/
export interface ToolCallRequest {
/** JSON-RPC版本固定为"2.0" */
jsonrpc: '2.0';
/** 请求ID用于匹配响应 */
id: number;
/** 方法名,固定为"tools/call" */
method: 'tools/call';
/** 调用参数 */
params: {
/** 工具名称 */
name: string;
/** 工具参数 */
arguments: Record<string, unknown>;
};
}
/**
* 工具执行结果MCP格式
* POST /api/tool/result
*/
export interface ToolCallResult {
/** JSON-RPC版本 */
jsonrpc: '2.0';
/** 请求ID与ToolCallRequest.id对应 */
id: number;
/** 执行结果与error互斥 */
result?: ToolResultContent;
/** 错误信息与result互斥 */
error?: ToolResultError;
}
/** 工具执行结果内容 */
export interface ToolResultContent {
/** 内容列表 */
content: ContentItem[];
/** 是否为错误结果(业务错误,如编译失败) */
isError: boolean;
}
/** 内容项 */
export interface ContentItem {
/** 内容类型text, image, resource */
type: string;
/** 文本内容 */
text: string;
}
/** 工具系统错误 */
export interface ToolResultError {
/** 错误码 */
code: number;
/** 错误消息 */
message: string;
}
// ============== 用户回答 ==============
/**
* 用户回答请求
* POST /api/task/answer
*/
export interface AnswerRequest {
/** 问题ID */
askId: string;
/** 任务ID */
taskId: string;
/** 选中的选项列表 */
selected?: string[];
/** 自定义输入内容 */
customInput?: string;
}
/** 用户回答响应 */
export interface AnswerResponse {
success: boolean;
message?: string;
error?: string;
}
// ============== 工具结果响应 ==============
/** 工具结果响应 */
export interface ToolResultResponse {
success: boolean;
message?: string;
error?: string;
}
// ============== 辅助类型 ==============
/** 后端工具名称 */
export type ToolName =
| 'file_read'
| 'file_write'
| 'file_list'
| 'syntax_check'
| 'simulation'
| 'waveform_summary';
/** file_read 工具参数 */
export interface FileReadArgs {
path: string;
}
/** file_write 工具参数 */
export interface FileWriteArgs {
path: string;
content: string;
}
/** file_list 工具参数 */
export interface FileListArgs {
path?: string;
extension?: string;
}
/** syntax_check 工具参数 */
export interface SyntaxCheckArgs {
code: string;
}
/** simulation 工具参数 */
export interface SimulationArgs {
rtlPath: string;
tbPath: string;
duration?: string;
}
/** waveform_summary 工具参数 */
export interface WaveformSummaryArgs {
vcdPath: string;
signals: string;
checkpoints?: string;
}
/** 工具参数联合类型 */
export type ToolArgs =
| FileReadArgs
| FileWriteArgs
| FileListArgs
| SyntaxCheckArgs
| SimulationArgs
| WaveformSummaryArgs;

116
src/types/chatHistory.ts Normal file
View File

@ -0,0 +1,116 @@
/**
* 消息类型枚举(对应 LangChain4j 格式)
*/
export enum MessageType {
SYSTEM = "SYSTEM",
USER = "USER",
AI = "AI",
TOOL_EXECUTION_RESULT = "TOOL_EXECUTION_RESULT"
}
/**
* 工具执行请求
*/
export interface ToolExecutionRequest {
id: string;
name: string;
arguments: string; // JSON字符串
}
/**
* 消息内容用于USER消息
*/
export interface MessageContent {
type: "TEXT";
text: string;
}
/**
* 基础消息接口
*/
export interface BaseMessage {
type: MessageType;
}
/**
* 系统消息
*/
export interface SystemMessage extends BaseMessage {
type: MessageType.SYSTEM;
text: string;
}
/**
* 用户消息
*/
export interface UserMessage extends BaseMessage {
type: MessageType.USER;
contents: MessageContent[];
}
/**
* AI消息
*/
export interface AiMessage extends BaseMessage {
type: MessageType.AI;
text?: string;
toolExecutionRequests?: ToolExecutionRequest[];
thinking?: string;
segments?: any[]; // 保存完整的 segments 信息用于还原显示
}
/**
* 工具执行结果消息
*/
export interface ToolExecutionResultMessage extends BaseMessage {
type: MessageType.TOOL_EXECUTION_RESULT;
id: string;
toolName: string;
text: string; // JSON字符串
}
/**
* 联合消息类型
*/
export type ChatMessage = SystemMessage | UserMessage | AiMessage | ToolExecutionResultMessage;
/**
* 对话轮次元数据
*/
export interface ConversationMeta {
turnId: number;
timestamp: string; // ISO 8601格式
usage?: {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
};
model?: string;
duration?: number; // 响应耗时(秒)
}
/**
* 任务元数据meta.json
*/
export interface TaskMeta {
taskId: string;
taskName: string;
projectPath: string;
createdAt: string; // ISO 8601格式
updatedAt: string; // ISO 8601格式
stats: {
credits: number;
totalTokens: number;
inputTokens: number;
outputTokens: number;
};
}
/**
* 任务会话(包含所有相关数据)
*/
export interface TaskSession {
meta: TaskMeta;
messages: ChatMessage[]; // conversation.json的内容
conversationMeta: ConversationMeta[]; // conversation_meta.jsonl的内容
}

View File

@ -0,0 +1,693 @@
import * as vscode from 'vscode';
import * as path from 'path';
import {
ChatMessage,
TaskMeta,
TaskSession,
ConversationMeta,
MessageType,
UserMessage,
AiMessage,
SystemMessage,
ToolExecutionResultMessage
} from '../types/chatHistory';
/**
* 会话历史管理器
* 按照设计文档实现:~/.iccoder/projects/{项目路径编码}/{taskId}/
*/
export class ChatHistoryManager {
private static instance: ChatHistoryManager;
private baseDir: string; // ~/.iccoder
private currentTaskId: string | null = null;
private currentProjectPath: string | null = null;
// 存储每个面板的任务信息taskId 和 projectPath
private panelTaskMap: Map<string, { taskId: string; projectPath: string }> = new Map();
private constructor() {
// 设置存储路径: ~/.iccoder
const userHome = process.env.USERPROFILE || process.env.HOME || '';
this.baseDir = path.join(userHome, '.iccoder');
this.ensureBaseDir();
}
/**
* 项目路径编码
* 规则:
* - 替换 \ 和 / 为 --
* - 替换 : 为空
* 例如C:\Users\admin\Documents\Project -> C--Users--admin--Documents--Project
*/
private encodeProjectPath(projectPath: string): string {
return projectPath
.replace(/\\/g, '--') // 替换反斜杠为 --
.replace(/\//g, '--') // 替换正斜杠为 --
.replace(/:/g, ''); // 移除冒号
}
/**
* 生成任务ID
* 格式task_{date}_{sequence}
*/
private generateTaskId(): string {
const date = new Date().toISOString().split('T')[0].replace(/-/g, '');
const sequence = Math.random().toString(36).substr(2, 6);
return `task_${date}_${sequence}`;
}
/**
* 获取任务目录路径
*/
private getTaskDir(projectPath: string, taskId: string): string {
const encodedPath = this.encodeProjectPath(projectPath);
return path.join(this.baseDir, 'projects', encodedPath, taskId);
}
/**
* 确保基础目录存在
*/
private async ensureBaseDir(): Promise<void> {
try {
const uri = vscode.Uri.file(this.baseDir);
try {
await vscode.workspace.fs.stat(uri);
} catch {
// 目录不存在,创建它
await vscode.workspace.fs.createDirectory(uri);
console.log(`创建存储目录: ${this.baseDir}`);
}
} catch (error) {
console.error("创建存储目录失败:", error);
vscode.window.showErrorMessage("创建会话历史存储目录失败");
}
}
/**
* 确保任务目录存在
*/
private async ensureTaskDir(taskDir: string): Promise<void> {
try {
const uri = vscode.Uri.file(taskDir);
try {
await vscode.workspace.fs.stat(uri);
} catch {
// 目录不存在,创建它
await vscode.workspace.fs.createDirectory(uri);
console.log(`创建任务目录: ${taskDir}`);
}
} catch (error) {
console.error("创建任务目录失败:", error);
throw error;
}
}
/**
* 获取单例实例
*/
public static getInstance(): ChatHistoryManager {
if (!ChatHistoryManager.instance) {
ChatHistoryManager.instance = new ChatHistoryManager();
}
return ChatHistoryManager.instance;
}
/**
* 为面板设置任务ID
*/
public setPanelTask(panelId: string, taskId: string, projectPath: string): void {
this.panelTaskMap.set(panelId, { taskId, projectPath });
this.currentTaskId = taskId;
this.currentProjectPath = projectPath;
}
/**
* 获取面板的任务ID
*/
public getPanelTask(panelId: string): string | null {
const taskInfo = this.panelTaskMap.get(panelId);
return taskInfo ? taskInfo.taskId : null;
}
/**
* 切换到指定面板的任务上下文
*/
public switchToPanelTask(panelId: string): boolean {
const taskInfo = this.panelTaskMap.get(panelId);
if (taskInfo) {
this.currentTaskId = taskInfo.taskId;
this.currentProjectPath = taskInfo.projectPath;
return true;
}
return false;
}
/**
* 移除面板的任务映射
*/
public removePanelTask(panelId: string): void {
this.panelTaskMap.delete(panelId);
}
/**
* 创建新任务
*/
public async createTask(projectPath: string, taskName: string): Promise<TaskMeta> {
const taskId = this.generateTaskId();
const now = new Date().toISOString();
const meta: TaskMeta = {
taskId,
taskName,
projectPath,
createdAt: now,
updatedAt: now,
stats: {
credits: 0,
totalTokens: 0,
inputTokens: 0,
outputTokens: 0
}
};
this.currentTaskId = taskId;
this.currentProjectPath = projectPath;
// 创建任务目录
const taskDir = this.getTaskDir(projectPath, taskId);
await this.ensureTaskDir(taskDir);
// 保存 meta.json
await this.saveTaskMeta(meta);
// 初始化空的 conversation.json
await this.saveConversation([]);
return meta;
}
/**
* 保存任务元数据
*/
private async saveTaskMeta(meta: TaskMeta): Promise<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
throw new Error("没有当前任务");
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const metaPath = path.join(taskDir, 'meta.json');
try {
const uri = vscode.Uri.file(metaPath);
const content = Buffer.from(JSON.stringify(meta, null, 2), 'utf-8');
await vscode.workspace.fs.writeFile(uri, content);
} catch (error) {
console.error("保存任务元数据失败:", error);
throw error;
}
}
/**
* 加载任务元数据
*/
private async loadTaskMeta(): Promise<TaskMeta | null> {
if (!this.currentTaskId || !this.currentProjectPath) {
return null;
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const metaPath = path.join(taskDir, 'meta.json');
try {
const uri = vscode.Uri.file(metaPath);
const content = await vscode.workspace.fs.readFile(uri);
const data = Buffer.from(content).toString('utf-8');
return JSON.parse(data);
} catch (error) {
// 文件不存在或读取失败
return null;
}
}
/**
* 保存对话历史conversation.json
*/
private async saveConversation(messages: ChatMessage[]): Promise<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
throw new Error("没有当前任务");
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const conversationPath = path.join(taskDir, 'conversation.json');
try {
const uri = vscode.Uri.file(conversationPath);
const content = Buffer.from(JSON.stringify(messages, null, 2), 'utf-8');
await vscode.workspace.fs.writeFile(uri, content);
} catch (error) {
console.error("保存对话历史失败:", error);
throw error;
}
}
/**
* 加载对话历史
*/
private async loadConversation(): Promise<ChatMessage[]> {
if (!this.currentTaskId || !this.currentProjectPath) {
return [];
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const conversationPath = path.join(taskDir, 'conversation.json');
try {
const uri = vscode.Uri.file(conversationPath);
const content = await vscode.workspace.fs.readFile(uri);
const data = Buffer.from(content).toString('utf-8');
return JSON.parse(data);
} catch (error) {
// 文件不存在或读取失败
return [];
}
}
/**
* 追加对话元数据conversation_meta.jsonl
*/
private async appendConversationMeta(meta: ConversationMeta): Promise<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
throw new Error("没有当前任务");
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const metaPath = path.join(taskDir, 'conversation_meta.jsonl');
try {
const uri = vscode.Uri.file(metaPath);
const line = JSON.stringify(meta) + '\n';
// 读取现有内容
let existingContent = '';
try {
const content = await vscode.workspace.fs.readFile(uri);
existingContent = Buffer.from(content).toString('utf-8');
} catch {
// 文件不存在,忽略错误
}
// 追加新内容
const newContent = existingContent + line;
await vscode.workspace.fs.writeFile(uri, Buffer.from(newContent, 'utf-8'));
} catch (error) {
console.error("追加对话元数据失败:", error);
throw error;
}
}
/**
* 确保有当前任务,如果没有则抛出错误
*/
private async ensureCurrentTask(): Promise<void> {
if (!this.currentTaskId || !this.currentProjectPath) {
throw new Error("没有当前任务上下文,请确保已正确初始化面板任务");
}
}
/**
* 添加用户消息
*/
public async addUserMessage(text: string): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const userMessage: UserMessage = {
type: MessageType.USER,
contents: [{ type: "TEXT", text }]
};
messages.push(userMessage);
await this.saveConversation(messages);
// 更新任务元数据
await this.updateTaskTimestamp();
}
/**
* 添加AI消息
*/
public async addAiMessage(text: string, toolRequests?: any[], segments?: any[]): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const aiMessage: AiMessage = {
type: MessageType.AI,
text,
toolExecutionRequests: toolRequests,
segments // 保存完整的 segments 信息
};
messages.push(aiMessage);
await this.saveConversation(messages);
// 更新任务元数据
await this.updateTaskTimestamp();
}
/**
* 添加系统消息
*/
public async addSystemMessage(text: string): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const systemMessage: SystemMessage = {
type: MessageType.SYSTEM,
text
};
messages.push(systemMessage);
await this.saveConversation(messages);
}
/**
* 添加工具执行结果消息
*/
public async addToolExecutionResult(id: string, toolName: string, result: string): Promise<void> {
await this.ensureCurrentTask();
const messages = await this.loadConversation();
const toolResultMessage: ToolExecutionResultMessage = {
type: MessageType.TOOL_EXECUTION_RESULT,
id,
toolName,
text: result
};
messages.push(toolResultMessage);
await this.saveConversation(messages);
}
/**
* 记录对话轮次元数据
*/
public async recordTurnMeta(
turnId: number,
usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number },
model?: string,
duration?: number
): Promise<void> {
const meta: ConversationMeta = {
turnId,
timestamp: new Date().toISOString(),
usage,
model,
duration
};
await this.appendConversationMeta(meta);
// 更新任务统计
if (usage) {
await this.updateTaskStats(usage);
}
}
/**
* 更新任务时间戳
*/
private async updateTaskTimestamp(): Promise<void> {
const meta = await this.loadTaskMeta();
if (meta) {
meta.updatedAt = new Date().toISOString();
await this.saveTaskMeta(meta);
}
}
/**
* 更新任务统计
*/
private async updateTaskStats(usage: { inputTokens?: number; outputTokens?: number; totalTokens?: number }): Promise<void> {
const meta = await this.loadTaskMeta();
if (meta) {
meta.stats.inputTokens += usage.inputTokens || 0;
meta.stats.outputTokens += usage.outputTokens || 0;
meta.stats.totalTokens += usage.totalTokens || 0;
meta.updatedAt = new Date().toISOString();
await this.saveTaskMeta(meta);
}
}
/**
* 获取当前任务会话
*/
public async getCurrentTaskSession(): Promise<TaskSession | null> {
const meta = await this.loadTaskMeta();
if (!meta) {
return null;
}
const messages = await this.loadConversation();
const conversationMeta = await this.loadConversationMeta();
return {
meta,
messages,
conversationMeta
};
}
/**
* 加载对话元数据
*/
private async loadConversationMeta(): Promise<ConversationMeta[]> {
if (!this.currentTaskId || !this.currentProjectPath) {
return [];
}
const taskDir = this.getTaskDir(this.currentProjectPath, this.currentTaskId);
const metaPath = path.join(taskDir, 'conversation_meta.jsonl');
try {
const uri = vscode.Uri.file(metaPath);
const content = await vscode.workspace.fs.readFile(uri);
const data = Buffer.from(content).toString('utf-8');
return data
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
} catch (error) {
// 文件不存在或读取失败
return [];
}
}
/**
* 列出项目的所有任务
*/
public async listProjectTasks(projectPath: string): Promise<TaskMeta[]> {
const encodedPath = this.encodeProjectPath(projectPath);
const projectDir = path.join(this.baseDir, 'projects', encodedPath);
try {
const uri = vscode.Uri.file(projectDir);
const entries = await vscode.workspace.fs.readDirectory(uri);
const tasks: TaskMeta[] = [];
for (const [taskId, type] of entries) {
if (type === vscode.FileType.Directory) {
const metaPath = path.join(projectDir, taskId, 'meta.json');
try {
const metaUri = vscode.Uri.file(metaPath);
const content = await vscode.workspace.fs.readFile(metaUri);
const data = Buffer.from(content).toString('utf-8');
tasks.push(JSON.parse(data));
} catch (error) {
console.error(`加载任务 ${taskId} 失败:`, error);
// 跳过无效的任务目录
// 尝试清理空目录
try {
const taskDirUri = vscode.Uri.file(path.join(projectDir, taskId));
const taskDirEntries = await vscode.workspace.fs.readDirectory(taskDirUri);
if (taskDirEntries.length === 0) {
// 目录为空,删除它
await vscode.workspace.fs.delete(taskDirUri, { recursive: false });
console.log(`已清理空任务目录: ${taskId}`);
}
} catch (cleanupError) {
// 清理失败,忽略错误
console.warn(`清理任务目录 ${taskId} 失败:`, cleanupError);
}
}
}
}
return tasks.sort((a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
} catch (error) {
// 目录不存在
return [];
}
}
/**
* 切换到指定任务
*/
public async switchTask(projectPath: string, taskId: string): Promise<boolean> {
const taskDir = this.getTaskDir(projectPath, taskId);
const metaPath = path.join(taskDir, 'meta.json');
try {
const uri = vscode.Uri.file(metaPath);
await vscode.workspace.fs.stat(uri);
this.currentProjectPath = projectPath;
this.currentTaskId = taskId;
return true;
} catch {
return false;
}
}
/**
* 获取当前任务ID
*/
public getCurrentTaskId(): string | null {
return this.currentTaskId;
}
/**
* 获取基础目录
*/
public getBaseDir(): string {
return this.baseDir;
}
/**
* 加载指定任务的会话内容
* @param projectPath 项目路径
* @param taskId 任务ID
* @returns 任务会话内容如果任务不存在则返回null
*/
public async loadTaskSession(projectPath: string, taskId: string): Promise<TaskSession | null> {
const taskDir = this.getTaskDir(projectPath, taskId);
const metaPath = path.join(taskDir, 'meta.json');
try {
// 检查任务是否存在
const metaUri = vscode.Uri.file(metaPath);
const metaContent = await vscode.workspace.fs.readFile(metaUri);
const meta: TaskMeta = JSON.parse(Buffer.from(metaContent).toString('utf-8'));
// 读取会话内容
const conversationPath = path.join(taskDir, 'conversation.json');
let messages: ChatMessage[] = [];
try {
const conversationUri = vscode.Uri.file(conversationPath);
const conversationContent = await vscode.workspace.fs.readFile(conversationUri);
messages = JSON.parse(Buffer.from(conversationContent).toString('utf-8'));
} catch {
// 会话文件不存在,使用空数组
}
// 读取会话元数据
const conversationMetaPath = path.join(taskDir, 'conversation_meta.jsonl');
let conversationMeta: ConversationMeta[] = [];
try {
const metaUri = vscode.Uri.file(conversationMetaPath);
const content = await vscode.workspace.fs.readFile(metaUri);
const data = Buffer.from(content).toString('utf-8');
conversationMeta = data
.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
} catch {
// 元数据文件不存在,使用空数组
}
return {
meta,
messages,
conversationMeta
};
} catch (error) {
console.error(`加载任务 ${taskId} 的会话失败:`, error);
return null;
}
}
/**
* 获取会话历史列表(支持分页)
* 返回格式:{ id: taskId, title: 第一句用户消息, timestamp: 创建时间 }
* @param projectPath 项目路径
* @param offset 偏移量从第几条开始默认0
* @param limit 每页数量默认10条
* @returns { items: 历史列表, total: 总数, hasMore: 是否还有更多 }
*/
public async getConversationHistoryList(
projectPath: string,
offset: number = 0,
limit: number = 10
): Promise<{
items: Array<{ id: string; title: string; timestamp: string }>;
total: number;
hasMore: boolean;
}> {
const tasks = await this.listProjectTasks(projectPath);
const total = tasks.length;
const historyList: Array<{ id: string; title: string; timestamp: string }> = [];
// 计算分页范围
const start = offset;
const end = Math.min(offset + limit, total);
const limitedTasks = tasks.slice(start, end);
for (const task of limitedTasks) {
// 读取该任务的 conversation.json 获取第一句用户消息
const taskDir = this.getTaskDir(task.projectPath, task.taskId);
const conversationPath = path.join(taskDir, 'conversation.json');
try {
const uri = vscode.Uri.file(conversationPath);
const content = await vscode.workspace.fs.readFile(uri);
const data = Buffer.from(content).toString('utf-8');
const messages: ChatMessage[] = JSON.parse(data);
// 找到第一条用户消息
const firstUserMessage = messages.find(msg => msg.type === MessageType.USER) as UserMessage | undefined;
let title = '未命名会话';
if (firstUserMessage && firstUserMessage.contents && firstUserMessage.contents.length > 0) {
const textContent = firstUserMessage.contents.find(c => c.type === 'TEXT');
if (textContent && 'text' in textContent) {
// 截取前50个字符作为标题
title = textContent.text.length > 50
? textContent.text.substring(0, 50) + '...'
: textContent.text;
}
}
historyList.push({
id: task.taskId,
title,
timestamp: task.createdAt
});
} catch (error) {
console.error(`读取任务 ${task.taskId} 的会话历史失败:`, error);
// 如果读取失败,使用任务名称作为标题
historyList.push({
id: task.taskId,
title: task.taskName || '未命名会话',
timestamp: task.createdAt
});
}
}
// 返回分页结果
return {
items: historyList,
total,
hasMore: end < total
};
}
}

View File

@ -1,5 +1,4 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path"; import * as path from "path";
/** /**
@ -21,21 +20,31 @@ export async function createFile(
} }
} }
const fileUri = vscode.Uri.file(absolutePath);
// 检测文件是否已存在 // 检测文件是否已存在
if (fs.existsSync(absolutePath)) { try {
await vscode.workspace.fs.stat(fileUri);
throw new Error(`文件已存在: ${absolutePath}`); throw new Error(`文件已存在: ${absolutePath}`);
} catch (error: any) {
// 如果文件不存在,继续创建
if (error.code !== 'FileNotFound') {
throw error;
}
} }
// 确保目录存在 // 确保目录存在
const dirPath = path.dirname(absolutePath); const dirPath = path.dirname(absolutePath);
if (!fs.existsSync(dirPath)) { const dirUri = vscode.Uri.file(dirPath);
fs.mkdirSync(dirPath, { try {
recursive: true, await vscode.workspace.fs.stat(dirUri);
}); } catch {
await vscode.workspace.fs.createDirectory(dirUri);
} }
// 创建文件 // 创建文件
fs.writeFileSync(absolutePath, content, "utf-8"); const contentBytes = Buffer.from(content, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, contentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -62,14 +71,17 @@ export async function createOrOverwriteFile(
// 确保目录存在 // 确保目录存在
const dirPath = path.dirname(absolutePath); const dirPath = path.dirname(absolutePath);
if (!fs.existsSync(dirPath)) { const dirUri = vscode.Uri.file(dirPath);
fs.mkdirSync(dirPath, { try {
recursive: true, await vscode.workspace.fs.stat(dirUri);
}); } catch {
await vscode.workspace.fs.createDirectory(dirUri);
} }
// 创建或覆盖文件 // 创建或覆盖文件
fs.writeFileSync(absolutePath, content, "utf-8"); const fileUri = vscode.Uri.file(absolutePath);
const contentBytes = Buffer.from(content, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, contentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -91,18 +103,25 @@ export async function createDirectory(dirPath: string): Promise<void> {
} }
} }
const dirUri = vscode.Uri.file(absolutePath);
// 检测创建目录是否存在 // 检测创建目录是否存在
if (fs.existsSync(absolutePath)) { try {
const state = fs.statSync(absolutePath); const stat = await vscode.workspace.fs.stat(dirUri);
if (state.isDirectory()) { if (stat.type === vscode.FileType.Directory) {
throw new Error(`目录已存在: ${absolutePath}`); throw new Error(`目录已存在: ${absolutePath}`);
} else { } else {
throw new Error(`路径已存在且不是目录: ${absolutePath}`); throw new Error(`路径已存在且不是目录: ${absolutePath}`);
} }
} catch (error: any) {
// 如果目录不存在,继续创建
if (error.code !== 'FileNotFound') {
throw error;
}
} }
// 创建目录 // 创建目录
fs.mkdirSync(absolutePath, { recursive: true }); await vscode.workspace.fs.createDirectory(dirUri);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -146,19 +165,20 @@ export async function deleteFile(filePath: string): Promise<void> {
} }
} }
const fileUri = vscode.Uri.file(absolutePath);
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(absolutePath)) { try {
const stat = await vscode.workspace.fs.stat(fileUri);
if (stat.type !== vscode.FileType.File) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`); throw new Error(`文件不存在: ${absolutePath}`);
} }
// 检查是否是文件
const stats = fs.statSync(absolutePath);
if (!stats.isFile()) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
// 删除文件 // 删除文件
fs.unlinkSync(absolutePath); await vscode.workspace.fs.delete(fileUri);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -181,19 +201,21 @@ export async function updateFile(
} }
} }
const fileUri = vscode.Uri.file(absolutePath);
// 检查文件是否存在 // 检查文件是否存在
if (!fs.existsSync(absolutePath)) { try {
const stat = await vscode.workspace.fs.stat(fileUri);
if (stat.type !== vscode.FileType.File) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`); throw new Error(`文件不存在: ${absolutePath}`);
} }
// 检查是否是文件内容
const state = fs.statSync(absolutePath);
if (!state.isFile()) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
// 修改文件内容 // 修改文件内容
fs.writeFileSync(absolutePath, content, "utf-8"); const contentBytes = Buffer.from(content, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, contentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -218,13 +240,21 @@ export async function appendToFile(
} }
} }
// 检查文件是否存在 const fileUri = vscode.Uri.file(absolutePath);
if (!fs.existsSync(absolutePath)) {
// 检查文件是否存在并读取原内容
let existingContent = "";
try {
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
existingContent = Buffer.from(contentBytes).toString("utf-8");
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`); throw new Error(`文件不存在: ${absolutePath}`);
} }
// 追加内容 // 追加内容
fs.appendFileSync(absolutePath, content, "utf-8"); const newContent = existingContent + content;
const contentBytes = Buffer.from(newContent, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, contentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -248,13 +278,16 @@ export async function replaceFile(
} }
} }
// 检查文件是否存在 const fileUri = vscode.Uri.file(absolutePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`文件不存在: ${absolutePath}`);
}
// 读取文件内容 // 读取文件内容
const fileContent = fs.readFileSync(absolutePath, "utf-8"); let fileContent: string;
try {
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
fileContent = Buffer.from(contentBytes).toString("utf-8");
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`);
}
// 转义特殊字符,将字符串作为字面量处理 // 转义特殊字符,将字符串作为字面量处理
const escapeRegExp = (str: string) => { const escapeRegExp = (str: string) => {
@ -276,7 +309,8 @@ export async function replaceFile(
} }
// 写回文件 // 写回文件
fs.writeFileSync(absolutePath, newContent, "utf-8"); const contentBytes = Buffer.from(newContent, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, contentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -300,20 +334,26 @@ export async function insertAtLine(
} }
} }
// 检查文件是否存在 const fileUri = vscode.Uri.file(absolutePath);
if (!fs.existsSync(absolutePath)) {
// 读取文件内容
let fileContent: string;
try {
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
fileContent = Buffer.from(contentBytes).toString("utf-8");
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`); throw new Error(`文件不存在: ${absolutePath}`);
} }
// 读取文件内容
const fileContent = fs.readFileSync(absolutePath, "utf-8");
const lines = fileContent.split("\n"); const lines = fileContent.split("\n");
// 插入内容 // 插入内容
lines.splice(lineNumber, 0, content); lines.splice(lineNumber, 0, content);
// 写回文件 // 写回文件
fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8"); const newContent = lines.join("\n");
const newContentBytes = Buffer.from(newContent, "utf-8");
await vscode.workspace.fs.writeFile(fileUri, newContentBytes);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -345,24 +385,41 @@ export async function renameFile(
throw new Error("没有打开的工作区,无法重命名相对路径的文件"); throw new Error("没有打开的工作区,无法重命名相对路径的文件");
} }
const oldUri = vscode.Uri.file(absoluteOldPath);
const newUri = vscode.Uri.file(absoluteNewPath);
// 检查原文件是否存在 // 检查原文件是否存在
if (!fs.existsSync(absoluteOldPath)) { try {
await vscode.workspace.fs.stat(oldUri);
} catch (error) {
throw new Error(`文件不存在: ${absoluteOldPath}`); throw new Error(`文件不存在: ${absoluteOldPath}`);
} }
// 检查新文件名是否已存在 // 检查新文件名是否已存在
if (fs.existsSync(absoluteNewPath)) { try {
await vscode.workspace.fs.stat(newUri);
throw new Error(`目标文件已存在: ${absoluteNewPath}`); throw new Error(`目标文件已存在: ${absoluteNewPath}`);
} catch (error: any) {
// 如果文件不存在,继续重命名
if (error.code !== 'FileNotFound' && !error.message.includes('目标文件已存在')) {
throw error;
}
if (error.message.includes('目标文件已存在')) {
throw error;
}
} }
// 确保目标目录存在 // 确保目标目录存在
const newDir = path.dirname(absoluteNewPath); const newDir = path.dirname(absoluteNewPath);
if (!fs.existsSync(newDir)) { const newDirUri = vscode.Uri.file(newDir);
fs.mkdirSync(newDir, { recursive: true }); try {
await vscode.workspace.fs.stat(newDirUri);
} catch {
await vscode.workspace.fs.createDirectory(newDirUri);
} }
// 重命名文件 // 重命名文件
fs.renameSync(absoluteOldPath, absoluteNewPath); await vscode.workspace.fs.rename(oldUri, newUri, { overwrite: false });
} catch (error) { } catch (error) {
throw error; throw error;
} }

415
src/utils/iverilogRunner.ts Normal file
View File

@ -0,0 +1,415 @@
import * as vscode from "vscode";
import * as path from "path";
import { spawn } from "child_process";
import { promisify } from "util";
// 使用 spawn 来执行命令,正确处理路径空格
function execCommand(
command: string,
args: string[],
options: { cwd: string; env?: any }
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
// 在 Windows 上,如果路径包含空格,不使用 shell直接用 spawn
// spawn 在不使用 shell 时会正确处理带空格的路径
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env || process.env,
windowsHide: true,
shell: false, // 不使用 shell让 spawn 直接执行
});
let stdout = "";
let stderr = "";
// 在 Windows 上使用 GBK 编码解码输出
const encoding = process.platform === 'win32' ? 'gbk' : 'utf8';
child.stdout.on("data", (data) => {
try {
// 尝试使用 iconv-lite 解码(如果可用)
const iconv = require('iconv-lite');
stdout += iconv.decode(data, encoding);
} catch {
// 如果 iconv-lite 不可用,使用默认解码
stdout += data.toString('utf8');
}
});
child.stderr.on("data", (data) => {
try {
const iconv = require('iconv-lite');
stderr += iconv.decode(data, encoding);
} catch {
stderr += data.toString('utf8');
}
});
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr });
} else {
const error: any = new Error(`Command failed with exit code ${code}`);
error.stdout = stdout;
error.stderr = stderr;
error.code = code;
reject(error);
}
});
child.on("error", (error) => {
reject(error);
});
});
}
/**
* Verilog 项目文件检查结果
*/
export interface VerilogProjectCheck {
isComplete: boolean;
hasTopModule: boolean;
hasTestbench: boolean;
topModuleFile?: string;
testbenchFile?: string;
allVerilogFiles: string[];
missingFiles: string[];
errors: string[];
}
/**
* VCD 生成结果
*/
export interface VCDGenerationResult {
success: boolean;
vcdFilePath?: string;
outputPath?: string;
message: string;
stdout?: string;
stderr?: string;
}
/**
* 检查项目中的 Verilog 文件完整性
*/
export async function checkVerilogProject(
projectPath: string
): Promise<VerilogProjectCheck> {
const result: VerilogProjectCheck = {
isComplete: false,
hasTopModule: false,
hasTestbench: false,
allVerilogFiles: [],
missingFiles: [],
errors: [],
};
try {
// 检查项目路径是否存在
const projectUri = vscode.Uri.file(projectPath);
try {
await vscode.workspace.fs.stat(projectUri);
} catch (error) {
result.errors.push(`项目路径不存在: ${projectPath}`);
return result;
}
// 查找所有 Verilog 文件 (.v, .sv)
const verilogFiles = await findVerilogFiles(projectPath);
result.allVerilogFiles = verilogFiles;
if (verilogFiles.length === 0) {
result.errors.push("项目中没有找到 Verilog 文件 (.v 或 .sv)");
return result;
}
// 分析文件内容,查找 top module 和 testbench
for (const file of verilogFiles) {
const fileUri = vscode.Uri.file(file);
const contentBytes = await vscode.workspace.fs.readFile(fileUri);
const content = Buffer.from(contentBytes).toString("utf-8");
const fileName = path.basename(file).toLowerCase();
// 检查是否是 testbench 文件
if (
fileName.includes("tb") ||
fileName.includes("test") ||
content.includes("$dumpfile") ||
content.includes("$dumpvars")
) {
result.hasTestbench = true;
result.testbenchFile = file;
}
// 检查是否包含 module 定义(可能是 top module
if (content.match(/module\s+\w+/)) {
if (!result.hasTopModule && !fileName.includes("tb")) {
result.hasTopModule = true;
result.topModuleFile = file;
}
}
}
// 检查完整性
if (!result.hasTopModule) {
result.missingFiles.push("缺少顶层模块文件");
}
if (!result.hasTestbench) {
result.missingFiles.push("缺少 testbench 文件");
}
result.isComplete = result.hasTopModule && result.hasTestbench;
return result;
} catch (error) {
result.errors.push(
`检查项目时出错: ${error instanceof Error ? error.message : "未知错误"}`
);
return result;
}
}
/**
* 递归查找目录下所有 Verilog 文件
*/
async function findVerilogFiles(dir: string): Promise<string[]> {
const verilogFiles: string[] = [];
async function searchDir(currentDir: string) {
const dirUri = vscode.Uri.file(currentDir);
const entries = await vscode.workspace.fs.readDirectory(dirUri);
for (const [fileName, fileType] of entries) {
const filePath = path.join(currentDir, fileName);
if (fileType === vscode.FileType.Directory) {
// 跳过 node_modules 等目录
if (!fileName.startsWith(".") && fileName !== "node_modules") {
await searchDir(filePath);
}
} else if (fileType === vscode.FileType.File) {
const ext = path.extname(fileName).toLowerCase();
if (ext === ".v" || ext === ".sv") {
verilogFiles.push(filePath);
}
}
}
}
await searchDir(dir);
return verilogFiles;
}
/**
* 获取 iverilog 可执行文件路径
*/
async function getIverilogPath(extensionPath: string): Promise<string> {
const platform = process.platform;
let iverilogBin = "";
if (platform === "win32") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog.exe");
} else if (platform === "darwin") {
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
} else {
// Linux
iverilogBin = path.join(extensionPath, "tools", "iverilog", "bin", "iverilog");
}
// 如果插件包中没有,尝试使用系统安装的 iverilog
const iverilogUri = vscode.Uri.file(iverilogBin);
try {
await vscode.workspace.fs.stat(iverilogUri);
return iverilogBin;
} catch {
return "iverilog"; // 使用系统 PATH 中的 iverilog
}
}
/**
* 获取 vvp 可执行文件路径
*/
async function getVvpPath(extensionPath: string): Promise<string> {
const platform = process.platform;
let vvpBin = "";
if (platform === "win32") {
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp.exe");
} else if (platform === "darwin") {
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp");
} else {
// Linux
vvpBin = path.join(extensionPath, "tools", "iverilog", "bin", "vvp");
}
// 如果插件包中没有,尝试使用系统安装的 vvp
const vvpUri = vscode.Uri.file(vvpBin);
try {
await vscode.workspace.fs.stat(vvpUri);
return vvpBin;
} catch {
return "vvp"; // 使用系统 PATH 中的 vvp
}
}
/**
* 运行 iverilog 生成 VCD 文件
*/
export async function generateVCD(
projectPath: string,
extensionPath: string
): Promise<VCDGenerationResult> {
try {
// 1. 检查项目完整性
const projectCheck = await checkVerilogProject(projectPath);
if (!projectCheck.isComplete) {
return {
success: false,
message: `项目文件不完整:\n${projectCheck.missingFiles.join("\n")}\n${projectCheck.errors.join("\n")}`,
};
}
// 2. 获取 iverilog 和 vvp 路径
const iverilogPath = await getIverilogPath(extensionPath);
const vvpPath = await getVvpPath(extensionPath);
// 3. 准备输出路径
const outputDir = projectPath;
const outputFile = path.join(outputDir, "simulation.vvp");
// 4. 设置环境变量,确保 iverilog 能找到库文件
const libPath = path.join(extensionPath, "tools", "iverilog", "lib");
const env = {
...process.env,
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
};
// 5. 构建 iverilog 编译参数
const compileArgs = ["-o", outputFile, ...projectCheck.allVerilogFiles];
console.log("执行编译命令:", iverilogPath, compileArgs.join(" "));
console.log("IVERILOG_ROOT:", env.IVERILOG_ROOT);
// 6. 执行编译
let compileResult;
try {
compileResult = await execCommand(iverilogPath, compileArgs, {
cwd: projectPath,
env: env,
});
} catch (error: any) {
return {
success: false,
message: `iverilog 编译失败:\n${error.message}`,
stderr: error.stderr,
stdout: error.stdout,
};
}
// 7. 执行仿真生成 VCD
const simArgs = [outputFile];
console.log("执行仿真命令:", vvpPath, simArgs.join(" "));
let simResult;
try {
simResult = await execCommand(vvpPath, simArgs, {
cwd: projectPath,
env: env,
});
} catch (error: any) {
return {
success: false,
message: `VVP 仿真失败:\n${error.message}`,
stderr: error.stderr,
stdout: error.stdout,
};
}
// 8. 查找生成的 VCD 文件
const projectUri = vscode.Uri.file(projectPath);
const entries = await vscode.workspace.fs.readDirectory(projectUri);
const vcdFiles = entries
.filter(([fileName, fileType]) => fileType === vscode.FileType.File && fileName.endsWith('.vcd'))
.map(([fileName]) => fileName);
if (vcdFiles.length === 0) {
return {
success: false,
message: "VCD 文件未生成。请确保 testbench 中包含 $dumpfile 和 $dumpvars 语句。",
stdout: simResult.stdout,
};
}
// 使用找到的第一个 VCD 文件
const vcdFile = path.join(projectPath, vcdFiles[0]);
// 9. 清理中间文件
try {
const outputUri = vscode.Uri.file(outputFile);
await vscode.workspace.fs.stat(outputUri);
await vscode.workspace.fs.delete(outputUri);
} catch (error) {
// 忽略清理错误
}
return {
success: true,
vcdFilePath: vcdFile,
outputPath: projectPath,
message: `VCD 文件生成成功!\n路径: ${vcdFile}`,
stdout: simResult.stdout,
};
} catch (error) {
return {
success: false,
message: `生成 VCD 文件时出错: ${error instanceof Error ? error.message : "未知错误"}`,
};
}
}
/**
* 检查 iverilog 是否可用
*/
export async function checkIverilogAvailable(
extensionPath: string
): Promise<{ available: boolean; version?: string; message: string }> {
try {
const iverilogPath = await getIverilogPath(extensionPath);
// 检查文件是否存在
const iverilogUri = vscode.Uri.file(iverilogPath);
try {
await vscode.workspace.fs.stat(iverilogUri);
} catch (error) {
return {
available: false,
message: `iverilog 不可用。未找到文件: ${iverilogPath}`,
};
}
// 设置环境变量
const env = {
...process.env,
IVERILOG_ROOT: path.join(extensionPath, "tools", "iverilog"),
};
const result = await execCommand(iverilogPath, ["-V"], {
cwd: path.dirname(iverilogPath),
env: env,
});
const version = result.stdout.trim().split("\n")[0];
return {
available: true,
version: version,
message: `iverilog 可用: ${version}`,
};
} catch (error: any) {
return {
available: false,
message: `iverilog 执行失败: ${error.message}\n${error.stderr || ""}`,
};
}
}

View File

@ -1,4 +1,5 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as path from "path";
import { readFileContent } from "./readFiles"; import { readFileContent } from "./readFiles";
import { import {
createFile, createFile,
@ -8,30 +9,80 @@ import {
renameFile, renameFile,
replaceFile, replaceFile,
} from "./createFiles"; } from "./createFiles";
import {
generateVCD,
checkVerilogProject,
checkIverilogAvailable,
} from "./iverilogRunner";
import { ChatHistoryManager } from "./chatHistoryManager";
import { dialogManager, DialogSession } from "../services/dialogService";
import { userInteractionManager } from "../services/userInteraction";
import { healthCheck } from "../services/apiClient";
/** 是否使用后端服务(可通过配置控制) */
let useBackendService = true;
/** 当前对话会话 */
let currentSession: DialogSession | null = null;
/** /**
* 处理用户消息 * 处理用户消息
*/ */
export async function handleUserMessage( export async function handleUserMessage(
panel: vscode.WebviewPanel, panel: vscode.WebviewPanel,
text: string text: string,
extensionPath?: string
) { ) {
console.log("收到用户消息:", text); console.log("收到用户消息:", text);
// 检查是否是文件操作命令 // 记录用户消息到历史(允许失败,不阻塞主流程)
try {
const historyManager = ChatHistoryManager.getInstance();
await historyManager.addUserMessage(text);
} catch (error) {
console.warn("记录消息历史失败(可能没有打开工作区):", error);
}
// 设置 WebView 面板用于用户交互
userInteractionManager.setWebviewPanel(panel);
// 检查是否是 VCD 生成命令(本地处理)
if (isVCDGenerationCommand(text)) {
await handleVCDGeneration(panel, extensionPath || "");
return;
}
// 检查是否是文件操作命令(本地处理)
const fileOperation = parseFileOperation(text); const fileOperation = parseFileOperation(text);
console.log("解析结果:", fileOperation);
if (fileOperation) { if (fileOperation) {
console.log("执行文件操作:", fileOperation.type, fileOperation.filePath); console.log("执行文件操作:", fileOperation.type, fileOperation.filePath);
await handleFileOperation(panel, fileOperation); await handleFileOperation(panel, fileOperation);
return; return;
} }
// 普通消息处理 // 尝试使用后端服务
console.log("作为普通消息处理"); if (useBackendService && extensionPath) {
try {
await handleUserMessageWithBackend(panel, text, extensionPath);
return;
} catch (error) {
console.error("后端服务不可用,回退到本地模式:", error);
// 后端不可用时,使用本地模拟回复
}
}
// 本地模拟回复(后端不可用时的 fallback
console.log("使用本地模拟回复");
const reply = getMockReply(text); const reply = getMockReply(text);
// 记录AI回复到历史允许失败
try {
const historyManager = ChatHistoryManager.getInstance();
await historyManager.addAiMessage(reply);
} catch (error) {
console.warn("记录AI回复历史失败:", error);
}
setTimeout(() => { setTimeout(() => {
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
@ -40,6 +91,141 @@ export async function handleUserMessage(
}, 500); }, 500);
} }
/**
* 使用后端服务处理用户消息
*/
async function handleUserMessageWithBackend(
panel: vscode.WebviewPanel,
text: string,
extensionPath: string
): Promise<void> {
// 创建或复用会话
if (!currentSession || !currentSession.active) {
currentSession = dialogManager.createSession(extensionPath);
}
const historyManager = ChatHistoryManager.getInstance();
// 显示状态栏
panel.webview.postMessage({
command: "updateStatus",
text: "思考中...",
type: "thinking",
});
return new Promise((resolve, reject) => {
currentSession!.sendMessage(text, {
onText: (fullText, isStreaming) => {
// 不再单独处理文本,统一通过 onSegmentUpdate 处理
},
onSegmentUpdate: (segments) => {
// 实时发送段落更新,按后端返回顺序展示
panel.webview.postMessage({
command: "updateSegments",
segments: segments,
});
},
onToolStart: (toolName) => {
// 更新状态栏
panel.webview.postMessage({
command: "updateStatus",
text: `正在执行 ${toolName}...`,
type: "working",
});
},
onToolComplete: (toolName, result) => {
// 工具完成,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onToolError: (toolName, error) => {
// 工具错误,不需要单独处理,通过 onSegmentUpdate 统一更新
},
onQuestion: (askId, question, options) => {
// 只更新状态栏,问题显示由 onSegmentUpdate 统一处理
panel.webview.postMessage({
command: "updateStatus",
text: "等待用户回答...",
type: "working",
});
},
onComplete: async (segments) => {
// 隐藏状态栏
panel.webview.postMessage({
command: "hideStatus",
});
// 最后一次发送完整的段落
console.log('[MessageHandler] 对话完成, 段落数:', segments.length);
console.log('[MessageHandler] segments 内容:', JSON.stringify(segments));
const result = await panel.webview.postMessage({
command: "updateSegments",
segments: segments,
isComplete: true,
});
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');
await historyManager.addAiMessage(textContent, undefined, segments);
} catch (error) {
console.warn("保存AI响应历史失败:", error);
}
resolve();
},
onError: (message) => {
panel.webview.postMessage({
command: "hideLoading",
});
panel.webview.postMessage({
command: "receiveMessage",
text: `❌ 错误: ${message}`,
});
reject(new Error(message));
},
onNotification: (message) => {
vscode.window.showInformationMessage(message);
},
});
});
}
/**
* 处理用户回答(从 WebView 调用)
*/
export async function handleUserAnswer(
askId: string,
selected?: string[],
customInput?: string
): Promise<void> {
if (currentSession) {
await currentSession.submitAnswer(askId, selected, customInput);
}
}
/**
* 中止当前对话
*/
export function abortCurrentDialog(): void {
dialogManager.abortCurrentSession();
currentSession = null;
}
/** /**
* 解析文件操作命令 * 解析文件操作命令
*/ */
@ -154,28 +340,36 @@ async function handleFileOperation(
replaceText?: string; replaceText?: string;
} }
) { ) {
const historyManager = ChatHistoryManager.getInstance();
try { try {
let responseText = "";
switch (operation.type) { switch (operation.type) {
case "create": case "create":
await createFile(operation.filePath, operation.content || ""); await createFile(operation.filePath, operation.content || "");
responseText = `✅ 文件创建成功: ${operation.filePath}`;
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件创建成功: ${operation.filePath}`, text: responseText,
}); });
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
`文件创建成功: ${operation.filePath}` `文件创建成功: ${operation.filePath}`
); );
await historyManager.addAiMessage(responseText);
break; break;
case "delete": case "delete":
await deleteFile(operation.filePath); await deleteFile(operation.filePath);
responseText = `✅ 文件删除成功: ${operation.filePath}`;
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件删除成功: ${operation.filePath}`, text: responseText,
}); });
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
`文件删除成功: ${operation.filePath}` `文件删除成功: ${operation.filePath}`
); );
await historyManager.addAiMessage(responseText);
break; break;
case "read": case "read":
@ -185,6 +379,7 @@ async function handleFileOperation(
content: content, content: content,
filePath: operation.filePath, filePath: operation.filePath,
}); });
await historyManager.addAiMessage(`读取文件: ${operation.filePath}`);
break; break;
case "update": case "update":
@ -194,6 +389,7 @@ async function handleFileOperation(
content: currentContent, content: currentContent,
filePath: operation.filePath, filePath: operation.filePath,
}); });
await historyManager.addAiMessage(`编辑文件: ${operation.filePath}`);
break; break;
case "rename": case "rename":
@ -201,13 +397,15 @@ async function handleFileOperation(
throw new Error("缺少新文件名"); throw new Error("缺少新文件名");
} }
await renameFile(operation.filePath, operation.newPath); await renameFile(operation.filePath, operation.newPath);
responseText = `✅ 文件重命名成功: ${operation.filePath}${operation.newPath}`;
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件重命名成功: ${operation.filePath}${operation.newPath}`, text: responseText,
}); });
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
`文件重命名成功: ${operation.filePath}${operation.newPath}` `文件重命名成功: ${operation.filePath}${operation.newPath}`
); );
await historyManager.addAiMessage(responseText);
break; break;
case "replace": case "replace":
@ -219,13 +417,15 @@ async function handleFileOperation(
operation.searchText, operation.searchText,
operation.replaceText operation.replaceText
); );
responseText = `✅ 文件内容替换成功: ${operation.filePath}`;
panel.webview.postMessage({ panel.webview.postMessage({
command: "receiveMessage", command: "receiveMessage",
text: `✅ 文件内容替换成功: ${operation.filePath}`, text: responseText,
}); });
vscode.window.showInformationMessage( vscode.window.showInformationMessage(
`文件内容替换成功: ${operation.filePath}` `文件内容替换成功: ${operation.filePath}`
); );
await historyManager.addAiMessage(responseText);
break; break;
} }
} catch (error) { } catch (error) {
@ -235,6 +435,7 @@ async function handleFileOperation(
text: `${errorMsg}`, text: `${errorMsg}`,
}); });
vscode.window.showErrorMessage(errorMsg); vscode.window.showErrorMessage(errorMsg);
await historyManager.addAiMessage(`${errorMsg}`);
} }
} }
@ -466,3 +667,165 @@ export function insertCodeToEditor(code: string) {
vscode.window.showWarningMessage("请先打开一个编辑器"); vscode.window.showWarningMessage("请先打开一个编辑器");
} }
} }
/**
* 检查是否是 VCD 生成命令
*/
function isVCDGenerationCommand(text: string): boolean {
const lowerText = text.toLowerCase().trim();
// 匹配各种 VCD 生成命令
const vcdPatterns = [
/生成\s*vcd/,
/创建\s*vcd/,
/运行\s*仿真/,
/执行\s*仿真/,
/iverilog/,
/生成\s*波形/,
/仿真\s*生成/,
];
return vcdPatterns.some((pattern) => pattern.test(lowerText));
}
/**
* 处理 VCD 生成请求
*/
async function handleVCDGeneration(
panel: vscode.WebviewPanel,
extensionPath: string
) {
try {
// 获取当前工作区路径
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
panel.webview.postMessage({
command: "receiveMessage",
text: "❌ 请先打开一个工作区文件夹",
});
vscode.window.showErrorMessage("请先打开一个工作区文件夹");
return;
}
const projectPath = workspaceFolders[0].uri.fsPath;
// 发送开始消息
panel.webview.postMessage({
command: "receiveMessage",
text: "🔍 正在检查项目文件...",
});
// 1. 检查 iverilog 是否可用
const iverilogCheck = await checkIverilogAvailable(extensionPath);
if (!iverilogCheck.available) {
panel.webview.postMessage({
command: "receiveMessage",
text: `${iverilogCheck.message}\n\n请参考插件文档安装 iverilog 工具。`,
});
vscode.window.showErrorMessage(iverilogCheck.message);
return;
}
// 2. 检查项目文件完整性
const projectCheck = await checkVerilogProject(projectPath);
if (!projectCheck.isComplete) {
let errorMsg = "❌ 项目文件不完整:\n\n";
if (projectCheck.allVerilogFiles.length === 0) {
errorMsg += "• 未找到任何 Verilog 文件 (.v 或 .sv)\n";
} else {
errorMsg += `• 找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n`;
if (!projectCheck.hasTopModule) {
errorMsg += "• ❌ 缺少顶层模块文件\n";
} else {
errorMsg += `• ✅ 顶层模块: ${projectCheck.topModuleFile}\n`;
}
if (!projectCheck.hasTestbench) {
errorMsg += "• ❌ 缺少 testbench 文件\n";
errorMsg += "\n提示: testbench 文件应包含 $dumpfile 和 $dumpvars 语句来生成 VCD 文件。\n";
} else {
errorMsg += `• ✅ Testbench: ${projectCheck.testbenchFile}\n`;
}
}
if (projectCheck.errors.length > 0) {
errorMsg += "\n错误信息:\n" + projectCheck.errors.join("\n");
}
panel.webview.postMessage({
command: "receiveMessage",
text: errorMsg,
});
vscode.window.showWarningMessage("项目文件不完整,无法生成 VCD");
return;
}
// 3. 显示项目检查结果
panel.webview.postMessage({
command: "receiveMessage",
text: `✅ 项目检查通过!\n\n找到 ${projectCheck.allVerilogFiles.length} 个 Verilog 文件\n• 顶层模块: ${projectCheck.topModuleFile}\n• Testbench: ${projectCheck.testbenchFile}\n\n🚀 开始编译和仿真...`,
});
// 4. 生成 VCD 文件
const result = await generateVCD(projectPath, extensionPath);
if (result.success) {
let successMsg = `${result.message}`;
if (result.stdout) {
successMsg += `\n\n仿真输出:\n${result.stdout}`;
}
// 发送带波形预览的消息
if (result.vcdFilePath) {
const fileName = path.basename(result.vcdFilePath);
panel.webview.postMessage({
command: "vcdGenerated",
text: successMsg,
vcdFilePath: result.vcdFilePath,
fileName: fileName,
});
vscode.window.showInformationMessage(
`VCD 文件生成成功: ${fileName}`
);
} else {
panel.webview.postMessage({
command: "receiveMessage",
text: successMsg,
});
}
} else {
let errorMsg = `${result.message}`;
if (result.stderr) {
errorMsg += `\n\n错误输出:\n${result.stderr}`;
}
if (result.stdout) {
errorMsg += `\n\n标准输出:\n${result.stdout}`;
}
panel.webview.postMessage({
command: "receiveMessage",
text: errorMsg,
});
vscode.window.showErrorMessage("VCD 文件生成失败");
}
} catch (error) {
const errorMsg = `❌ 生成 VCD 文件时出错: ${
error instanceof Error ? error.message : "未知错误"
}`;
panel.webview.postMessage({
command: "receiveMessage",
text: errorMsg,
});
vscode.window.showErrorMessage(errorMsg);
}
}

View File

@ -1,5 +1,4 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path"; import * as path from "path";
/** /**
@ -16,19 +15,21 @@ export async function readFileContent(filePath: string): Promise<string> {
} }
} }
// 检查文件是否存在 const fileUri = vscode.Uri.file(absolutePath);
if (!fs.existsSync(absolutePath)) {
// 检查文件是否存在并获取文件类型
try {
const stat = await vscode.workspace.fs.stat(fileUri);
if (stat.type !== vscode.FileType.File) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
} catch (error) {
throw new Error(`文件不存在: ${absolutePath}`); throw new Error(`文件不存在: ${absolutePath}`);
} }
// 检查是否是文件
const stats = fs.statSync(absolutePath);
if (!stats.isFile()) {
throw new Error(`路径不是文件: ${absolutePath}`);
}
// 读取文件内容 // 读取文件内容
const content = fs.readFileSync(absolutePath, "utf-8"); const contentBytes = await vscode.workspace.fs.readFile(fileUri);
const content = Buffer.from(contentBytes).toString("utf-8");
return content; return content;
} catch (error) { } catch (error) {
throw error; throw error;
@ -76,36 +77,38 @@ export async function readDirectory(
} }
} }
const dirUri = vscode.Uri.file(absolutePath);
// 检查目录是否存在 // 检查目录是否存在
if (!fs.existsSync(absolutePath)) { try {
const stat = await vscode.workspace.fs.stat(dirUri);
if (stat.type !== vscode.FileType.Directory) {
throw new Error(`路径不是目录: ${absolutePath}`);
}
} catch (error) {
throw new Error(`目录不存在: ${absolutePath}`); throw new Error(`目录不存在: ${absolutePath}`);
} }
const stats = fs.statSync(absolutePath);
if (!stats.isDirectory()) {
throw new Error(`路径不是目录: ${absolutePath}`);
}
// 读取目录内容 // 读取目录内容
const files = fs.readdirSync(absolutePath); const entries = await vscode.workspace.fs.readDirectory(dirUri);
const results = []; const results = [];
for (const file of files) { for (const [fileName, fileType] of entries) {
const filePath = path.join(absolutePath, file); if (fileType === vscode.FileType.File) {
const fileStats = fs.statSync(filePath);
if (fileStats.isFile()) {
// 如果指定了扩展名过滤 // 如果指定了扩展名过滤
if (extensions && extensions.length > 0) { if (extensions && extensions.length > 0) {
const ext = path.extname(file); const ext = path.extname(fileName);
if (!extensions.includes(ext)) { if (!extensions.includes(ext)) {
continue; continue;
} }
} }
try { try {
const content = fs.readFileSync(filePath, "utf-8"); const filePath = path.join(absolutePath, fileName);
results.push({ path: file, content }); 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 });
} catch (error) { } catch (error) {
// 跳过无法读取的文件 // 跳过无法读取的文件
continue; continue;
@ -122,13 +125,13 @@ export async function readDirectory(
/** /**
* 获取文件信息 * 获取文件信息
*/ */
export function getFileInfo(filePath: string): { export async function getFileInfo(filePath: string): Promise<{
exists: boolean; exists: boolean;
isFile: boolean; isFile: boolean;
isDirectory: boolean; isDirectory: boolean;
size?: number; size?: number;
extension?: string; extension?: string;
} { }> {
try { try {
let absolutePath = filePath; let absolutePath = filePath;
if (!path.isAbsolute(filePath)) { if (!path.isAbsolute(filePath)) {
@ -138,20 +141,15 @@ export function getFileInfo(filePath: string): {
} }
} }
if (!fs.existsSync(absolutePath)) { const fileUri = vscode.Uri.file(absolutePath);
return {
exists: false,
isFile: false,
isDirectory: false,
};
}
const stats = fs.statSync(absolutePath); try {
const stat = await vscode.workspace.fs.stat(fileUri);
return { return {
exists: true, exists: true,
isFile: stats.isFile(), isFile: stat.type === vscode.FileType.File,
isDirectory: stats.isDirectory(), isDirectory: stat.type === vscode.FileType.Directory,
size: stats.size, size: stat.size,
extension: path.extname(absolutePath), extension: path.extname(absolutePath),
}; };
} catch (error) { } catch (error) {
@ -161,4 +159,11 @@ export function getFileInfo(filePath: string): {
isDirectory: false, isDirectory: false,
}; };
} }
} catch (error) {
return {
exists: false,
isFile: false,
isDirectory: false,
};
}
} }

View File

@ -5,12 +5,17 @@ import {
insertCodeToEditor, insertCodeToEditor,
handleReadFile, handleReadFile,
handleCreateFile, handleCreateFile,
handleUpdateFile,
handleRenameFile,
handleReplaceInFile,
handleUserAnswer,
abortCurrentDialog,
} from "../utils/messageHandler"; } from "../utils/messageHandler";
/** /**
* 创建并显示IC 侧边栏视图 * 创建并显示IC 侧边栏视图
*/ */
export function showICHelperPanel(content: vscode.ExtensionContext) { export function showICHelperPanel(context: vscode.ExtensionContext) {
// 创建WebView面板 // 创建WebView面板
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"icCoder", // 面板ID "icCoder", // 面板ID
@ -19,20 +24,20 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
{ {
enableScripts: true, enableScripts: true,
retainContextWhenHidden: true, retainContextWhenHidden: true,
localResourceRoots: [vscode.Uri.joinPath(content.extensionUri, "media")], localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
} }
); );
// 设置标签页图标 // 设置标签页图标
panel.iconPath = vscode.Uri.joinPath( panel.iconPath = vscode.Uri.joinPath(
content.extensionUri, context.extensionUri,
"media", "media",
"图案(方底).png" "icon.png"
); );
// 获取页面内图标URI // 获取页面内图标URI
const iconUri = panel.webview.asWebviewUri( const iconUri = panel.webview.asWebviewUri(
vscode.Uri.joinPath(content.extensionUri, "media", "图案(方底).png") vscode.Uri.joinPath(context.extensionUri, "media", "icon.png")
); );
// 设置HTML内容 // 设置HTML内容
panel.webview.html = getWebviewContent(iconUri.toString()); panel.webview.html = getWebviewContent(iconUri.toString());
@ -42,11 +47,25 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
(message) => { (message) => {
switch (message.command) { switch (message.command) {
case "sendMessage": case "sendMessage":
handleUserMessage(panel, message.text); handleUserMessage(panel, message.text, context.extensionPath);
break; break;
case "readFile": case "readFile":
handleReadFile(panel, message.filePath); handleReadFile(panel, message.filePath);
break; break;
case "updateFile":
handleUpdateFile(panel, message.filePath, message.content);
break;
case "renameFile":
handleRenameFile(panel, message.oldPath, message.newPath);
break;
case "replaceInFile":
handleReplaceInFile(
panel,
message.filePath,
message.searchText,
message.replaceText
);
break;
case "insertCode": case "insertCode":
insertCodeToEditor(message.code); insertCodeToEditor(message.code);
break; break;
@ -61,10 +80,22 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
case "showInfo": case "showInfo":
vscode.window.showInformationMessage(message.text); vscode.window.showInformationMessage(message.text);
break; break;
// 新增:处理用户回答
case "submitAnswer":
handleUserAnswer(
message.askId,
message.selected,
message.customInput
);
break;
// 新增:中止对话
case "abortDialog":
abortCurrentDialog();
break;
} }
}, },
undefined, undefined,
content.subscriptions context.subscriptions
); );
} }
@ -72,7 +103,23 @@ export function showICHelperPanel(content: vscode.ExtensionContext) {
* 侧边栏视图提供者 * 侧边栏视图提供者
*/ */
export class ICViewProvider implements vscode.WebviewViewProvider { export class ICViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly extensionUri: vscode.Uri) {} constructor(
private readonly extensionUri: vscode.Uri,
private readonly context: vscode.ExtensionContext
) {}
/**
* 检查登录状态(使用 Authentication API
*/
private async checkLoginStatus(): Promise<boolean> {
try {
const session = await vscode.authentication.getSession("iccoder", [], { createIfNone: false });
return !!session;
} catch (error) {
console.log("检查登录状态失败:", error);
return false;
}
}
resolveWebviewView(webviewView: vscode.WebviewView) { resolveWebviewView(webviewView: vscode.WebviewView) {
webviewView.webview.options = { webviewView.webview.options = {
@ -80,19 +127,30 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")], localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, "media")],
}; };
webviewView.webview.html = this.getWebviewContent(webviewView.webview); // 检查是否已登录(使用 Authentication API
this.checkLoginStatus().then((isLoggedIn) => {
webviewView.webview.html = this.getWebviewContent(
webviewView.webview,
isLoggedIn
);
});
// 处理侧边栏的消息 // 处理侧边栏的消息
webviewView.webview.onDidReceiveMessage((message) => { webviewView.webview.onDidReceiveMessage((message) => {
if (message.command === "openChat") { if (message.command === "openChat") {
vscode.commands.executeCommand("ic-coder.openChat"); vscode.commands.executeCommand("ic-coder.openChat");
} else if (message.command === "login") {
vscode.commands.executeCommand("ic-coder.login");
} }
}); });
} }
private getWebviewContent(webview: vscode.Webview): string { private getWebviewContent(
webview: vscode.Webview,
isLoggedIn: boolean
): string {
const logoUri = webview.asWebviewUri( const logoUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, "media", "ICCoder主页标志.png") vscode.Uri.joinPath(this.extensionUri, "media", "icon.png")
); );
return ` return `
@ -153,7 +211,11 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
<div class="container"> <div class="container">
<img src="${logoUri}" alt="IC Coder" width="120" /> <img src="${logoUri}" alt="IC Coder" width="120" />
<h2>欢迎使用 IC Coder</h2> <h2>欢迎使用 IC Coder</h2>
<button class="btn" onclick="openChat()">开始创作</button> ${
isLoggedIn
? '<button class="btn" onclick="openChat()">开始创作</button>'
: '<button class="btn" onclick="login()">登录账户</button>'
}
</div> </div>
<script> <script>
@ -163,6 +225,11 @@ export class ICViewProvider implements vscode.WebviewViewProvider {
vscode.postMessage({ command: 'openChat' }); vscode.postMessage({ command: 'openChat' });
} }
// 登录功能
function login() {
vscode.postMessage({ command: 'login' });
}
function generateCode(type) { function generateCode(type) {
const code = getCodeTemplate(type); const code = getCodeTemplate(type);
vscode.postMessage({ vscode.postMessage({

View File

@ -0,0 +1,162 @@
/**
* 模式选择器组件
* 提供 Agent/Ask/Auto 三种模式的选择功能
*/
/**
* 获取模式选择器的 HTML 内容
*/
export function getModeSelectorContent(): string {
return `
<div class="tooltip">
<div class="mode-select" id="modeSelect">
<div class="mode-trigger" onclick="toggleModeDropdown()">
<span class="mode-value" id="modeValue">Agent</span>
<svg class="mode-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
</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>
</div>
<span class="tooltiptext">切换模式</span>
</div>
`;
}
/**
* 获取模式选择器的样式
*/
export function getModeSelectorStyles(): string {
return `
/* 模式选择器样式 */
.mode-select {
position: relative;
user-select: none;
}
.mode-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s ease;
}
.mode-trigger:hover {
background: var(--vscode-list-hoverBackground);
}
.mode-value {
white-space: nowrap;
}
.mode-arrow {
width: 12px;
height: 12px;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.mode-select.active .mode-arrow {
transform: rotate(180deg);
}
.mode-dropdown {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 100%;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1100;
display: none;
overflow: hidden;
}
.mode-select.active .mode-dropdown {
display: block;
}
/* 模式选择器的选项样式 */
.mode-option {
padding: 6px 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);
}
`;
}
/**
* 获取模式选择器的脚本
*/
export function getModeSelectorScript(): string {
return `
// 模式选择器相关变量
let currentMode = 'agent';
// 切换模式下拉框显示/隐藏
function toggleModeDropdown() {
const modeSelect = document.getElementById('modeSelect');
const modelSelect = document.getElementById('modelSelect');
if (modeSelect) {
modeSelect.classList.toggle('active');
// 关闭模型下拉框
if (modelSelect) {
modelSelect.classList.remove('active');
}
}
}
// 选择模式
function selectMode(value, label) {
currentMode = value;
const modeValue = document.getElementById('modeValue');
if (modeValue) {
modeValue.textContent = label;
}
// 更新选中状态
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 modeSelect = document.getElementById('modeSelect');
if (modeSelect) {
modeSelect.classList.remove('active');
}
}
// 获取当前模式
function getCurrentMode() {
return currentMode;
}
// 点击外部关闭模式下拉框
document.addEventListener('click', (event) => {
const modeSelect = document.getElementById('modeSelect');
if (modeSelect && !modeSelect.contains(event.target)) {
modeSelect.classList.remove('active');
}
});
`;
}

View File

@ -0,0 +1,71 @@
/**
* 添加上下文按钮组件
*/
/**
* 获取添加上下文按钮的 HTML 内容
*/
export function getContextButtonContent(): string {
return `
<div class="tooltip">
<button class="add-context-button" onclick="handleAddContext()">
<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>
</button>
<span class="tooltiptext">添加文件或代码片段作为上下文</span>
</div>
`;
}
/**
* 获取添加上下文按钮的样式
*/
export function getContextButtonStyles(): string {
return `
/* 添加上下文按钮样式 */
.add-context-button {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(128, 128, 128, 0.2);
border: 1px solid var(--vscode-input-border);
border-radius: 6px;
color: var(--vscode-foreground);
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
}
.add-context-button:hover {
background: rgba(128, 128, 128, 0.3);
border-color: var(--vscode-focusBorder);
}
.add-context-button svg {
width: 16px;
height: 16px;
color: #409eff;
}
.add-context-label {
white-space: nowrap;
}
`;
}
/**
* 获取添加上下文按钮的脚本
*/
export function getContextButtonScript(): string {
return `
// 添加上下文处理函数
function handleAddContext() {
// 发送添加上下文请求到扩展
vscode.postMessage({ command: 'addContext' });
}
`;
}

View File

@ -0,0 +1,249 @@
/**
* 上下文压缩组件
* 提供上下文使用情况显示和压缩功能
*/
/**
* 获取上下文压缩组件的 HTML 内容
*/
export function getContextCompressContent(): string {
return `
<!-- 上下文显示 -->
<div class="context-display">
<div class="context-info" onclick="toggleContextPanel()">
<div class="database-icon">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" class="db-svg">
<!-- 数据库容器主体 - 底层灰色 -->
<path d="M870.4 57.6C780.8 19.2 652.8 0 512 0 371.2 0 243.2 19.2 153.6 57.6 51.2 102.4 0 153.6 0 211.2l0 595.2c0 57.6 51.2 115.2 153.6 153.6C243.2 1004.8 371.2 1024 512 1024c140.8 0 268.8-19.2 358.4-57.6 96-38.4 153.6-96 153.6-153.6L1024 211.2C1024 153.6 972.8 102.4 870.4 57.6L870.4 57.6zM812.8 320C729.6 352 614.4 364.8 512 364.8 403.2 364.8 294.4 352 211.2 320 115.2 294.4 70.4 256 70.4 211.2c0-38.4 51.2-76.8 140.8-108.8C294.4 76.8 403.2 64 512 64c102.4 0 217.6 19.2 300.8 44.8 89.6 32 140.8 70.4 140.8 108.8C953.6 256 908.8 294.4 812.8 320L812.8 320zM819.2 505.6C736 531.2 620.8 550.4 512 550.4c-108.8 0-217.6-19.2-307.2-44.8C115.2 473.6 64 435.2 64 396.8L64 326.4C128 352 172.8 384 243.2 396.8 326.4 416 416 428.8 512 428.8c96 0 185.6-12.8 268.8-32C851.2 384 896 352 960 326.4l0 76.8C960 435.2 908.8 473.6 819.2 505.6L819.2 505.6zM819.2 710.4c-83.2 25.6-198.4 44.8-307.2 44.8-108.8 0-217.6-19.2-307.2-44.8C115.2 684.8 64 646.4 64 601.6L64 505.6c64 32 108.8 57.6 179.2 76.8C326.4 601.6 416 614.4 512 614.4c96 0 185.6-12.8 268.8-32C851.2 563.2 896 537.6 960 505.6l0 96C960 646.4 908.8 684.8 819.2 710.4L819.2 710.4zM512 960c-108.8 0-217.6-19.2-307.2-44.8C115.2 889.6 64 851.2 64 812.8l0-96c64 32 108.8 57.6 179.2 76.8 76.8 19.2 172.8 32 262.4 32 96 0 185.6-12.8 268.8-32 76.8-19.2 121.6-44.8 185.6-76.8l0 96c0 38.4-51.2 76.8-140.8 108.8C736 947.2 614.4 960 512 960L512 960z" fill="#94a3b8" class="db-body"/>
<!-- 填充进度效果 - 从下往上填充蓝色 -->
<defs>
<mask id="fill-mask">
<rect x="0" y="0" width="1024" height="1024" id="fillRect" fill="white"/>
</mask>
</defs>
<g mask="url(#fill-mask)">
<path d="M870.4 57.6C780.8 19.2 652.8 0 512 0 371.2 0 243.2 19.2 153.6 57.6 51.2 102.4 0 153.6 0 211.2l0 595.2c0 57.6 51.2 115.2 153.6 153.6C243.2 1004.8 371.2 1024 512 1024c140.8 0 268.8-19.2 358.4-57.6 96-38.4 153.6-96 153.6-153.6L1024 211.2C1024 153.6 972.8 102.4 870.4 57.6L870.4 57.6zM812.8 320C729.6 352 614.4 364.8 512 364.8 403.2 364.8 294.4 352 211.2 320 115.2 294.4 70.4 256 70.4 211.2c0-38.4 51.2-76.8 140.8-108.8C294.4 76.8 403.2 64 512 64c102.4 0 217.6 19.2 300.8 44.8 89.6 32 140.8 70.4 140.8 108.8C953.6 256 908.8 294.4 812.8 320L812.8 320zM819.2 505.6C736 531.2 620.8 550.4 512 550.4c-108.8 0-217.6-19.2-307.2-44.8C115.2 473.6 64 435.2 64 396.8L64 326.4C128 352 172.8 384 243.2 396.8 326.4 416 416 428.8 512 428.8c96 0 185.6-12.8 268.8-32C851.2 384 896 352 960 326.4l0 76.8C960 435.2 908.8 473.6 819.2 505.6L819.2 505.6zM819.2 710.4c-83.2 25.6-198.4 44.8-307.2 44.8-108.8 0-217.6-19.2-307.2-44.8C115.2 684.8 64 646.4 64 601.6L64 505.6c64 32 108.8 57.6 179.2 76.8C326.4 601.6 416 614.4 512 614.4c96 0 185.6-12.8 268.8-32C851.2 563.2 896 537.6 960 505.6l0 96C960 646.4 908.8 684.8 819.2 710.4L819.2 710.4zM512 960c-108.8 0-217.6-19.2-307.2-44.8C115.2 889.6 64 851.2 64 812.8l0-96c64 32 108.8 57.6 179.2 76.8 76.8 19.2 172.8 32 262.4 32 96 0 185.6-12.8 268.8-32 76.8-19.2 121.6-44.8 185.6-76.8l0 96c0 38.4-51.2 76.8-140.8 108.8C736 947.2 614.4 960 512 960L512 960z" fill="#409eff" class="db-fill"/>
</g>
</svg>
</div>
<span class="context-percentage" id="contextPercentage">0%</span>
</div>
<!-- 上下文信息弹窗 -->
<div id="contextPanel" class="context-panel">
<div class="context-panel-content">
<div class="context-info-text" id="contextInfoText">
0k / 200k 已用上下文
</div>
<button class="compress-button" onclick="compressConversation()">
压缩会话
</button>
</div>
</div>
</div>
`;
}
/**
* 获取上下文压缩组件的样式
*/
export function getContextCompressStyles(): string {
return `
/* 上下文显示样式 */
.context-display {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.context-info {
display: flex;
align-items: center;
gap: 6px;
height: 40px;
background: transparent;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
color: var(--vscode-foreground);
transition: opacity 0.3s ease;
box-shadow: none;
position: relative;
overflow: hidden;
cursor: pointer;
}
.context-info:hover {
opacity: 0.8;
}
.database-icon {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
position: relative;
}
.db-svg {
width: 100%;
height: 100%;
}
.db-body {
fill: #ffffff;
}
.db-fill {
fill: #409eff;
transition: all 0.3s ease;
}
.context-percentage {
font-size: 14px;
font-weight: 500;
color: var(--vscode-foreground);
text-align: right;
}
/* 上下文信息弹窗样式 */
.context-panel {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 8px;
z-index: 1000;
animation: fadeInUp 0.2s ease-out;
display: none;
}
.context-panel.active {
display: block;
}
.context-panel::after {
content: "";
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #ffffff;
}
.context-panel-content {
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
min-width: 160px;
}
.context-info-text {
font-size: 12px;
color: #374151;
text-align: center;
margin-bottom: 8px;
white-space: nowrap;
}
.compress-button {
width: 100%;
background: linear-gradient(145deg, #3b82f6 0%, #1d4ed8 100%);
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 500;
padding: 6px 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.compress-button:hover {
background: linear-gradient(145deg, #2563eb 0%, #1e40af 100%);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.compress-button:active {
transform: translateY(0);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
`;
}
/**
* 获取上下文压缩组件的脚本
*/
export function getContextCompressScript(): string {
return `
// 上下文面板相关函数
function toggleContextPanel() {
const contextPanel = document.getElementById('contextPanel');
if (contextPanel) {
if (contextPanel.classList.contains('active')) {
contextPanel.classList.remove('active');
} else {
contextPanel.classList.add('active');
}
}
}
function compressConversation() {
// 发送压缩会话请求
vscode.postMessage({ command: 'compressConversation' });
addMessage('正在压缩会话...', 'bot');
// 关闭面板
const contextPanel = document.getElementById('contextPanel');
if (contextPanel) {
contextPanel.classList.remove('active');
}
}
function updateContextDisplay(currentTokens, maxTokens) {
const percentage = Math.min(Math.round((currentTokens / maxTokens) * 100), 100);
// 更新百分比显示
const contextPercentage = document.getElementById('contextPercentage');
if (contextPercentage) {
contextPercentage.textContent = percentage + '%';
}
// 更新详细信息
const contextInfoText = document.getElementById('contextInfoText');
if (contextInfoText) {
const currentK = Math.round((currentTokens / 1000) * 10) / 10;
const maxK = Math.round(maxTokens / 1000);
contextInfoText.textContent = \`\${currentK}k / \${maxK}k 已用上下文\`;
}
// 更新SVG填充效果从下往上填充
const fillRect = document.getElementById('fillRect');
if (fillRect) {
const fillHeight = (1024 * percentage) / 100;
const fillY = 1024 - fillHeight;
fillRect.setAttribute('y', fillY.toString());
fillRect.setAttribute('height', fillHeight.toString());
}
}
// 点击外部关闭上下文面板
document.addEventListener('click', (event) => {
const contextDisplay = document.querySelector('.context-display');
const contextPanel = document.getElementById('contextPanel');
if (contextPanel && contextPanel.classList.contains('active') && contextDisplay) {
if (!contextDisplay.contains(event.target)) {
contextPanel.classList.remove('active');
}
}
});
`;
}

View File

@ -0,0 +1,401 @@
/**
* 获取会话历史栏的 HTML 内容
*/
export function getConversationHistoryBarContent(): string {
return `
<div class="conversation-history-bar">
<div class="history-dropdown-container">
<button class="history-dropdown-button" onclick="toggleHistoryDropdown()">
<span class="dropdown-label">Past Conversations</span>
<svg class="dropdown-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z" fill="currentColor"/>
</svg>
</button>
<div class="history-dropdown-menu" id="historyDropdownMenu">
<div class="history-list" id="historyList">
<!-- 会话历史列表将在这里动态生成 -->
</div>
</div>
</div>
<button class="new-conversation-button" onclick="createNewConversation()" title="新建对话">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor"/>
</svg>
</button>
</div>
`;
}
/**
* 获取会话历史栏的 CSS 样式
*/
export function getConversationHistoryBarStyles(): string {
return `
.conversation-history-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--vscode-tab-activeBackground);
border-bottom: 1px solid var(--vscode-panel-border);
flex-shrink: 0;
min-height: 35px;
}
.history-dropdown-container {
position: relative;
flex: 1;
}
.history-dropdown-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
}
.history-dropdown-button:hover {
opacity: 0.8;
}
.dropdown-label {
white-space: nowrap;
}
.dropdown-icon {
width: 12px;
height: 12px;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.history-dropdown-button.active .dropdown-icon {
transform: rotate(180deg);
}
.history-dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 300px;
max-height: 400px;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 400px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.history-dropdown-menu.active {
display: block;
}
.history-list {
padding: 4px 0;
}
.history-item {
padding: 10px 16px;
cursor: pointer;
transition: background 0.2s ease;
border-bottom: 1px solid var(--vscode-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.history-item:last-child {
border-bottom: none;
}
.history-item:hover {
background: var(--vscode-list-hoverBackground);
}
.history-item-title {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.history-item-time {
font-size: 12px;
opacity: 0.7;
white-space: nowrap;
flex-shrink: 0;
}
.history-empty {
padding: 20px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-size: 14px;
}
.history-load-more {
padding: 12px 16px;
text-align: center;
color: var(--vscode-descriptionForeground);
font-size: 12px;
border-top: 1px solid var(--vscode-panel-border);
}
.new-conversation-button {
width: 36px;
height: 36px;
padding: 0;
background: transparent;
color: var(--vscode-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.new-conversation-button:hover {
opacity: 0.7;
}
.new-conversation-button:active {
opacity: 0.5;
}
.new-conversation-button svg {
width: 20px;
height: 20px;
}
/* 滚动条样式 */
.history-dropdown-menu::-webkit-scrollbar {
width: 8px;
}
.history-dropdown-menu::-webkit-scrollbar-track {
background: transparent;
}
.history-dropdown-menu::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.5);
border-radius: 4px;
}
.history-dropdown-menu::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.7);
}
`;
}
/**
* 获取会话历史栏的 JavaScript 脚本
*/
export function getConversationHistoryBarScript(): string {
return `
// 会话历史相关变量
let conversationHistory = [];
let currentConversationId = null;
let currentOffset = 0;
let totalHistory = 0;
let hasMoreHistory = false;
let isLoadingHistory = false;
const HISTORY_PAGE_SIZE = 10;
const MAX_HISTORY_ITEMS = 100;
// 切换历史记录下拉菜单
function toggleHistoryDropdown() {
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
if (menu.classList.contains('active')) {
menu.classList.remove('active');
button.classList.remove('active');
} else {
menu.classList.add('active');
button.classList.add('active');
// 重置并加载会话历史
resetAndLoadHistory();
}
}
// 重置并加载会话历史
function resetAndLoadHistory() {
conversationHistory = [];
currentOffset = 0;
totalHistory = 0;
hasMoreHistory = false;
const historyList = document.getElementById('historyList');
if (historyList) {
historyList.innerHTML = '<div class="history-empty">加载中...</div>';
}
loadMoreHistory();
}
// 加载更多会话历史
function loadMoreHistory() {
if (isLoadingHistory || (currentOffset > 0 && !hasMoreHistory)) {
return;
}
// 检查是否已达到最大数量
if (currentOffset >= MAX_HISTORY_ITEMS) {
return;
}
isLoadingHistory = true;
vscode.postMessage({
command: 'loadConversationHistory',
offset: currentOffset,
limit: HISTORY_PAGE_SIZE
});
}
// 渲染会话历史列表(支持追加)
function renderConversationHistory(data) {
isLoadingHistory = false;
if (!data || !data.items) {
return;
}
// 追加新数据
conversationHistory = conversationHistory.concat(data.items);
totalHistory = data.total;
hasMoreHistory = data.hasMore;
currentOffset += data.items.length;
const historyList = document.getElementById('historyList');
if (!historyList) {
return;
}
// 如果没有任何历史记录
if (conversationHistory.length === 0) {
historyList.innerHTML = '<div class="history-empty">暂无会话历史</div>';
return;
}
// 渲染所有历史记录
historyList.innerHTML = conversationHistory.map(item => \`
<div class="history-item" onclick="selectConversation('\${item.id}')">
<div class="history-item-title">\${item.title || '未命名会话'}</div>
<div class="history-item-time">\${formatTime(item.timestamp)}</div>
</div>
\`).join('');
// 如果还有更多数据,添加"加载更多"提示
if (hasMoreHistory && currentOffset < MAX_HISTORY_ITEMS) {
historyList.innerHTML += \`
<div class="history-load-more" id="loadMoreIndicator">
<span>滚动加载更多...</span>
</div>
\`;
} else if (currentOffset >= MAX_HISTORY_ITEMS && hasMoreHistory) {
historyList.innerHTML += \`
<div class="history-load-more">
<span>已显示最近 \${MAX_HISTORY_ITEMS} 条记录</span>
</div>
\`;
}
}
// 选择会话
function selectConversation(conversationId) {
currentConversationId = conversationId;
vscode.postMessage({
command: 'selectConversation',
conversationId: conversationId
});
// 关闭下拉菜单
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
menu.classList.remove('active');
button.classList.remove('active');
}
// 创建新会话
function createNewConversation() {
vscode.postMessage({ command: 'createNewConversation' });
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
// 小于1分钟
if (diff < 60000) {
return '刚刚';
}
// 小于1小时
if (diff < 3600000) {
return Math.floor(diff / 60000) + '分钟前';
}
// 小于1天
if (diff < 86400000) {
return Math.floor(diff / 3600000) + '小时前';
}
// 小于7天
if (diff < 604800000) {
return Math.floor(diff / 86400000) + '天前';
}
// 超过7天显示具体日期
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 监听下拉菜单滚动事件
const historyDropdownMenu = document.getElementById('historyDropdownMenu');
if (historyDropdownMenu) {
historyDropdownMenu.addEventListener('scroll', () => {
const menu = historyDropdownMenu;
const scrollTop = menu.scrollTop;
const scrollHeight = menu.scrollHeight;
const clientHeight = menu.clientHeight;
// 当滚动到距离底部 50px 时,加载更多
if (scrollHeight - scrollTop - clientHeight < 50) {
loadMoreHistory();
}
});
}
// 点击外部关闭下拉菜单
document.addEventListener('click', (event) => {
const container = document.querySelector('.history-dropdown-container');
const menu = document.getElementById('historyDropdownMenu');
const button = document.querySelector('.history-dropdown-button');
if (menu && menu.classList.contains('active')) {
if (!container.contains(event.target)) {
menu.classList.remove('active');
button.classList.remove('active');
}
}
});
`;
}

346
src/views/inputArea.ts Normal file
View File

@ -0,0 +1,346 @@
import { getWaveformPreviewContent } from "./waveformPreviewContent";
import {
getModelSelectorContent,
getModelSelectorStyles,
getModelSelectorScript
} from "./modelSelector";
import {
getModeSelectorContent,
getModeSelectorStyles,
getModeSelectorScript
} from "./agentModeSelector";
import {
getContextButtonContent,
getContextButtonStyles,
getContextButtonScript
} from "./contextButton";
import {
getContextCompressContent,
getContextCompressStyles,
getContextCompressScript
} from "./contextCompress";
import {
getPlanToggleContent,
getPlanToggleStyles,
getPlanToggleScript
} from "./planToggle";
import {
getOptimizeButtonContent,
getOptimizeButtonStyles,
getOptimizeButtonScript
} from "./optimizeButton";
import {
sendIconSvg,
stopIconSvg
} from "../constants/toolIcons";
/**
* 获取输入区域的 HTML 内容
*/
export function getInputAreaContent(): string {
return `
<div class="input-area">
<div class="input-group">
<div class="input-wrapper">
<!-- 顶部工具栏 -->
<div class="input-top-toolbar">
${getContextButtonContent()}
${getPlanToggleContent()}
</div>
<textarea
id="messageInput"
placeholder="输入您的问题..."
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea>
<div class="input-bottom-row">
<div class="mode-selector">
${getModeSelectorContent()}
${getModelSelectorContent()}
</div>
<div class="input-actions">
${getContextCompressContent()}
${getOptimizeButtonContent()}
<button id="sendButton" onclick="handleSendOrStop()">
${sendIconSvg}
<span style="display: none;">${stopIconSvg}</span>
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
/**
* 获取输入区域的样式
*/
export function getInputAreaStyles(): string {
return `
${getModeSelectorStyles()}
${getModelSelectorStyles()}
${getContextButtonStyles()}
${getContextCompressStyles()}
${getPlanToggleStyles()}
${getOptimizeButtonStyles()}
.input-area {
border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px;
flex-shrink: 0;
}
.input-group {
display: flex;
flex-direction: column;
gap: 10px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.input-group:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2), 0 3px 8px rgba(0, 0, 0, 0.15);
}
.input-group:focus-within {
border-color: var(--vscode-focusBorder);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25), 0 3px 10px rgba(0, 0, 0, 0.2);
}
.input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
/* 顶部工具栏样式 */
.input-top-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
gap: 12px;
}
.input-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: -17px;
}
.mode-selector {
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.input-actions {
display: flex;
align-items: center;
gap: 10px;
}
/* Tooltip 样式 */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: auto;
background: #1e1e1e;
color: #ffffff;
text-align: center;
border-radius: 6px;
padding: 6px 12px;
position: absolute;
z-index: 1000;
bottom: 150%;
left: 50%;
transform: translateX(-50%) translateY(10px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
font-size: 12px;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3);
white-space: nowrap;
letter-spacing: 0.3px;
}
.tooltip .tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -6px;
border-width: 6px;
border-style: solid;
border-color: #1e1e1e transparent transparent transparent;
}
.tooltip .tooltiptext::before {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -7px;
border-width: 7px;
border-style: solid;
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
z-index: -1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
transform: translateX(-50%) translateY(0);
}
textarea {
width: 100%;
padding: 10px;
background: transparent;
color: var(--vscode-input-foreground);
border: none;
border-radius: 4px;
font-family: inherit;
resize: none;
min-height: 40px;
max-height: 200px;
outline: none;
box-sizing: border-box;
overflow-y: auto;
line-height: 1.5;
}
/* 简洁的滚动条样式 */
textarea::-webkit-scrollbar {
width: 8px;
}
textarea::-webkit-scrollbar-track {
background: transparent;
}
textarea::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.5);
border-radius: 4px;
}
textarea::-webkit-scrollbar-button {
display: none;
}
button {
padding: 0 20px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
button:hover {
background: var(--vscode-button-hoverBackground);
}
/* 发送按钮状态样式 */
#sendButton {
position: relative;
min-width: 32px;
padding: 6px 8px;
}
#sendButton svg {
width: 14px;
height: 14px;
display: block;
}
#sendButton.sending {
background: var(--vscode-button-background);
}
#sendButton.sending:hover {
background: var(--vscode-button-hoverBackground);
}
`;
}
/**
* 获取输入区域的脚本
*/
export function getInputAreaScript(): string {
return `
${getModeSelectorScript()}
${getModelSelectorScript()}
${getContextButtonScript()}
${getContextCompressScript()}
${getPlanToggleScript()}
${getOptimizeButtonScript()}
// 对话状态管理
let isConversationActive = false;
// 自动调整 textarea 高度
function autoResizeTextarea() {
if (messageInput) {
messageInput.style.height = 'auto';
messageInput.style.height = messageInput.scrollHeight + 'px';
}
}
// 监听输入事件,自动调整高度
if (messageInput) {
messageInput.addEventListener('input', autoResizeTextarea);
// 初始化时调整一次高度
autoResizeTextarea();
// 聚焦到输入框
messageInput.focus();
}
// 切换发送按钮状态
function setSendButtonState(isSending) {
const sendButton = document.getElementById('sendButton');
const children = sendButton.children;
const sendIconContainer = children[0]; // 第一个子元素是发送图标的 SVG
const stopIconContainer = children[1]; // 第二个子元素是包含暂停图标的 span
if (isSending) {
sendButton.classList.add('sending');
sendIconContainer.style.display = 'none';
stopIconContainer.style.display = 'block';
isConversationActive = true;
} else {
sendButton.classList.remove('sending');
sendIconContainer.style.display = 'block';
stopIconContainer.style.display = 'none';
isConversationActive = false;
}
}
// 处理发送或停止
function handleSendOrStop() {
if (isConversationActive) {
// 当前正在对话,执行停止操作
vscode.postMessage({ command: 'abortDialog' });
setSendButtonState(false);
} else {
// 当前未在对话,执行发送操作
sendMessage();
}
}
function sendMessage() {
const text = messageInput.value.trim();
if (!text) return;
const mode = getCurrentMode(); // 从模式选择器组件获取当前模式
const model = getCurrentModel(); // 从模型选择器组件获取当前模型
addMessage(text, 'user');
// 切换按钮为暂停状态
setSendButtonState(true);
vscode.postMessage({ command: 'sendMessage', text: text, mode: mode, model: model });
messageInput.value = '';
autoResizeTextarea(); // 重置输入框高度
messageInput.focus();
// 重置优化状态
resetOptimizeButton();
}
`;
}

1279
src/views/messageArea.ts Normal file

File diff suppressed because it is too large Load Diff

250
src/views/modelSelector.ts Normal file
View File

@ -0,0 +1,250 @@
/**
* 模型选择器组件
*/
/**
* 获取模型选择器的 HTML 内容
*/
export function getModelSelectorContent(): string {
return `
<!-- 模型选择 -->
<div class="tooltip">
<div class="custom-select" id="modelSelect">
<div class="select-trigger" onclick="toggleModelDropdown()">
<span class="select-value" id="modelValue">Auto</span>
<svg class="select-arrow" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M507.8 727.728a30.016 30.016 0 0 1-21.288-8.824L231.104 463.496a30.088 30.088 0 0 1 0-42.568 30.088 30.088 0 0 1 42.568 0l234.128 234.128 234.16-234.128a30.088 30.088 0 0 1 42.568 0 30.088 30.088 0 0 1 0 42.568L529.08 718.904a30 30 0 0 1-21.28 8.824z" fill="#8a8a8a"/>
</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>
<!-- 模型选择器的 tooltip 容器 -->
<div id="modelTooltip" class="model-tooltip"></div>
</div>
<span class="tooltiptext">选择模型</span>
</div>
`;
}
/**
* 获取模型选择器的样式
*/
export function getModelSelectorStyles(): string {
return `
/* 自定义下拉框样式 */
.custom-select {
position: relative;
user-select: none;
}
.select-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--vscode-input-background);
color: var(--vscode-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s ease;
}
.select-trigger:hover {
background: var(--vscode-list-hoverBackground);
}
.select-value {
white-space: nowrap;
}
.select-arrow {
width: 12px;
height: 12px;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.custom-select.active .select-arrow {
transform: rotate(180deg);
}
.select-dropdown {
position: absolute;
bottom: calc(100% + 2px);
left: 0;
min-width: 100%;
background: var(--vscode-dropdown-background);
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1100;
display: none;
overflow: visible;
}
.custom-select.active .select-dropdown {
display: block;
}
/* 模型选择器的选项样式 */
#modelDropdown .select-option {
position: relative;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s ease;
white-space: nowrap;
}
#modelDropdown .select-option:hover {
background: rgba(128, 128, 128, 0.3);
}
#modelDropdown .select-option.selected {
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;
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;
}
`;
}
/**
* 获取模型选择器的脚本
*/
export function getModelSelectorScript(): string {
return `
// 模型选择相关变量
let currentModel = 'auto';
// 切换模型下拉框显示/隐藏
function toggleModelDropdown() {
const modelSelect = document.getElementById('modelSelect');
const customSelect = document.getElementById('customSelect');
if (modelSelect) {
modelSelect.classList.toggle('active');
// 关闭模式下拉框
if (customSelect) {
customSelect.classList.remove('active');
}
}
}
// 选择模型
function selectModel(value, label) {
currentModel = value;
const modelValue = document.getElementById('modelValue');
if (modelValue) {
modelValue.textContent = label;
}
// 更新选中状态
const options = document.querySelectorAll('#modelDropdown .select-option');
options.forEach(option => {
if (option.getAttribute('data-value') === value) {
option.classList.add('selected');
} else {
option.classList.remove('selected');
}
});
// 关闭下拉框
const modelSelect = document.getElementById('modelSelect');
if (modelSelect) {
modelSelect.classList.remove('active');
}
}
// 点击外部关闭模型下拉框
document.addEventListener('click', (event) => {
const modelSelect = document.getElementById('modelSelect');
if (modelSelect && !modelSelect.contains(event.target)) {
modelSelect.classList.remove('active');
}
});
// 获取当前选中的模型
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');
});
});
})();
`;
}

117
src/views/optimizeButton.ts Normal file
View File

@ -0,0 +1,117 @@
/**
* 一键优化按钮组件
*/
/**
* 获取一键优化按钮的 HTML 内容
*/
export function getOptimizeButtonContent(): string {
return `
<!-- 一键优化按钮 -->
<div class="tooltip">
<button id="optimizeButton" class="optimize-button" onclick="handleOptimize()">
<svg t="1765867478136" id="optimizeIcon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2314"><path d="M490.048929 399.773864c7.042381-21.120144 36.85976-21.120144 43.902142 0l41.273372 123.957105A184.967743 184.967743 0 0 0 692.274156 640.713687l123.890111 41.273373c21.119144 7.042381 21.119144 36.85976 0 43.902141L692.207161 767.162574A184.967743 184.967743 0 0 0 575.224443 884.212286l-41.273372 123.890111A23.09997 23.09997 0 0 1 512 1024c-9.983123 0-18.838344-6.409437-21.951071-15.897603L448.775557 884.145292A184.946745 184.946745 0 0 0 331.792839 767.162574L207.836733 725.889201A23.09997 23.09997 0 0 1 191.93813 703.93813c0-9.983123 6.409437-18.838344 15.897603-21.95107l123.957106-41.273373A184.946745 184.946745 0 0 0 448.775557 523.730969zM242.840657 73.466543A13.888779 13.888779 0 0 1 256.022498 63.94338c5.987474 0 11.299007 3.839663 13.182841 9.523163l24.767824 74.360464a111.070238 111.070238 0 0 0 70.19983 70.20083l74.360464 24.767824A13.888779 13.888779 0 0 1 448.05662 255.977502c0 5.987474-3.839663 11.299007-9.523163 13.182841l-74.360464 24.767823a110.947249 110.947249 0 0 0-70.20083 70.199831l-24.767824 74.360464A13.888779 13.888779 0 0 1 256.022498 448.011624a13.888779 13.888779 0 0 1-13.182841-9.523163l-24.767823-74.360464a110.947249 110.947249 0 0 0-70.199831-70.20083l-74.360464-24.767824A13.888779 13.888779 0 0 1 63.988376 255.977502c0-5.987474 3.839663-11.299007 9.523163-13.182841l74.360464-24.767824a110.947249 110.947249 0 0 0 70.20083-70.19983zM695.213897 6.335443a9.283184 9.283184 0 0 1 17.538459 0L729.260905 55.86509a73.889506 73.889506 0 0 0 46.843883 46.843883l49.530646 16.509549a9.283184 9.283184 0 0 1 0 17.538458L776.106787 153.266529a73.9585 73.9585 0 0 0-46.843882 46.843883l-16.509549 49.530647a9.283184 9.283184 0 0 1-17.538459 0L678.705348 200.112412a73.9585 73.9585 0 0 0-46.843883-46.843883l-49.468652-16.509549a9.283184 9.283184 0 0 1 0-17.538458l49.535646-16.509549a73.897505 73.897505 0 0 0 46.842883-46.843883L695.213897 6.397438z m0 0" p-id="2315" fill="#409eff"></path></svg>
</button>
<span class="tooltiptext" id="optimizeTooltip">一键优化</span>
</div>
`;
}
/**
* 获取一键优化按钮的样式
*/
export function getOptimizeButtonStyles(): string {
return `
/* 一键优化按钮样式 */
.optimize-button {
padding: 8px;
background: transparent;
color: var(--vscode-foreground);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s ease;
width: 32px;
height: 32px;
}
.optimize-button:hover {
opacity: 0.7;
}
.optimize-button svg {
width: 16px;
height: 16px;
}
.optimize-button-wrapper {
display: flex;
align-items: flex-end;
}
`;
}
/**
* 获取一键优化按钮的脚本
*/
export function getOptimizeButtonScript(): string {
return `
let isOptimized = false; // 标记是否已优化
let originalText = ''; // 保存原始文本用于撤回
function handleOptimize() {
if (isOptimized) {
// 撤回操作
messageInput.value = originalText;
resetOptimizeButton();
} else {
// 优化操作
originalText = messageInput.value; // 保存原始文本
// 使用死数据替换输入框内容
const optimizedTexts = [
'请帮我优化这段代码,提高性能和可读性',
'请分析这个问题并给出最佳解决方案',
'请帮我重构这段代码,使其更加简洁高效',
'请检查代码中的潜在问题并提供改进建议'
];
const randomText = optimizedTexts[Math.floor(Math.random() * optimizedTexts.length)];
messageInput.value = randomText;
// 切换到撤回状态
isOptimized = true;
updateOptimizeButton();
}
messageInput.focus();
autoResizeTextarea();
}
function updateOptimizeButton() {
const optimizeIcon = document.getElementById('optimizeIcon');
const optimizeTooltip = document.getElementById('optimizeTooltip');
if (optimizeIcon && optimizeTooltip) {
// 切换为撤回图标
optimizeIcon.innerHTML = '<path d="M581.056 288.32H232.96l108.352-102.208c15.552-15.744 19.456-31.104 4.16-46.208-16.064-15.872-32.576-15.808-48.64 0l-145.92 144.768c-8.768 8.832-23.488 20.608-22.08 32.448l0.64 4.8-0.64 4.864c-1.344 11.776 6.4 18.24 14.848 26.816l147.648 145.216c16.064 15.808 38.08 20.992 54.144 5.12 15.296-15.104 3.84-38.208-11.328-53.504L233.152 353.6 581.056 352c126.464 0 250.944 111.488 250.944 236.16C832 712.832 707.52 832 581.056 832H246.4c-22.592 0-29.696 9.6-29.696 32.256s7.04 31.744 29.696 31.744H581.12C755.136 896 896 757.696 896 588.16c0-169.408-140.8-299.84-314.944-299.84z" fill="currentColor"/><path d="M323.392 192a32 32 0 1 1 0-64 32 32 0 0 1 0 64zM320.192 514.048a32 32 0 1 1 0-64 32 32 0 0 1 0 64zM237.824 896a32 32 0 1 1 0-64 32 32 0 0 1 0 64z" fill="currentColor"/>';
optimizeTooltip.textContent = '撤回';
}
}
function resetOptimizeButton() {
const optimizeIcon = document.getElementById('optimizeIcon');
const optimizeTooltip = document.getElementById('optimizeTooltip');
if (optimizeIcon && optimizeTooltip) {
// 切换回优化图标(星星图标)
optimizeIcon.innerHTML = '<path d="M490.048929 399.773864c7.042381-21.120144 36.85976-21.120144 43.902142 0l41.273372 123.957105A184.967743 184.967743 0 0 0 692.274156 640.713687l123.890111 41.273373c21.119144 7.042381 21.119144 36.85976 0 43.902141L692.207161 767.162574A184.967743 184.967743 0 0 0 575.224443 884.212286l-41.273372 123.890111A23.09997 23.09997 0 0 1 512 1024c-9.983123 0-18.838344-6.409437-21.951071-15.897603L448.775557 884.145292A184.946745 184.946745 0 0 0 331.792839 767.162574L207.836733 725.889201A23.09997 23.09997 0 0 1 191.93813 703.93813c0-9.983123 6.409437-18.838344 15.897603-21.95107l123.957106-41.273373A184.946745 184.946745 0 0 0 448.775557 523.730969zM242.840657 73.466543A13.888779 13.888779 0 0 1 256.022498 63.94338c5.987474 0 11.299007 3.839663 13.182841 9.523163l24.767824 74.360464a111.070238 111.070238 0 0 0 70.19983 70.20083l74.360464 24.767824A13.888779 13.888779 0 0 1 448.05662 255.977502c0 5.987474-3.839663 11.299007-9.523163 13.182841l-74.360464 24.767823a110.947249 110.947249 0 0 0-70.20083 70.199831l-24.767824 74.360464A13.888779 13.888779 0 0 1 256.022498 448.011624a13.888779 13.888779 0 0 1-13.182841-9.523163l-24.767823-74.360464a110.947249 110.947249 0 0 0-70.199831-70.20083l-74.360464-24.767824A13.888779 13.888779 0 0 1 63.988376 255.977502c0-5.987474 3.839663-11.299007 9.523163-13.182841l74.360464-24.767824a110.947249 110.947249 0 0 0 70.20083-70.19983zM695.213897 6.335443a9.283184 9.283184 0 0 1 17.538459 0L729.260905 55.86509a73.889506 73.889506 0 0 0 46.843883 46.843883l49.530646 16.509549a9.283184 9.283184 0 0 1 0 17.538458L776.106787 153.266529a73.9585 73.9585 0 0 0-46.843882 46.843883l-16.509549 49.530647a9.283184 9.283184 0 0 1-17.538459 0L678.705348 200.112412a73.9585 73.9585 0 0 0-46.843883-46.843883l-49.468652-16.509549a9.283184 9.283184 0 0 1 0-17.538458l49.535646-16.509549a73.897505 73.897505 0 0 0 46.842883-46.843883L695.213897 6.397438z m0 0" fill="#409eff"/>';
optimizeTooltip.textContent = '一键优化';
}
isOptimized = false;
originalText = '';
}
`;
}

100
src/views/planToggle.ts Normal file
View File

@ -0,0 +1,100 @@
/**
* Plan 开关组件
*/
/**
* 获取 Plan 开关的 HTML 内容
*/
export function getPlanToggleContent(): string {
return `
<div class="tooltip">
<label class="plan-toggle">
<input type="checkbox" id="planToggle" onchange="handlePlanToggle()">
<span class="plan-toggle-slider"></span>
<span class="plan-toggle-label">Plan</span>
</label>
<span class="tooltiptext" id="planTooltip">启用 Plan 模式</span>
</div>
`;
}
/**
* 获取 Plan 开关的样式
*/
export function getPlanToggleStyles(): string {
return `
/* Plan 开关样式 */
.plan-toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.plan-toggle input[type="checkbox"] {
display: none;
}
.plan-toggle-slider {
position: relative;
width: 36px;
height: 20px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 10px;
transition: all 0.3s ease;
}
.plan-toggle-slider::before {
content: "";
position: absolute;
width: 14px;
height: 14px;
left: 2px;
top: 2px;
background: var(--vscode-foreground);
border-radius: 50%;
transition: all 0.3s ease;
}
.plan-toggle input[type="checkbox"]:checked + .plan-toggle-slider {
background: #409eff;
border-color: #409eff;
}
.plan-toggle input[type="checkbox"]:checked + .plan-toggle-slider::before {
transform: translateX(16px);
background: white;
}
.plan-toggle-label {
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
`;
}
/**
* 获取 Plan 开关的脚本
*/
export function getPlanToggleScript(): string {
return `
// Plan 开关处理函数
function handlePlanToggle() {
const planToggle = document.getElementById('planToggle');
const planTooltip = document.getElementById('planTooltip');
if (planToggle && planTooltip) {
if (planToggle.checked) {
// 开启 Plan 模式
planTooltip.textContent = '关闭 Plan 模式';
} else {
// 关闭 Plan 模式
planTooltip.textContent = '启用 Plan 模式';
}
}
}
`;
}

View File

@ -0,0 +1,350 @@
/**
* 获取波形预览组件的样式内容(纯 CSS不包含 style 标签)
*/
export function getWaveformPreviewContent(): string {
return `
/* 波形预览组件样式 */
.waveform-preview {
margin-top: 12px;
border: 1px solid var(--vscode-panel-border);
border-radius: 8px;
overflow: hidden;
background: var(--vscode-editor-background);
}
.waveform-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: var(--vscode-input-background);
border-bottom: 1px solid var(--vscode-panel-border);
}
.waveform-preview-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 500;
color: var(--vscode-foreground);
}
.waveform-preview-title svg {
width: 16px;
height: 16px;
color: var(--vscode-button-background);
}
.waveform-expand-btn {
padding: 4px 12px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
transition: opacity 0.2s ease;
}
.waveform-expand-btn:hover {
opacity: 0.9;
}
.waveform-expand-btn svg {
width: 14px;
height: 14px;
}
.waveform-preview-content {
padding: 0;
min-height: 200px;
max-height: 300px;
overflow: hidden;
position: relative;
background: var(--vscode-editor-background);
}
.waveform-preview-canvas {
width: 100%;
height: 100%;
min-height: 200px;
}
.waveform-preview-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--vscode-descriptionForeground);
font-size: 13px;
text-align: center;
padding: 20px;
}
.waveform-preview-placeholder svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.waveform-info {
margin-top: 8px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
}
.waveform-mini-viewer {
width: 100%;
height: 200px;
background: var(--vscode-editor-background);
position: relative;
overflow: hidden;
}
.waveform-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--vscode-descriptionForeground);
font-size: 12px;
}
`;
}
/**
* 获取波形预览组件的 JavaScript 代码
*/
export function getWaveformPreviewScript(): string {
return `
/**
* 创建波形预览组件
*/
function createWaveformPreview(vcdFilePath, fileName) {
const previewDiv = document.createElement('div');
previewDiv.className = 'waveform-preview';
// 头部
const header = document.createElement('div');
header.className = 'waveform-preview-header';
const title = document.createElement('div');
title.className = 'waveform-preview-title';
title.innerHTML = \`
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M128 512h128l64-128 64 128 64-256 64 384 64-128h320"
stroke="currentColor"
stroke-width="64"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
<span>波形预览 - \${fileName}</span>
\`;
const expandBtn = document.createElement('button');
expandBtn.className = 'waveform-expand-btn';
expandBtn.innerHTML = \`
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M342 88.3h340c56.6 0 102.6 46 102.6 102.6v340c0 56.6-46 102.6-102.6 102.6H342c-56.6 0-102.6-46-102.6-102.6v-340c0-56.6 46-102.6 102.6-102.6z"
fill="none"
stroke="currentColor"
stroke-width="48"/>
<path d="M239.4 390.5v340c0 56.6 46 102.6 102.6 102.6h340"
fill="none"
stroke="currentColor"
stroke-width="48"
stroke-linecap="round"/>
</svg>
展开查看
\`;
expandBtn.onclick = () => openFullWaveform(vcdFilePath);
header.appendChild(title);
header.appendChild(expandBtn);
// 内容区域 - 创建一个唯一ID的容器用于显示波形
const content = document.createElement('div');
content.className = 'waveform-preview-content';
const miniViewerId = 'waveform-mini-' + Date.now();
const miniViewer = document.createElement('div');
miniViewer.id = miniViewerId;
miniViewer.className = 'waveform-mini-viewer';
// 添加加载提示
const loadingDiv = document.createElement('div');
loadingDiv.className = 'waveform-loading';
loadingDiv.textContent = '正在加载波形预览...';
miniViewer.appendChild(loadingDiv);
content.appendChild(miniViewer);
previewDiv.appendChild(header);
previewDiv.appendChild(content);
// 异步加载波形数据
loadMiniWaveform(miniViewerId, vcdFilePath, loadingDiv);
return previewDiv;
}
/**
* 加载迷你波形预览
*/
async function loadMiniWaveform(containerId, vcdFilePath, loadingDiv) {
try {
// 请求 VCD 文件信息
vscode.postMessage({
command: 'getVCDInfo',
vcdFilePath: vcdFilePath,
containerId: containerId
});
} catch (error) {
console.error('加载波形预览失败:', error);
loadingDiv.textContent = '波形预览加载失败';
loadingDiv.style.color = 'var(--vscode-errorForeground)';
}
}
/**
* 渲染波形预览信息
*/
function renderWaveformInfo(containerId, vcdInfo) {
const container = document.getElementById(containerId);
if (!container) return;
// 清空容器
container.innerHTML = '';
// 绘制真实波形
const waveformSvg = document.createElement('div');
waveformSvg.innerHTML = drawRealWaveform(vcdInfo.signals || []);
container.appendChild(waveformSvg);
}
/**
* 绘制真实波形
*/
function drawRealWaveform(signals) {
if (!signals || signals.length === 0) {
return \`
<svg width="100%" height="80" viewBox="0 0 800 80" style="background: var(--vscode-editor-background);">
<text x="400" y="40" fill="var(--vscode-descriptionForeground)" font-size="12" text-anchor="middle">
无波形数据
</text>
</svg>
\`;
}
const svgWidth = 800;
const svgHeight = Math.max(80, signals.length * 30 + 20);
const signalHeight = 20;
const signalSpacing = 30;
const leftMargin = 80;
const rightMargin = 20;
const waveformWidth = svgWidth - leftMargin - rightMargin;
const colors = ['var(--vscode-charts-blue)', 'var(--vscode-charts-green)', 'var(--vscode-charts-orange)'];
let svgContent = \`<svg width="100%" height="\${svgHeight}" viewBox="0 0 \${svgWidth} \${svgHeight}" style="background: var(--vscode-editor-background);">\`;
// 绘制每个信号
signals.forEach((signal, index) => {
const y = 10 + index * signalSpacing;
const color = colors[index % colors.length];
// 绘制信号名称
svgContent += \`<text x="5" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-foreground)" font-size="10" opacity="0.8">\${signal.name}</text>\`;
// 如果没有值变化数据,显示提示
if (!signal.values || signal.values.length === 0) {
svgContent += \`<text x="\${leftMargin + waveformWidth / 2}" y="\${y + signalHeight / 2 + 4}" fill="var(--vscode-descriptionForeground)" font-size="9" text-anchor="middle" opacity="0.5">无数据</text>\`;
return;
}
// 计算时间范围
const times = signal.values.map(v => v.time);
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const timeRange = maxTime - minTime || 1;
// 绘制波形
let pathData = '';
let lastX = leftMargin;
let lastValue = signal.values[0].value;
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;
if (i === 0) {
pathData = \`M \${x} \${currentY}\`;
} else {
// 绘制垂直跳变
const prevY = (lastValue === '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);
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"/>\`;
});
// 绘制时间轴
const timeAxisY = svgHeight - 5;
svgContent += \`<line x1="\${leftMargin}" y1="\${timeAxisY}" x2="\${leftMargin + waveformWidth}" y2="\${timeAxisY}" stroke="var(--vscode-foreground)" stroke-width="1" opacity="0.2"/>\`;
svgContent += \`</svg>\`;
return svgContent;
}
/**
* 打开完整波形查看器
*/
function openFullWaveform(vcdFilePath) {
vscode.postMessage({
command: 'openWaveformViewer',
vcdFilePath: vcdFilePath
});
}
/**
* 在消息中添加波形预览
*/
function addWaveformPreviewToMessage(messageDiv, vcdFilePath, fileName) {
const preview = createWaveformPreview(vcdFilePath, fileName);
messageDiv.appendChild(preview);
}
`;
}

View File

@ -1,3 +1,22 @@
import {
getWaveformPreviewContent,
getWaveformPreviewScript,
} from "./waveformPreviewContent";
import {
getConversationHistoryBarContent,
getConversationHistoryBarStyles,
getConversationHistoryBarScript,
} from "./conversationHistoryBar";
import {
getInputAreaContent,
getInputAreaStyles,
getInputAreaScript,
} from "./inputArea";
import {
getMessageAreaContent,
getMessageAreaStyles,
getMessageAreaScript,
} from "./messageArea";
/** /**
* 获取 WebView 面板的 HTML 内容 * 获取 WebView 面板的 HTML 内容
*/ */
@ -14,16 +33,24 @@ export function getWebviewContent(iconUri?: string): string {
background: var(--vscode-editor-background); background: var(--vscode-editor-background);
color: var(--vscode-foreground); color: var(--vscode-foreground);
margin: 0; margin: 0;
padding: 20px; padding: 0;
height: 100vh; height: 100vh;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 20px; padding: 20px;
padding-bottom: 15px; overflow: hidden;
border-bottom: 1px solid var(--vscode-panel-border); display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
}
.header.hidden {
display: none;
} }
.header h1 { .header h1 {
color: var(--vscode-button-background); color: var(--vscode-button-background);
@ -33,125 +60,23 @@ export function getWebviewContent(iconUri?: string): string {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 0 20px 20px 20px;
} }
.messages { ${getMessageAreaStyles()}
flex: 1; ${getWaveformPreviewContent()}
overflow-y: auto; ${getConversationHistoryBarStyles()}
margin-bottom: 15px; ${getInputAreaStyles()}
}
.message {
margin-bottom: 12px;
padding: 10px 15px;
border-radius: 8px;
max-width: 80%;
}
.user-message {
background: var(--vscode-button-secondaryBackground);
margin-left: auto;
}
.bot-message {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
margin-right: auto;
}
.input-area {
border-top: 1px solid var(--vscode-panel-border);
padding-top: 15px;
}
.input-group {
display: flex;
gap: 10px;
}
textarea {
flex: 1;
padding: 10px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
font-family: inherit;
resize: none;
min-height: 40px;
}
button {
padding: 0 20px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 4px;
cursor: pointer;
}
.quick-actions {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.quick-btn {
padding: 6px 12px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.file-reader-section {
margin-bottom: 20px;
padding: 15px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
}
.file-reader-section h3 {
margin: 0 0 10px 0;
color: var(--vscode-button-background);
}
.file-input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.file-input-group input {
flex: 1;
padding: 8px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px solid var(--vscode-input-border);
border-radius: 4px;
}
.file-content {
margin-top: 15px;
padding: 10px;
background: var(--vscode-editor-background);
border: 1px solid var(--vscode-panel-border);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
.file-content.empty {
color: var(--vscode-descriptionForeground);
font-style: italic;
}
.error-message {
color: var(--vscode-errorForeground);
padding: 8px;
background: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder);
border-radius: 4px;
margin-top: 10px;
}
.file-editor-section { .file-editor-section {
margin-bottom: 20px; margin-bottom: 15px;
padding: 15px; padding: 15px;
background: var(--vscode-input-background); background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border); border: 1px solid var(--vscode-input-border);
border-radius: 8px; border-radius: 8px;
display: none; display: none;
flex-shrink: 0;
} }
.file-editor-section.active { .file-editor-section.active {
display: block; display: block;
@ -177,9 +102,276 @@ export function getWebviewContent(iconUri?: string): string {
gap: 10px; gap: 10px;
margin-top: 10px; margin-top: 10px;
} }
/* 流式消息样式 */
.streaming .message-content {
border-right: 2px solid var(--vscode-focusBorder);
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { border-color: var(--vscode-focusBorder); }
51%, 100% { border-color: transparent; }
}
/* 加载指示器样式 */
.loading-message {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
color: var(--vscode-descriptionForeground);
}
.loading-dots {
display: flex;
gap: 4px;
}
.loading-dots span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--vscode-focusBorder);
animation: loadingDot 1.4s infinite ease-in-out;
}
.loading-dots span:nth-child(1) { animation-delay: 0s; }
.loading-dots span:nth-child(2) { animation-delay: 0.2s; }
.loading-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes loadingDot {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
.loading-text {
font-size: 13px;
}
/* 工具状态样式 */
.tool-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin: 4px 0;
font-size: 12px;
border-radius: 6px;
background: var(--vscode-textBlockQuote-background);
}
.tool-status.tool-start {
border-left: 3px solid var(--vscode-charts-blue);
}
.tool-status.tool-complete {
border-left: 3px solid var(--vscode-charts-green);
}
.tool-status.tool-error {
border-left: 3px solid var(--vscode-charts-red);
}
.tool-icon {
font-size: 14px;
}
.tool-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.tool-status-text {
color: var(--vscode-descriptionForeground);
}
.tool-detail {
margin-top: 4px;
font-size: 11px;
color: var(--vscode-descriptionForeground);
white-space: pre-wrap;
max-height: 100px;
overflow-y: auto;
}
/* 用户问题样式 */
.question-message {
padding: 16px;
}
.question-text {
margin-bottom: 12px;
font-weight: 500;
}
.question-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.question-option {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.question-option:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.question-option.selected {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}
.question-message.answered .question-option:not(.selected) {
opacity: 0.5;
pointer-events: none;
}
.custom-input-container {
display: flex;
gap: 8px;
width: 100%;
margin-top: 8px;
}
.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: 6px;
font-size: 13px;
}
.custom-submit {
padding: 8px 16px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 6px;
cursor: pointer;
}
.custom-submit:hover {
background: var(--vscode-button-hoverBackground);
}
.question-message.answered .custom-input-container {
display: none;
}
/* 分段消息样式 */
.segmented-message {
padding: 0;
}
.message-segment {
padding: 10px 22 px;
}
.segment-text {
line-height: 1.6;
}
.segment-tool {
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
padding: 10px 14px;
}
.tool-segment-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.tool-segment-icon {
font-size: 14px;
}
.tool-segment-name {
font-weight: 500;
color: var(--vscode-foreground);
}
.tool-segment-result {
margin-top: 6px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
padding-left: 22px;
}
.segment-tool.tool-success {
border-left: 3px solid var(--vscode-charts-green);
}
.segment-tool.tool-error {
border-left: 3px solid var(--vscode-charts-red);
}
.segment-tool.tool-running {
border-left: 3px solid var(--vscode-charts-blue);
}
.segment-question {
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
padding: 12px 14px;
border-left: 3px solid var(--vscode-charts-orange);
}
.question-segment .question-text {
margin-bottom: 8px;
font-weight: 500;
}
.question-segment .question-options {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.question-opt {
padding: 4px 10px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border-radius: 4px;
font-size: 12px;
}
/* 状态栏样式 */
.status-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--vscode-textBlockQuote-background);
border-radius: 6px;
margin: 8px 0;
font-size: 13px;
color: var(--vscode-descriptionForeground);
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--vscode-charts-blue);
animation: statusPulse 1.5s ease-in-out infinite;
}
@keyframes statusPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
.status-bar.working .status-indicator {
background: var(--vscode-charts-orange);
}
.status-bar.success .status-indicator {
background: var(--vscode-charts-green);
animation: none;
}
.status-bar.error .status-indicator {
background: var(--vscode-charts-red);
animation: none;
}
/* 快捷操作按钮样式 */
.quick-actions {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.quick-btn {
padding: 8px 16px;
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.quick-btn:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
</style> </style>
</head> </head>
<body> <body>
${getConversationHistoryBarContent()}
<div class="header"> <div class="header">
<div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" /> <img src="${iconUri}" alt="IC Coder" style="width: 28px; height: 28px;" />
@ -188,36 +380,13 @@ export function getWebviewContent(iconUri?: string): string {
<p>专注于真实FPGA研发的Verilog智能体编程平台</p> <p>专注于真实FPGA研发的Verilog智能体编程平台</p>
</div> </div>
<div class="file-reader-section">
<h3>📁 文件读取</h3>
<div class="file-input-group">
<input
type="text"
id="filePathInput"
placeholder="输入文件路径(相对或绝对路径)..."
/>
<button onclick="readFile()">读取文件</button>
</div>
<div id="fileContent" class="file-content empty">
文件内容将在这里显示...
</div>
<div id="errorMessage" style="display: none;"></div>
</div>
<div id="fileEditorSection" class="file-editor-section">
<h3>✏️ 编辑文件: <span id="editingFileName"></span></h3>
<textarea id="fileEditorTextarea" class="file-editor-textarea"></textarea>
<div class="editor-actions">
<button onclick="saveFile()">保存修改</button>
<button onclick="cancelEdit()" style="background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground);">取消</button>
</div>
</div>
<div class="chat-container"> <div class="chat-container">
<div id="messages" class="messages"> ${getMessageAreaContent()}
<div class="message bot-message">
👋 你好!我是 IC Coder 助手,可以帮你生成代码、回答问题。 <!-- 状态栏 -->
</div> <div id="statusBar" class="status-bar" style="display: none;">
<div class="status-indicator"></div>
<span id="statusText">思考中...</span>
</div> </div>
<div class="quick-actions"> <div class="quick-actions">
@ -226,73 +395,21 @@ export function getWebviewContent(iconUri?: string): string {
<button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button> <button class="quick-btn" onclick="quickAction('testbench')">生成测试平台</button>
</div> </div>
<div class="input-area"> ${getInputAreaContent()}
<div class="input-group">
<textarea
id="messageInput"
placeholder="输入您的问题..."
onkeydown="if(event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); }"
></textarea>
<button onclick="sendMessage()">发送</button>
</div>
</div>
</div> </div>
<script> <script>
console.log('[WebView] 脚本开始执行');
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
const messagesEl = document.getElementById('messages'); console.log('[WebView] vscode API 已获取');
const messageInput = document.getElementById('messageInput'); const messageInput = document.getElementById('messageInput');
const filePathInput = document.getElementById('filePathInput'); const modeSelect = document.getElementById('modeSelect');
const fileContentEl = document.getElementById('fileContent'); const messagesEl = document.getElementById('messages');
const errorMessageEl = document.getElementById('errorMessage');
const fileEditorSection = document.getElementById('fileEditorSection');
const fileEditorTextarea = document.getElementById('fileEditorTextarea');
const editingFileName = document.getElementById('editingFileName');
let currentEditingFile = null; // 全局变量
let currentStreamingMessage = null;
function sendMessage() { let loadingIndicator = null;
const text = messageInput.value.trim(); let currentSegmentedMessage = null; // 当前分段消息容器
if (!text) return;
addMessage(text, 'user');
vscode.postMessage({ command: 'sendMessage', text: text });
messageInput.value = '';
messageInput.focus();
}
function readFile() {
const filePath = filePathInput.value.trim();
if (!filePath) {
showError('请输入文件路径');
return;
}
// 清空之前的内容和错误
fileContentEl.textContent = '正在读取文件...';
fileContentEl.className = 'file-content';
errorMessageEl.style.display = 'none';
// 发送读取文件请求
vscode.postMessage({ command: 'readFile', filePath: filePath });
}
function showError(message) {
errorMessageEl.textContent = message;
errorMessageEl.className = 'error-message';
errorMessageEl.style.display = 'block';
fileContentEl.textContent = '文件内容将在这里显示...';
fileContentEl.className = 'file-content empty';
}
function displayFileContent(content, filePath) {
fileContentEl.textContent = content;
fileContentEl.className = 'file-content';
errorMessageEl.style.display = 'none';
// 在消息区域也显示一条提示
addMessage(\`已读取文件: \${filePath}\`, 'bot');
}
function quickAction(type) { function quickAction(type) {
const questions = { const questions = {
@ -306,84 +423,173 @@ export function getWebviewContent(iconUri?: string): string {
} }
} }
function addMessage(text, sender) { // 检查 header 显示状态
const div = document.createElement('div'); function checkHeaderVisibility() {
div.className = \`message \${sender}-message\`; const header = document.querySelector('.header');
div.textContent = text; const messages = document.getElementById('messages');
messagesEl.appendChild(div); if (header && messages) {
messagesEl.scrollTop = messagesEl.scrollHeight; if (messages.children.length > 0) {
header.classList.add('hidden');
} else {
header.classList.remove('hidden');
}
}
} }
function openFileEditor(filePath, content) { messageInput.focus();
currentEditingFile = filePath;
editingFileName.textContent = filePath;
fileEditorTextarea.value = content;
fileEditorSection.classList.add('active');
// 滚动到编辑器位置
fileEditorSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
// 在消息区域显示提示
addMessage(\`正在编辑文件: \${filePath}\`, 'bot');
}
function saveFile() {
if (!currentEditingFile) {
return;
}
const content = fileEditorTextarea.value;
vscode.postMessage({
command: 'updateFile',
filePath: currentEditingFile,
content: content
});
addMessage(\`正在保存文件: \${currentEditingFile}\`, 'user');
cancelEdit();
}
function cancelEdit() {
currentEditingFile = null;
fileEditorSection.classList.remove('active');
fileEditorTextarea.value = '';
editingFileName.textContent = '';
}
// 监听来自插件的消息
window.addEventListener('message', event => { window.addEventListener('message', event => {
const message = event.data; const message = event.data;
console.log('[WebView] 收到消息:', message.command, message);
switch (message.command) { switch (message.command) {
case 'receiveMessage': case 'receiveMessage':
// 接收完整消息
addMessage(message.text, 'bot'); addMessage(message.text, 'bot');
break; break;
case 'updateStreamingMessage':
// 更新流式消息
updateOrCreateStreamingMessage(message.text);
break;
case 'updateSegments':
// 实时更新分段消息(按后端返回顺序)
console.log('[WebView] 实时更新段落, segments:', message.segments);
updateSegmentsRealtime(message.segments, message.isComplete);
// 如果对话完成,恢复发送按钮状态
if (message.isComplete && typeof setSendButtonState === 'function') {
setSendButtonState(false);
}
break;
case 'receiveSegments':
// 接收分段消息(兼容旧代码)
console.log('[WebView] 调用 renderSegments, segments:', message.segments);
renderSegments(message.segments);
break;
case 'toolStart':
// 工具开始执行
addToolStatus(message.toolName, 'start');
break;
case 'toolComplete':
// 工具执行完成
addToolStatus(message.toolName, 'complete', message.result);
break;
case 'toolError':
// 工具执行错误
addToolStatus(message.toolName, 'error', message.error);
break;
case 'updateStatus':
// 更新状态栏
const statusBar = document.getElementById('statusBar');
const statusText = document.getElementById('statusText');
if (statusBar && statusText) {
statusBar.style.display = 'flex';
statusText.textContent = message.text;
statusBar.className = 'status-bar ' + (message.type || '');
}
break;
case 'hideStatus':
// 隐藏状态栏
const statusBarHide = document.getElementById('statusBar');
if (statusBarHide) {
statusBarHide.style.display = 'none';
}
break;
case 'hideLoading':
// 隐藏加载指示器
hideLoadingIndicator();
break;
case 'vcdInfo':
// 渲染迷你波形预览信息
try {
if (message.containerId && typeof renderWaveformInfo === 'function') {
renderWaveformInfo(message.containerId, message.vcdInfo || {});
}
} catch (e) {
console.warn('[WebView] 渲染波形信息失败:', e);
}
break;
case 'vcdGenerated':
// VCD 文件生成成功,添加消息并附带波形预览
addMessage(message.text, 'bot');
try {
if (message.vcdFilePath) {
const lastMsg = messagesEl ? messagesEl.lastElementChild : null;
if (lastMsg && typeof addWaveformPreviewToMessage === 'function') {
addWaveformPreviewToMessage(lastMsg, message.vcdFilePath, message.fileName || 'waveform.vcd');
}
}
} catch (e) {
console.warn('[WebView] 添加波形预览失败:', e);
}
break;
case 'fileContent': case 'fileContent':
displayFileContent(message.content, message.filePath); // 文件内容
addMessage('文件内容:\\n' + message.content, 'bot');
break; break;
case 'fileError': case 'fileError':
showError(message.error); // 文件错误
addMessage('❌ ' + message.error, 'bot');
break; break;
case 'editFile':
openFileEditor(message.filePath, message.content); case 'showQuestion':
// 显示用户问题
showQuestion(message.askId, message.question, message.options);
break; break;
case 'fileUpdated':
addMessage(\`\${message.message}: \${message.filePath}\`, 'bot'); case 'conversationHistory':
// 渲染会话历史列表(支持分页)
renderConversationHistory({
items: message.items || [],
total: message.total || 0,
hasMore: message.hasMore || false
});
break; break;
case 'fileUpdateError':
addMessage(\`\${message.error}\`, 'bot'); case 'clearChat':
// 清空聊天界面
const messagesContainer = document.getElementById('messages');
if (messagesContainer) {
messagesContainer.innerHTML = '';
}
break; break;
case 'addUserMessage':
// 添加用户消息
if (message.text) {
addMessage(message.text, 'user');
}
break;
case 'addAiMessage':
// 添加AI消息
if (message.text) {
addMessage(message.text, 'bot');
}
break;
default:
console.log('[WebView] 未处理的消息类型:', message.command);
} }
}); });
// 支持回车键读取文件 ${getMessageAreaScript()}
filePathInput.addEventListener('keydown', (event) => { ${getWaveformPreviewScript()}
if (event.key === 'Enter') { ${getConversationHistoryBarScript()}
readFile(); ${getInputAreaScript()}
} </script></body>
});
messageInput.focus();
</script>
</body>
</html>`; </html>`;
} }

View File

@ -0,0 +1,183 @@
# Iverilog 工具下载和安装说明
## 重要提示
由于网络限制,需要手动下载 iverilog 工具并放置到插件包中。
## 快速开始
### 步骤 1下载 Iverilog
**Windows x64 用户:**
1. 访问官方下载页面http://bleyer.org/icarus/
2. 下载文件:`iverilog-v12-20220611-x64_setup.exe` (约 18.2 MB)
3. 保存到任意位置
**备用下载地址:**
- GitHub Releases: https://github.com/steveicarus/iverilog/releases
- SourceForge: https://sourceforge.net/projects/iverilog/
### 步骤 2安装到临时目录
1. 运行下载的安装程序 `iverilog-v12-20220611-x64_setup.exe`
2. 选择安装路径(建议:`C:\iverilog-temp`
3. 完成安装
### 步骤 3复制文件到插件目录
**需要复制的文件:**
#### A. 可执行文件(从 `C:\iverilog-temp\bin\` 复制到 `tools\iverilog\bin\`
```
iverilog.exe # Verilog 编译器
vvp.exe # Verilog 仿真器
```
#### B. 依赖的 DLL 文件(从 `C:\iverilog-temp\bin\` 复制到 `tools\iverilog\bin\`
```
libgcc_s_seh-1.dll
libwinpthread-1.dll
libstdc++-6.dll
```
如果运行时提示缺少其他 DLL也需要从安装目录复制。
#### C. 库文件目录(重要!)
将整个 `C:\iverilog-temp\lib\` 目录复制到 `tools\iverilog\lib\`
最终目录结构:
```
tools\iverilog\lib\ivl\
├── system.vpi
├── v2005_math.vpi
├── vhdl_sys.vpi
└── ... (其他 .vpi 和 .vpl 文件)
```
### 步骤 4验证安装
在命令行中运行:
```bash
cd "D:\IC Coder Plugin\ic-coder\tools\iverilog\bin"
.\iverilog.exe -V
```
应该看到版本信息:
```
Icarus Verilog version 12.0 (stable) (s20220611-xxx)
```
### 步骤 5清理
安装完成后,可以:
1. 卸载临时安装的 iverilog通过控制面板
2. 删除临时安装目录 `C:\iverilog-temp`
## 最终目录结构
```
D:\IC Coder Plugin\ic-coder\
└── tools\
└── iverilog\
├── bin\
│ ├── iverilog.exe
│ ├── vvp.exe
│ ├── libgcc_s_seh-1.dll
│ ├── libwinpthread-1.dll
│ └── libstdc++-6.dll
├── lib\
│ └── ivl\
│ ├── system.vpi
│ ├── v2005_math.vpi
│ └── ... (其他库文件)
├── README.md
├── INSTALL.md
└── DOWNLOAD_INSTRUCTIONS.md (本文件)
```
## 文件大小参考
- `iverilog.exe`: ~2 MB
- `vvp.exe`: ~1 MB
- DLL 文件: ~1-2 MB
- lib/ivl/ 目录: ~3-5 MB
- **总计**: 约 7-10 MB
## 自动化脚本(可选)
如果你已经安装了 iverilog可以使用以下 PowerShell 脚本自动复制文件:
```powershell
# 设置路径
$iverilogInstallPath = "C:\iverilog" # 修改为你的安装路径
$pluginToolsPath = "D:\IC Coder Plugin\ic-coder\tools\iverilog"
# 创建目录
New-Item -ItemType Directory -Force -Path "$pluginToolsPath\bin"
New-Item -ItemType Directory -Force -Path "$pluginToolsPath\lib"
# 复制可执行文件
Copy-Item "$iverilogInstallPath\bin\iverilog.exe" "$pluginToolsPath\bin\"
Copy-Item "$iverilogInstallPath\bin\vvp.exe" "$pluginToolsPath\bin\"
# 复制 DLL 文件
Copy-Item "$iverilogInstallPath\bin\*.dll" "$pluginToolsPath\bin\"
# 复制库文件
Copy-Item "$iverilogInstallPath\lib\*" "$pluginToolsPath\lib\" -Recurse
Write-Host "复制完成!"
```
保存为 `copy-iverilog.ps1` 并在 PowerShell 中运行。
## 许可证信息
Icarus Verilog 使用 **GPL v2+** 许可证,允许自由分发。
- 官方网站http://iverilog.icarus.com/
- GitHubhttps://github.com/steveicarus/iverilog
- 许可证文本https://github.com/steveicarus/iverilog/blob/master/COPYING
## 需要帮助?
如果遇到问题,请:
1. 检查是否复制了所有必需的文件
2. 确认 DLL 文件都在 bin 目录中
3. 验证 lib/ivl 目录包含所有 .vpi 文件
4. 查看插件的错误日志
## 其他平台
### macOS
```bash
# 使用 Homebrew 安装
brew install icarus-verilog
# 复制文件到插件目录
cp /usr/local/bin/iverilog "tools/iverilog/bin/"
cp /usr/local/bin/vvp "tools/iverilog/bin/"
cp -r /usr/local/lib/ivl "tools/iverilog/lib/"
```
### Linux
```bash
# Ubuntu/Debian
sudo apt-get install iverilog
# 复制文件到插件目录
cp /usr/bin/iverilog "tools/iverilog/bin/"
cp /usr/bin/vvp "tools/iverilog/bin/"
cp -r /usr/lib/ivl "tools/iverilog/lib/"
```
---
**注意**:完成上述步骤后,插件即可使用内置的 iverilog 工具进行 Verilog 编译和 VCD 文件生成。

131
tools/iverilog/INSTALL.md Normal file
View File

@ -0,0 +1,131 @@
# Iverilog 工具安装指南
## 自动安装(推荐)
插件会自动使用打包在 `tools/iverilog/bin/` 目录中的 iverilog 工具。
## 手动安装步骤
如果需要手动更新或安装 iverilog 工具,请按照以下步骤操作:
### Windows x64
1. **下载安装包**
- 访问http://bleyer.org/icarus/
- 下载:`iverilog-v12-20220611-x64_setup.exe` (18.2MB)
- 或直接下载http://bleyer.org/icarus/iverilog-v12-20220611-x64_setup.exe
2. **安装到临时目录**
- 运行安装程序
- 选择安装路径(例如:`C:\iverilog-temp`
- 完成安装
3. **复制必要文件**
将以下文件从安装目录复制到 `tools/iverilog/bin/`
**核心可执行文件:**
- `iverilog.exe` - Verilog 编译器
- `vvp.exe` - Verilog 仿真器
**依赖的 DLL 文件:**
- `libgcc_s_seh-1.dll`
- `libwinpthread-1.dll`
- `libstdc++-6.dll`
- 以及其他可能需要的 DLL 文件
**库文件目录(重要):**
- 复制整个 `lib/ivl/` 目录到 `tools/iverilog/lib/ivl/`
- 这个目录包含 iverilog 的标准库和模块
4. **目录结构**
最终的目录结构应该是:
```
tools/
└── iverilog/
├── bin/
│ ├── iverilog.exe
│ ├── vvp.exe
│ ├── libgcc_s_seh-1.dll
│ ├── libwinpthread-1.dll
│ └── libstdc++-6.dll
├── lib/
│ └── ivl/
│ ├── system.vpi
│ ├── v2005_math.vpi
│ └── ... (其他库文件)
└── README.md
```
5. **清理**
- 可以卸载临时安装的 iverilog
- 删除临时安装目录
### 验证安装
在命令行中运行:
```bash
cd "D:\IC Coder Plugin\ic-coder\tools\iverilog\bin"
.\iverilog.exe -V
```
应该看到类似输出:
```
Icarus Verilog version 12.0 (stable) (s20220611-xxx)
```
## 文件清单
### 必需文件(约 5-10 MB
**bin/ 目录:**
- iverilog.exe (~2MB)
- vvp.exe (~1MB)
- libgcc_s_seh-1.dll
- libwinpthread-1.dll
- libstdc++-6.dll
**lib/ivl/ 目录:**
- system.vpi
- v2005_math.vpi
- vhdl_sys.vpi
- vhdl_textio.vpi
- va_math.vpi
- 以及其他 .vpi 和 .vpl 文件
## 许可证
Icarus Verilog 使用 GPL v2+ 许可证。
- 官方网站http://iverilog.icarus.com/
- 源代码https://github.com/steveicarus/iverilog
- 许可证https://github.com/steveicarus/iverilog/blob/master/COPYING
## 注意事项
1. **文件大小**:完整的 iverilog 工具约 5-10 MB不包含 GTKWave
2. **依赖项**:确保复制所有必需的 DLL 文件
3. **库文件**lib/ivl/ 目录是必需的,包含 Verilog 标准库
4. **版本**:推荐使用 v12.0 或更高版本
## 故障排除
### 问题:运行时提示缺少 DLL
**解决方案**
- 使用 Dependency Walker 或 Dependencies.exe 检查缺少的 DLL
- 从 iverilog 安装目录复制缺少的 DLL 到 bin/ 目录
### 问题:编译时提示找不到标准库
**解决方案**
- 确保 lib/ivl/ 目录存在且包含所有 .vpi 文件
- 检查 iverilog 是否能找到库文件路径
### 问题vvp 运行失败
**解决方案**
- 确保 vvp.exe 和相关 DLL 都在 bin/ 目录中
- 检查是否有权限问题

247
tools/iverilog/README.md Normal file
View File

@ -0,0 +1,247 @@
# Iverilog 工具集成说明
本插件**已集成** Icarus Verilog (iverilog) 工具,用于 Verilog 代码的编译和仿真,以及 VCD 波形文件的生成。
## 目录结构
```
tools/
└── iverilog/
├── bin/ # 可执行文件目录(已包含 iverilog 工具)
│ ├── iverilog.exe # Verilog 编译器 (Windows)
│ ├── vvp.exe # Verilog 仿真器 (Windows)
│ └── ... # 其他依赖文件
└── README.md # 本说明文件
```
## 工具说明
插件包中**已包含** Windows x64 版本的 iverilog 工具,无需额外安装。
- **版本**: Icarus Verilog 11.0 (或更高)
- **平台**: Windows x64
- **许可证**: GPL v2+
### Windows 安装
1. 访问 Icarus Verilog 官方网站http://bleyer.org/icarus/
2. 下载最新的 Windows 安装包例如iverilog-v11-20210204-x64_setup.exe
3. 运行安装程序,按照提示完成安装
4. 安装完成后,将以下文件复制到插件的 `tools/iverilog/bin/` 目录:
- `iverilog.exe`
- `vvp.exe`
- 以及相关的 DLL 文件(如果有)
**或者**,将 iverilog 安装目录添加到系统 PATH 环境变量中,插件会自动使用系统安装的版本。
### macOS 安装
使用 Homebrew 安装:
```bash
brew install icarus-verilog
```
安装完成后iverilog 会自动添加到系统 PATH 中,插件可以直接使用。
**可选**:如果想将 iverilog 打包到插件中,可以将以下文件复制到 `tools/iverilog/bin/`
- `/usr/local/bin/iverilog`
- `/usr/local/bin/vvp`
### Linux 安装
#### Ubuntu/Debian
```bash
sudo apt-get update
sudo apt-get install iverilog
```
#### Fedora/CentOS
```bash
sudo yum install iverilog
```
#### Arch Linux
```bash
sudo pacman -S iverilog
```
安装完成后iverilog 会自动添加到系统 PATH 中,插件可以直接使用。
## 使用方法
### 1. 准备 Verilog 项目
确保您的项目包含以下文件:
- **顶层模块文件**:包含您的设计代码(例如:`counter.v`
- **Testbench 文件**:包含测试代码,文件名通常包含 `tb``test`(例如:`counter_tb.v`
### 2. Testbench 要求
为了生成 VCD 波形文件,您的 testbench 必须包含以下语句:
```verilog
module counter_tb;
// ... 信号声明 ...
initial begin
// 生成 VCD 文件
$dumpfile("output.vcd"); // 指定 VCD 文件名
$dumpvars(0, counter_tb); // 记录所有信号
// ... 测试代码 ...
#1000 $finish; // 结束仿真
end
// ... 其他测试代码 ...
endmodule
```
### 3. 生成 VCD 文件
在 IC Coder 插件的聊天界面中,输入以下任一命令:
- `生成 VCD`
- `创建 VCD`
- `运行仿真`
- `执行仿真`
- `生成波形`
插件会自动:
1. 检查项目文件完整性
2. 使用 iverilog 编译所有 Verilog 文件
3. 使用 vvp 运行仿真
4. 在项目根目录生成 `output.vcd` 文件
### 4. 查看波形
生成的 VCD 文件可以使用以下工具查看:
- **GTKWave**(推荐):开源波形查看器
- Windows: http://gtkwave.sourceforge.net/
- macOS: `brew install gtkwave`
- Linux: `sudo apt-get install gtkwave`
- **其他工具**
- ModelSim
- Vivado
- 在线 VCD 查看器
## 示例项目
### counter.v顶层模块
```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 <= 4'b0000;
else
count <= count + 1;
end
endmodule
```
### counter_tb.vTestbench
```verilog
`timescale 1ns/1ps
module counter_tb;
reg clk;
reg rst_n;
wire [3:0] count;
// 实例化被测模块
counter uut (
.clk(clk),
.rst_n(rst_n),
.count(count)
);
// 生成时钟信号
initial begin
clk = 0;
forever #5 clk = ~clk; // 10ns 周期
end
// 测试序列
initial begin
// 生成 VCD 文件
$dumpfile("output.vcd");
$dumpvars(0, counter_tb);
// 初始化
rst_n = 0;
#20;
// 释放复位
rst_n = 1;
#200;
// 结束仿真
$finish;
end
// 监控输出
initial begin
$monitor("Time=%0t rst_n=%b count=%d", $time, rst_n, count);
end
endmodule
```
## 故障排除
### 问题:提示 "iverilog 不可用"
**解决方案**
1. 确认已安装 iverilog
2. 检查 iverilog 是否在系统 PATH 中:
- Windows: 在命令提示符中运行 `iverilog -V`
- macOS/Linux: 在终端中运行 `iverilog -V`
3. 或者将 iverilog 可执行文件复制到 `tools/iverilog/bin/` 目录
### 问题:提示 "项目文件不完整"
**解决方案**
1. 确保项目中至少有一个 `.v``.sv` 文件
2. 确保有 testbench 文件(文件名包含 `tb``test`
3. 确保 testbench 中包含 `$dumpfile``$dumpvars` 语句
### 问题:编译失败
**解决方案**
1. 检查 Verilog 代码语法错误
2. 查看错误输出信息
3. 确保所有模块都正确实例化
### 问题VCD 文件未生成
**解决方案**
1. 确保 testbench 中包含 `$dumpfile("output.vcd")` 语句
2. 确保 testbench 中包含 `$dumpvars` 语句
3. 确保仿真运行了足够的时间(使用 `#时间 $finish;`
## 版本信息
- 推荐 Icarus Verilog 版本v11.0 或更高
- 支持的 Verilog 标准Verilog-1995, Verilog-2001, Verilog-2005, SystemVerilog (部分)
## 相关链接
- Icarus Verilog 官网http://iverilog.icarus.com/
- GTKWave 官网http://gtkwave.sourceforge.net/
- Verilog 教程https://www.asic-world.com/verilog/
## 许可证
Icarus Verilog 是开源软件,遵循 GPL 许可证。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
tools/iverilog/bin/vvp.exe Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
@echo off
chcp 65001 >nul
echo =====================================
echo Iverilog 文件复制脚本
echo =====================================
echo.
REM 检查 PowerShell 是否可用
where powershell >nul 2>&1
if %errorlevel% neq 0 (
echo 错误: 未找到 PowerShell
pause
exit /b 1
)
REM 运行 PowerShell 脚本
powershell -ExecutionPolicy Bypass -File "%~dp0copy-iverilog.ps1"
echo.
pause

View File

@ -0,0 +1,136 @@
# Iverilog 文件自动复制脚本
# 用于将已安装的 iverilog 复制到插件目录
param(
[string]$IverilogPath = "C:\iverilog"
)
# 设置插件工具路径
$PluginPath = Split-Path -Parent $PSScriptRoot
$ToolsPath = Join-Path $PSScriptRoot ""
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host "Iverilog 文件复制脚本" -ForegroundColor Cyan
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host ""
# 检查源路径是否存在
if (-not (Test-Path $IverilogPath)) {
Write-Host "错误: 找不到 iverilog 安装目录: $IverilogPath" -ForegroundColor Red
Write-Host ""
Write-Host "请指定正确的 iverilog 安装路径,例如:" -ForegroundColor Yellow
Write-Host " .\copy-iverilog.ps1 -IverilogPath 'C:\Program Files\iverilog'" -ForegroundColor Yellow
Write-Host ""
# 尝试查找常见安装位置
$CommonPaths = @(
"C:\iverilog",
"C:\Program Files\iverilog",
"C:\Program Files (x86)\iverilog",
"$env:LOCALAPPDATA\iverilog"
)
Write-Host "正在搜索常见安装位置..." -ForegroundColor Yellow
foreach ($path in $CommonPaths) {
if (Test-Path $path) {
Write-Host "找到: $path" -ForegroundColor Green
$IverilogPath = $path
break
}
}
if (-not (Test-Path $IverilogPath)) {
Write-Host "未找到 iverilog 安装目录" -ForegroundColor Red
exit 1
}
}
Write-Host "源路径: $IverilogPath" -ForegroundColor Green
Write-Host "目标路径: $ToolsPath" -ForegroundColor Green
Write-Host ""
# 创建目标目录
Write-Host "创建目录结构..." -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path "$ToolsPath\bin" | Out-Null
New-Item -ItemType Directory -Force -Path "$ToolsPath\lib" | Out-Null
# 复制可执行文件
Write-Host "复制可执行文件..." -ForegroundColor Cyan
$executables = @("iverilog.exe", "vvp.exe")
foreach ($exe in $executables) {
$sourcePath = Join-Path "$IverilogPath\bin" $exe
if (Test-Path $sourcePath) {
Copy-Item $sourcePath "$ToolsPath\bin\" -Force
Write-Host "$exe" -ForegroundColor Green
} else {
Write-Host " ✗ 未找到 $exe" -ForegroundColor Red
}
}
# 复制 DLL 文件
Write-Host "复制 DLL 文件..." -ForegroundColor Cyan
$dlls = Get-ChildItem "$IverilogPath\bin\*.dll" -ErrorAction SilentlyContinue
if ($dlls) {
foreach ($dll in $dlls) {
Copy-Item $dll.FullName "$ToolsPath\bin\" -Force
Write-Host "$($dll.Name)" -ForegroundColor Green
}
} else {
Write-Host " ! 未找到 DLL 文件" -ForegroundColor Yellow
}
# 复制库文件
Write-Host "复制库文件..." -ForegroundColor Cyan
$libPath = Join-Path $IverilogPath "lib"
if (Test-Path $libPath) {
Copy-Item "$libPath\*" "$ToolsPath\lib\" -Recurse -Force
# 统计复制的文件
$vpiFiles = Get-ChildItem "$ToolsPath\lib\ivl\*.vpi" -ErrorAction SilentlyContinue
$vplFiles = Get-ChildItem "$ToolsPath\lib\ivl\*.vpl" -ErrorAction SilentlyContinue
Write-Host " ✓ 复制了 $($vpiFiles.Count) 个 .vpi 文件" -ForegroundColor Green
Write-Host " ✓ 复制了 $($vplFiles.Count) 个 .vpl 文件" -ForegroundColor Green
} else {
Write-Host " ✗ 未找到 lib 目录" -ForegroundColor Red
}
Write-Host ""
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host "验证安装..." -ForegroundColor Cyan
Write-Host "=====================================" -ForegroundColor Cyan
# 验证 iverilog
$iverilogExe = Join-Path "$ToolsPath\bin" "iverilog.exe"
if (Test-Path $iverilogExe) {
Write-Host ""
Write-Host "运行 iverilog -V:" -ForegroundColor Yellow
& $iverilogExe -V
Write-Host ""
} else {
Write-Host "错误: iverilog.exe 未找到" -ForegroundColor Red
}
# 显示文件大小统计
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host "文件大小统计" -ForegroundColor Cyan
Write-Host "=====================================" -ForegroundColor Cyan
$binSize = (Get-ChildItem "$ToolsPath\bin" -Recurse -File | Measure-Object -Property Length -Sum).Sum
$libSize = (Get-ChildItem "$ToolsPath\lib" -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
$totalSize = $binSize + $libSize
Write-Host "bin/ 目录: $([math]::Round($binSize / 1MB, 2)) MB" -ForegroundColor Green
Write-Host "lib/ 目录: $([math]::Round($libSize / 1MB, 2)) MB" -ForegroundColor Green
Write-Host "总计: $([math]::Round($totalSize / 1MB, 2)) MB" -ForegroundColor Cyan
Write-Host ""
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host "复制完成!" -ForegroundColor Green
Write-Host "=====================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "现在可以使用插件的 VCD 生成功能了。" -ForegroundColor Green
Write-Host ""

View File

@ -0,0 +1,134 @@
# Iverilog 测试示例
这个目录包含一个简单的 Verilog 项目示例,用于测试 IC Coder 插件的 VCD 生成功能。
## 文件说明
- **counter.v** - 一个简单的 4 位计数器模块
- **counter_tb.v** - 计数器的测试平台testbench
## 功能说明
### counter.v顶层模块
这是一个同步复位的 4 位计数器:
- **输入**
- `clk` - 时钟信号
- `rst_n` - 低电平有效的复位信号
- **输出**
- `count[3:0]` - 4 位计数值0-15
### counter_tb.v测试平台
测试平台包含:
- 时钟生成器100MHz周期 10ns
- 复位序列(初始复位 20ns
- VCD 波形文件生成
- 信号监控和显示
## 使用方法
### 方法 1使用 IC Coder 插件(推荐)
1. 在 VS Code 中打开这个 `examples` 目录作为工作区
2. 打开 IC Coder 插件面板
3. 在聊天框中输入以下任一命令:
- `生成 VCD`
- `运行仿真`
- `生成波形`
4. 插件会自动编译并运行仿真,生成 `output.vcd` 文件
### 方法 2手动运行用于测试
在命令行中执行:
```bash
# 进入示例目录
cd "D:\IC Coder Plugin\ic-coder\tools\iverilog\examples"
# 编译 Verilog 文件
"D:\IC Coder Plugin\ic-coder\tools\iverilog\bin\iverilog.exe" -o simulation.vvp counter.v counter_tb.v
# 运行仿真
"D:\IC Coder Plugin\ic-coder\tools\iverilog\bin\vvp.exe" simulation.vvp
# 查看生成的 VCD 文件
ls -lh output.vcd
```
## 预期输出
### 控制台输出
```
Time=0 ns, rst_n=0, count=0 (0x0)
Time=20 ns, rst_n=1, count=0 (0x0)
Time=25 ns, rst_n=1, count=1 (0x1)
Time=35 ns, rst_n=1, count=2 (0x2)
Time=45 ns, rst_n=1, count=3 (0x3)
...
Time=215 ns, rst_n=1, count=15 (0xf)
Final count value: 15
```
### 生成的文件
- **simulation.vvp** - 编译后的中间文件(可以删除)
- **output.vcd** - VCD 波形文件(约 2-5 KB
## 查看波形
使用 GTKWave 查看生成的 VCD 文件:
```bash
gtkwave output.vcd
```
在 GTKWave 中:
1. 在左侧 SST 窗口选择 `counter_tb`
2. 将信号 `clk``rst_n``count[3:0]` 拖到波形窗口
3. 点击 "Zoom Fit" 查看完整波形
## 波形说明
你应该能看到:
- **clk** - 规则的时钟信号10ns 周期)
- **rst_n** - 初始 20ns 为低电平,然后保持高电平
- **count** - 从 0 开始递增到 15 的计数值
## 故障排除
### 问题:编译失败
**检查**
- 确认 iverilog.exe 在 `tools/iverilog/bin/` 目录中
- 确认所有 DLL 文件都已复制
### 问题VCD 文件未生成
**检查**
- testbench 中是否包含 `$dumpfile("output.vcd");`
- testbench 中是否包含 `$dumpvars(0, counter_tb);`
- 仿真是否正常结束(有 `$finish;`
### 问题:路径错误
**解决**
- 确保在正确的工作区目录中运行
- 检查路径中是否有特殊字符
## 扩展练习
你可以修改这个示例来学习:
1. **修改计数器位宽**:将 4 位改为 8 位
2. **添加使能信号**:只在使能时计数
3. **添加加载功能**:可以加载初始值
4. **创建递减计数器**:向下计数
5. **添加溢出标志**:计数到最大值时输出标志
## 参考资料
- Verilog 语法https://www.asic-world.com/verilog/
- Icarus Verilog 文档http://iverilog.icarus.com/
- GTKWave 使用http://gtkwave.sourceforge.net/

View File

@ -0,0 +1,17 @@
// counter.v - 简单的 4 位计数器模块
module counter (
input wire clk,
input wire rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 4'b0000;
end else begin
count <= count + 1;
end
end
endmodule

View File

@ -0,0 +1,54 @@
// counter_tb.v - 计数器测试平台
`timescale 1ns/1ps
module counter_tb;
// 信号声明
reg clk;
reg rst_n;
wire [3:0] count;
// 实例化被测模块
counter uut (
.clk(clk),
.rst_n(rst_n),
.count(count)
);
// 生成时钟信号 (10ns 周期 = 100MHz)
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 测试序列
initial begin
// 生成 VCD 波形文件
$dumpfile("output.vcd");
$dumpvars(0, counter_tb);
// 初始化信号
rst_n = 0;
// 等待 20ns 后释放复位
#20;
rst_n = 1;
// 运行 200ns 让计数器计数
#200;
// 显示最终计数值
$display("Final count value: %d", count);
// 结束仿真
$finish;
end
// 监控输出变化
initial begin
$monitor("Time=%0t ns, rst_n=%b, count=%d (0x%h)",
$time, rst_n, count, count);
end
endmodule

View File

@ -0,0 +1,43 @@
// Mathematical and physical constants
`ifdef CONSTANTS_VAMS
`else
`define CONSTANTS_VAMS 1
// M_ is a mathematical constant
`define M_E 2.7182818284590452354
`define M_LOG2E 1.4426950408889634074
`define M_LOG10E 0.43429448190325182765
`define M_LN2 0.69314718055994530942
`define M_LN10 2.30258509299404568402
`define M_PI 3.14159265358979323846
`define M_TWO_PI 6.28318530717958647693
`define M_PI_2 1.57079632679489661923
`define M_PI_4 0.78539816339744830962
`define M_1_PI 0.31830988618379067154
`define M_2_PI 0.63661977236758134308
`define M_2_SQRTPI 1.12837916709551257390
`define M_SQRT2 1.41421356237309504880
`define M_SQRT1_2 0.70710678118654752440
/*
* Do we need these? For now they are not available.
*
// The following constants have been taken from http://physics.nist.gov
// P_ is a physical constant
// charge of electron in coulombs
`define P_Q 1.602176462e-19
// speed of light in vacuum in meters/sec
`define P_C 2.99792458e8
// Boltzmann's constant in joules/kelvin
`define P_K 1.3806503e-23
// Planck's constant in joules*sec
`define P_H 6.62606876e-34
// permittivity of vacuum in farads/meter
`define P_EPS0 8.854187817e-12
// permeability of vacuum in henrys/meter
`define P_U0 (4.0e-7 * `M_PI)
// zero celsius in kelvin
`define P_CELSIUS0 273.15
*/
`endif

View File

@ -0,0 +1,72 @@
// Standard definitions for Verilog-AMS
`ifdef DISCIPLINES_VAMS
`else
`define DISCIPLINES_VAMS 1
discipline \logic ;
domain discrete;
enddiscipline
discipline ddiscrete;
domain discrete;
enddiscipline
nature Current;
units = "A";
access = I;
idt_nature = Charge;
`ifdef CURRENT_ABSTOL
abstol = `CURRENT_ABSTOL
`else
abstol = 1e-12;
`endif
endnature
nature Charge;
units = "coul";
access = Q;
ddt_nature = Current;
`ifdef CHARGE_ABSTOL
abstol = `CHARGE_ABSTOL;
`else
abstol = 1e-14;
`endif
endnature
nature Voltage;
units = "V";
access = V;
idt_nature = Flux;
`ifdef VOLTAGE_ABSTOL
abstol = `VOLTAGE_ABSTOL;
`else
abstol = 1e-6;
`endif
endnature
nature Flux;
units = "Wb";
access = Phi;
ddt_nature = Voltage;
`ifdef FLUX_ABSTOL
abstol = `flux_ABSTOL;
`else
abstol = 1e-9;
`endif
endnature
discipline electrical;
potential Voltage;
flow Current;
enddiscipline
discipline voltage;
potential Voltage;
enddiscipline
discipline current;
flow Current;
enddiscipline
`endif // !`ifdef DISCIPLINES_VAMS

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=blif.tgt

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=blif.tgt

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
functor:synth2
functor:synth
functor:syn-rules
flag:DLL=null.tgt

View File

@ -0,0 +1 @@
flag:DLL=null.tgt

Binary file not shown.

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=pcb.tgt

View File

@ -0,0 +1,3 @@
functor:cprop
functor:nodangle
flag:DLL=pcb.tgt

Binary file not shown.

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=sizer.tgt

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=sizer.tgt

Binary file not shown.

View File

@ -0,0 +1,6 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
flag:DLL=stub.tgt

View File

@ -0,0 +1,3 @@
functor:cprop
functor:nodangle
flag:DLL=stub.tgt

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
functor:synth2
functor:synth
functor:syn-rules
functor:cprop
functor:nodangle
-t:dll
flag:DLL=vhdl.tgt

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